mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
Compare commits
39 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c594ecb58d | ||
![]() |
baaf69202b | ||
![]() |
a949c698f9 | ||
![]() |
ffa0d3e944 | ||
![]() |
6d81c8cda0 | ||
![]() |
440d3f8c92 | ||
![]() |
e5b69afee2 | ||
![]() |
75d351afad | ||
![]() |
f6824e90a1 | ||
![]() |
89d84f0bdd | ||
![]() |
4620b5fd25 | ||
![]() |
6c60a8db84 | ||
![]() |
79eb38d582 | ||
![]() |
0e59a62ead | ||
![]() |
97bcc76ac1 | ||
![]() |
499f931f67 | ||
![]() |
416c6d3ca4 | ||
![]() |
36e5f8895d | ||
![]() |
b4e25fe42f | ||
![]() |
7be313e91f | ||
![]() |
36a449c935 | ||
![]() |
31829d61fc | ||
![]() |
bae37d9448 | ||
![]() |
bfea0f6836 | ||
![]() |
2a64b5c880 | ||
![]() |
15bb71764e | ||
![]() |
5b70ce6be2 | ||
![]() |
22608cb44e | ||
![]() |
f1cb656f75 | ||
![]() |
db23195f4d | ||
![]() |
4a464c13a8 | ||
![]() |
a3b44bdef5 | ||
![]() |
bc16297274 | ||
![]() |
7bf3be2f07 | ||
![]() |
768d45197b | ||
![]() |
f9c17d9b5b | ||
![]() |
1f6e7de569 | ||
![]() |
2ee5fb287c | ||
![]() |
b792018c00 |
35 changed files with 1019 additions and 423 deletions
5
.github/dependabot.yml
vendored
5
.github/dependabot.yml
vendored
|
@ -5,6 +5,11 @@ updates:
|
|||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
# Enable version updates for git submodules
|
||||
- package-ecosystem: "gitsubmodule"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
# Enable updates for Rust packages
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/" # Location of package manifests
|
||||
|
|
6
.github/workflows/docker-image.yaml
vendored
6
.github/workflows/docker-image.yaml
vendored
|
@ -33,10 +33,10 @@ jobs:
|
|||
submodules: "recursive"
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@v3.8.1
|
||||
uses: sigstore/cosign-installer@v3.9.0
|
||||
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v3.3.0
|
||||
uses: docker/login-action@v3.4.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.repository_owner }}
|
||||
|
@ -44,7 +44,7 @@ jobs:
|
|||
|
||||
- name: Build and push Taskwarrior Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v6.15.0
|
||||
uses: docker/build-push-action@v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: "./docker/task.dockerfile"
|
||||
|
|
|
@ -9,7 +9,7 @@ repos:
|
|||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
rev: v19.1.7
|
||||
rev: v20.1.6
|
||||
hooks:
|
||||
- id: clang-format
|
||||
types_or: [c++, c]
|
||||
|
|
|
@ -4,7 +4,7 @@ enable_testing()
|
|||
set (CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
project (task
|
||||
VERSION 3.4.0
|
||||
VERSION 3.4.1
|
||||
DESCRIPTION "Taskwarrior - a command-line TODO list manager"
|
||||
HOMEPAGE_URL https://taskwarrior.org/)
|
||||
|
||||
|
@ -67,7 +67,6 @@ SET (TASK_BINDIR bin CACHE STRING "Installation directory for the bi
|
|||
# rust libs require these
|
||||
set (TASK_LIBRARIES dl pthread)
|
||||
|
||||
check_function_exists (timegm HAVE_TIMEGM)
|
||||
check_function_exists (get_current_dir_name HAVE_GET_CURRENT_DIR_NAME)
|
||||
check_function_exists (wordexp HAVE_WORDEXP)
|
||||
|
||||
|
|
1150
Cargo.lock
generated
1150
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,11 @@
|
|||
------ current release ---------------------------
|
||||
|
||||
3.4.1 -
|
||||
|
||||
- The nagging to read `task news` has been fixed.
|
||||
|
||||
------ old releases ------------------------------
|
||||
|
||||
3.4.0 -
|
||||
|
||||
- Where possible, the task DB is now opened in read-only mode, which improves
|
||||
|
@ -19,8 +25,6 @@ Thanks to the following people for contributions to this release:
|
|||
- Yong Li
|
||||
- jrmarino
|
||||
|
||||
------ old releases ------------------------------
|
||||
|
||||
3.3.0 -
|
||||
|
||||
- Sync now supports AWS S3 as a backend.
|
||||
|
|
9
INSTALL
9
INSTALL
|
@ -34,7 +34,7 @@ Briefly, these shell commands will unpack, build and install Taskwarrior:
|
|||
$ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release . [3]
|
||||
$ cmake --build build [4]
|
||||
$ sudo cmake --install build [5]
|
||||
$ cd .. ; rm -r task-X.Y.Z [6]
|
||||
$ cd .. ; rm -r task-X.Y.Z [6] (see: Uninstallation)
|
||||
|
||||
These commands are explained below:
|
||||
|
||||
|
@ -103,6 +103,13 @@ There is no uninstall option in CMake makefiles. This is a manual process.
|
|||
To uninstall Taskwarrior, remove the files listed in the install_manifest.txt
|
||||
file that was generated when you built Taskwarrior.
|
||||
|
||||
```sh
|
||||
cd task-X.Y.Z
|
||||
sudo xargs rm < build/install_manifest.txt
|
||||
```
|
||||
|
||||
If you want to uninstall this way, you will need to omit step [6] above and
|
||||
retain the source folder after installation.
|
||||
|
||||
Taskwarrior Build Notes
|
||||
-----------------------
|
||||
|
|
|
@ -41,9 +41,6 @@
|
|||
/* Found tm_gmtoff */
|
||||
#cmakedefine HAVE_TM_GMTOFF
|
||||
|
||||
/* Found timegm */
|
||||
#cmakedefine HAVE_TIMEGM
|
||||
|
||||
/* Found st.st_birthtime struct member */
|
||||
#cmakedefine HAVE_ST_BIRTHTIME
|
||||
|
||||
|
|
|
@ -143,7 +143,9 @@ Then configure Taskwarrior with:
|
|||
|
||||
To synchronize your tasks to AWS, select a region near you and use the AWS
|
||||
console to create a new S3 bucket. The default settings for the bucket are
|
||||
adequate.
|
||||
adequate. In particular, ensure that no lifecycle policies are enabled, as they
|
||||
may automatically delete or transition objects, potentially impacting data
|
||||
availability.
|
||||
|
||||
You will also need an AWS IAM user with the following policy, where BUCKETNAME
|
||||
is the name of the bucket. The same user can be configured for multiple
|
||||
|
|
|
@ -1384,7 +1384,7 @@ if you define a UDA named 'estimate', Taskwarrior will not know that this value
|
|||
is weeks, hours, minutes, money, or some other resource count.
|
||||
|
||||
.TP
|
||||
.B uda.<name>.type=string|numeric|date|duration
|
||||
.B uda.<name>.type=string|numeric|uuid|date|duration
|
||||
.RS
|
||||
Defines a UDA called '<name>', of the specified type.
|
||||
.RE
|
||||
|
|
|
@ -1173,6 +1173,13 @@ void Context::staticInitialization() {
|
|||
void Context::createDefaultConfig() {
|
||||
// Do we need to create a default rc?
|
||||
if (rc_file._data != "" && !rc_file.exists()) {
|
||||
// If stdout is not a file, we are probably executing in a completion context and should not
|
||||
// prompt (as the user won't see it) or modify the config (as completion functions are typically
|
||||
// read-only).
|
||||
if (!isatty(STDOUT_FILENO)) {
|
||||
throw std::string("Cannot proceed without rc file.");
|
||||
}
|
||||
|
||||
if (config.getBoolean("confirmation") &&
|
||||
!confirm(format("A configuration file could not be found in {1}\n\nWould you like a sample "
|
||||
"{2} created, so Taskwarrior can proceed?",
|
||||
|
|
|
@ -286,6 +286,13 @@ bool getDOM(const std::string& name, const Task* task, Variant& value) {
|
|||
return true;
|
||||
}
|
||||
|
||||
// The "tags" property is deprecated, but it is documented as part of the DOM, so simulate it.
|
||||
if (size == 1 && canonical == "tags") {
|
||||
auto tags = ref->getTags();
|
||||
value = Variant(join(",", tags));
|
||||
return true;
|
||||
}
|
||||
|
||||
Column* column = Context::getContext().columns[canonical];
|
||||
|
||||
if (size == 1 && column) {
|
||||
|
|
|
@ -275,7 +275,7 @@ void Hooks::onAdd(Task& task) const {
|
|||
// - all emitted non-JSON lines are considered feedback or error messages
|
||||
// depending on the status code.
|
||||
//
|
||||
void Hooks::onModify(const Task& before, Task& after) const {
|
||||
void Hooks::onModify(Task& before, Task& after) const {
|
||||
if (!_enabled) return;
|
||||
|
||||
Timer timer;
|
||||
|
|
|
@ -40,7 +40,7 @@ class Hooks {
|
|||
void onLaunch() const;
|
||||
void onExit() const;
|
||||
void onAdd(Task&) const;
|
||||
void onModify(const Task&, Task&) const;
|
||||
void onModify(Task&, Task&) const;
|
||||
std::vector<std::string> list() const;
|
||||
|
||||
private:
|
||||
|
|
|
@ -354,8 +354,7 @@ bool TDB2::get(const std::string& uuid, Task& task) {
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Locate task by UUID, wherever it is.
|
||||
bool TDB2::has(const std::string& uuid) {
|
||||
Task task;
|
||||
return get(uuid, task);
|
||||
return replica()->get_task_data(tc::uuid_from_string(uuid)).is_some();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -777,7 +777,7 @@ void Task::parseLegacy(const std::string& line) {
|
|||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
std::string Task::composeJSON(bool decorate /*= false*/) const {
|
||||
std::string Task::composeJSON(bool decorate /*= false*/) {
|
||||
std::stringstream out;
|
||||
out << '{';
|
||||
|
||||
|
@ -894,7 +894,7 @@ std::string Task::composeJSON(bool decorate /*= false*/) const {
|
|||
|
||||
#ifdef PRODUCT_TASKWARRIOR
|
||||
// Include urgency.
|
||||
if (decorate) out << ',' << "\"urgency\":" << urgency_c();
|
||||
if (decorate) out << ',' << "\"urgency\":" << urgency();
|
||||
#endif
|
||||
|
||||
out << '}';
|
||||
|
@ -928,7 +928,7 @@ void Task::addAnnotation(const std::string& description) {
|
|||
++now;
|
||||
} while (has(key));
|
||||
|
||||
data[key] = json::decode(description);
|
||||
data[key] = description;
|
||||
++annotation_count;
|
||||
recalc_urgency = true;
|
||||
}
|
||||
|
@ -1999,7 +1999,7 @@ void Task::modify(modType type, bool text_required /* = false */) {
|
|||
// Delegate modification to the column object or their base classes.
|
||||
if (name == "depends" || name == "tags" || name == "recur" || column->type() == "date" ||
|
||||
column->type() == "duration" || column->type() == "numeric" ||
|
||||
column->type() == "string") {
|
||||
column->type() == "string" || column->type() == "uuid") {
|
||||
column->modify(*this, value);
|
||||
mods = true;
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ class Task {
|
|||
Task(rust::Box<tc::TaskData>);
|
||||
|
||||
void parse(const std::string&);
|
||||
std::string composeJSON(bool decorate = false) const;
|
||||
std::string composeJSON(bool decorate = false);
|
||||
|
||||
// Status values.
|
||||
enum status { pending, completed, deleted, recurring, waiting };
|
||||
|
|
|
@ -56,11 +56,9 @@ void ColumnTypeDuration::modify(Task& task, const std::string& value) {
|
|||
evaluatedValue = Variant(value);
|
||||
}
|
||||
|
||||
// The duration is stored in raw form, but it must still be valid,
|
||||
// and therefore is parsed first.
|
||||
// The duration is first parsed, then stored inside the variant as a `time_t`
|
||||
std::string label = " [1;37;43mMODIFICATION[0m ";
|
||||
if (evaluatedValue.type() == Variant::type_duration) {
|
||||
// Store the raw value, for 'recur'.
|
||||
Context::getContext().debug(label + _name + " <-- " + (std::string)evaluatedValue + " <-- '" +
|
||||
value + '\'');
|
||||
task.set(_name, evaluatedValue);
|
||||
|
|
|
@ -297,3 +297,23 @@ void ColumnUDADuration::render(std::vector<std::string>& lines, Task& task, int
|
|||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
ColumnUDAUUID::ColumnUDAUUID() {
|
||||
_name = "<uda>";
|
||||
_type = "uuid";
|
||||
_style = "long";
|
||||
_label = "";
|
||||
_modifiable = true;
|
||||
_uda = true;
|
||||
_styles = {"long", "short"};
|
||||
_examples = {"f30cb9c3-3fc0-483f-bfb2-3bf134f00694", "f30cb9c3"};
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool ColumnUDAUUID::validate(const std::string& input) const {
|
||||
Lexer lex(input);
|
||||
std::string token;
|
||||
Lexer::Type type;
|
||||
return lex.isUUID(token, type, true);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
#include <ColTypeDuration.h>
|
||||
#include <ColTypeNumeric.h>
|
||||
#include <ColTypeString.h>
|
||||
#include <ColUUID.h>
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
class ColumnUDAString : public ColumnTypeString {
|
||||
|
@ -83,5 +84,12 @@ class ColumnUDADuration : public ColumnTypeDuration {
|
|||
std::vector<std::string> _values;
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
class ColumnUDAUUID : public ColumnUUID {
|
||||
public:
|
||||
ColumnUDAUUID();
|
||||
bool validate(const std::string&) const;
|
||||
};
|
||||
|
||||
#endif
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -246,9 +246,15 @@ Column* Column::uda(const std::string& name) {
|
|||
c->_label = label;
|
||||
if (values != "") c->_values = split(values, ',');
|
||||
return c;
|
||||
} else if (type == "uuid") {
|
||||
auto c = new ColumnUDAUUID();
|
||||
c->_name = name;
|
||||
c->_label = label;
|
||||
return c;
|
||||
} else if (type != "")
|
||||
throw std::string(
|
||||
"User defined attributes may only be of type 'string', 'date', 'duration' or 'numeric'.");
|
||||
"User defined attributes may only be of type 'string', 'uuid', date', 'duration' or "
|
||||
"'numeric'.");
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
|
|
@ -218,8 +218,8 @@ void CmdContext::defineContext(const std::vector<std::string>& words, std::strin
|
|||
if (!valid_write_context) {
|
||||
std::stringstream warning;
|
||||
warning
|
||||
<< format("The filter '{1}' is not a valid modification string, because it contains {2}.",
|
||||
value, reason)
|
||||
<< format("The filter '{1}' is not a valid modification string, because it {2}.", value,
|
||||
reason)
|
||||
<< "\nAs such, value for the write context cannot be set (context will not apply on task "
|
||||
"add / task log).\n\n"
|
||||
<< format(
|
||||
|
|
|
@ -317,7 +317,6 @@ void CmdEdit::parseTask(Task& task, const std::string& after, const std::string&
|
|||
|
||||
// tags
|
||||
value = findValue(after, "\n Tags:");
|
||||
task.remove("tags");
|
||||
task.setTags(split(value, ' '));
|
||||
|
||||
// description.
|
||||
|
@ -619,10 +618,9 @@ CmdEdit::editResult CmdEdit::editFile(Task& task) {
|
|||
auto dateformat = Context::getContext().config.get("dateformat.edit");
|
||||
if (dateformat == "") dateformat = Context::getContext().config.get("dateformat");
|
||||
|
||||
// Change directory for the editor
|
||||
// Change directory for the editor, doing nothing on error.
|
||||
auto current_dir = Directory::cwd();
|
||||
int ignored = chdir(location._data.c_str());
|
||||
++ignored; // Keep compiler quiet.
|
||||
chdir(location._data.c_str());
|
||||
|
||||
// Check if the file already exists, if so, bail out
|
||||
Path filepath = Path(file.str());
|
||||
|
@ -702,7 +700,7 @@ ARE_THESE_REALLY_HARMFUL:
|
|||
|
||||
// Cleanup.
|
||||
File::remove(file.str());
|
||||
ignored = chdir(current_dir.c_str());
|
||||
chdir(current_dir.c_str());
|
||||
return changes ? CmdEdit::editResult::changes : CmdEdit::editResult::nochanges;
|
||||
}
|
||||
|
||||
|
|
|
@ -588,6 +588,11 @@ int CmdNews::execute(std::string& output) {
|
|||
}
|
||||
wait_for_enter();
|
||||
|
||||
// Set a mark in the config to remember which version's release notes were displayed
|
||||
if (news_version < current_version) {
|
||||
CmdConfig::setConfigVariable("news.version", std::string(current_version), false);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
#include <taskchampion-cpp/lib.h>
|
||||
#include <util.h>
|
||||
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -78,6 +79,12 @@ int CmdSync::execute(std::string& output) {
|
|||
out << "sync.server.origin is deprecated. Use sync.server.url instead.\n";
|
||||
}
|
||||
|
||||
// redact credentials from `server_url`, if present
|
||||
std::regex remove_creds_regex("^(https?://.+):(.+)@(.+)");
|
||||
std::string safe_server_url = std::regex_replace(server_url, remove_creds_regex, "$1:****@$3");
|
||||
|
||||
auto num_local_operations = replica->num_local_operations();
|
||||
|
||||
if (server_dir != "") {
|
||||
if (verbose) {
|
||||
out << format("Syncing with {1}", server_dir) << '\n';
|
||||
|
@ -130,6 +137,7 @@ int CmdSync::execute(std::string& output) {
|
|||
replica->sync_to_aws_with_default_creds(aws_region, aws_bucket, encryption_secret,
|
||||
avoid_snapshots);
|
||||
}
|
||||
|
||||
} else if (gcp_bucket != "") {
|
||||
std::string gcp_credential_path = Context::getContext().config.get("sync.gcp.credential_path");
|
||||
if (encryption_secret == "") {
|
||||
|
@ -139,15 +147,17 @@ int CmdSync::execute(std::string& output) {
|
|||
out << format("Syncing with GCP bucket {1}", gcp_bucket) << '\n';
|
||||
}
|
||||
replica->sync_to_gcp(gcp_bucket, gcp_credential_path, encryption_secret, avoid_snapshots);
|
||||
|
||||
} else if (server_url != "") {
|
||||
if (client_id == "" || encryption_secret == "") {
|
||||
throw std::string("sync.server.client_id and sync.encryption_secret are required");
|
||||
}
|
||||
if (verbose) {
|
||||
out << format("Syncing with sync server at {1}", server_url) << '\n';
|
||||
out << format("Syncing with sync server at {1}", safe_server_url) << '\n';
|
||||
}
|
||||
replica->sync_to_remote(server_url, tc::uuid_from_string(client_id), encryption_secret,
|
||||
avoid_snapshots);
|
||||
|
||||
} else {
|
||||
throw std::string("No sync.* settings are configured. See task-sync(5).");
|
||||
}
|
||||
|
@ -156,6 +166,15 @@ int CmdSync::execute(std::string& output) {
|
|||
context.tdb2.expire_tasks();
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
out << "Success!\n";
|
||||
// Taskchampion does not provide a measure of the number of operations received from
|
||||
// the server, but we can give some indication of the number sent.
|
||||
if (num_local_operations) {
|
||||
out << format("Sent {1} local operations to the server", num_local_operations) << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
output = out.str();
|
||||
return status;
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ bool CmdUndo::confirm_revert(const std::vector<Operation>& undo_ops) {
|
|||
view.set(row, 1, mods.str());
|
||||
}
|
||||
last_uuid = op.get_uuid();
|
||||
mods.clear();
|
||||
mods = std::stringstream();
|
||||
}
|
||||
|
||||
if (op.is_create()) {
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 1a06cb4caebdae3c5e58fe83e2fd2211d2959815
|
||||
Subproject commit 121f757c3ec1b1f548f7835208b8c72d85d141a7
|
|
@ -1 +1 @@
|
|||
Subproject commit fcd8b41981cb1e80f4dcc20fa8970dc6aa981c9f
|
||||
Subproject commit 4eccadd67819b427978ca540e0c31e6cce08f226
|
18
src/util.cpp
18
src/util.cpp
|
@ -218,24 +218,6 @@ const std::vector<std::string> extractParents(const std::string& project,
|
|||
return vec;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
#ifndef HAVE_TIMEGM
|
||||
time_t timegm(struct tm* tm) {
|
||||
time_t ret;
|
||||
char* tz;
|
||||
tz = getenv("TZ");
|
||||
setenv("TZ", "UTC", 1);
|
||||
tzset();
|
||||
ret = mktime(tm);
|
||||
if (tz)
|
||||
setenv("TZ", tz, 1);
|
||||
else
|
||||
unsetenv("TZ");
|
||||
tzset();
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool nontrivial(const std::string& input) {
|
||||
std::string::size_type i = 0;
|
||||
|
|
|
@ -54,10 +54,6 @@ const std::string indentProject(const std::string&, const std::string& whitespac
|
|||
|
||||
const std::vector<std::string> extractParents(const std::string&, const char& delimiter = '.');
|
||||
|
||||
#ifndef HAVE_TIMEGM
|
||||
time_t timegm(struct tm* tm);
|
||||
#endif
|
||||
|
||||
bool nontrivial(const std::string&);
|
||||
const char* optionalBlankLine();
|
||||
void setHeaderUnderline(Table&);
|
||||
|
|
|
@ -109,29 +109,26 @@ class TestUUIDFormats(TestCase):
|
|||
def setUpClass(cls):
|
||||
"""Executed once before any test in the class"""
|
||||
cls.t = Task()
|
||||
cls.t.config("report.xxx.columns", "id,uuid")
|
||||
cls.t.config("report.xxx.columns", "uuid")
|
||||
cls.t.config("verbose", "nothing")
|
||||
|
||||
cls.t("add zero")
|
||||
code, out, err = cls.t("_get 1.uuid")
|
||||
cls.uuid = out.strip()
|
||||
|
||||
def setUp(self):
|
||||
"""Executed before each test in the class"""
|
||||
|
||||
def test_uuid_long(self):
|
||||
"""Verify formatting of 'uuid.long' column"""
|
||||
code, out, err = self.t("xxx rc.report.xxx.columns:id,uuid.long")
|
||||
self.assertIn(self.uuid, out)
|
||||
code, out, err = self.t("xxx rc.report.xxx.columns:uuid.long")
|
||||
self.assertEqual(self.uuid, out.strip())
|
||||
|
||||
def test_uuid_short(self):
|
||||
"""Verify formatting of 'uuid.short' column"""
|
||||
code, out, err = self.t("xxx rc.report.xxx.columns:id,uuid.short")
|
||||
self.assertIn(self.uuid[:7], out)
|
||||
code, out, err = self.t("xxx rc.report.xxx.columns:uuid.short")
|
||||
self.assertEqual(self.uuid[:8], out.strip())
|
||||
|
||||
def test_uuid_format_unrecognized(self):
|
||||
"""Verify uuid.donkey formatting fails"""
|
||||
code, out, err = self.t.runError("xxx rc.report.xxx.columns:id,uuid.donkey")
|
||||
code, out, err = self.t.runError("xxx rc.report.xxx.columns:uuid.donkey")
|
||||
self.assertEqual(err, "Unrecognized column format 'uuid.donkey'\n")
|
||||
|
||||
|
||||
|
@ -482,6 +479,70 @@ start active* ✓
|
|||
"""
|
||||
|
||||
|
||||
class TestUDAUUIDFormats(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Executed once before any test in the class"""
|
||||
cls.t = Task()
|
||||
cls.t.config("verbose", "nothing")
|
||||
cls.t.config("uda.uda_uuid.label", "uda_uuid")
|
||||
cls.t.config("uda.uda_uuid.type", "uuid")
|
||||
cls.t.config("report.xxx.columns", "uda_uuid")
|
||||
|
||||
cls.t("add zero")
|
||||
code, out, err = cls.t("_get 1.uuid")
|
||||
cls.t("add uda_uuid:{} one".format(out.strip()))
|
||||
code, out, err = cls.t("_get 2.uda_uuid")
|
||||
cls.uda_uuid = out.strip()
|
||||
|
||||
def test_uda_uuid_invalid_fails(self):
|
||||
"""Verify adding invalid uuid fails"""
|
||||
code, out, err = self.t.runError("add uda_uuid:shrek three")
|
||||
self.assertNotEqual(code, 0)
|
||||
self.assertIn("uda_uuid", err.strip())
|
||||
self.assertIn("shrek", err.strip())
|
||||
|
||||
def test_uda_uuid_long(self):
|
||||
"""Verify formatting of 'uda_uuid.long' column"""
|
||||
code, out, err = self.t("2 xxx rc.report.xxx.columns:uda_uuid.long")
|
||||
self.assertEqual(self.uda_uuid, out.strip())
|
||||
|
||||
def test_uda_uuid_short(self):
|
||||
"""Verify formatting of 'uda_uuid.short' column"""
|
||||
code, out, err = self.t("2 xxx rc.report.xxx.columns:uda_uuid.short")
|
||||
self.assertEqual(self.uda_uuid[:8], out.strip())
|
||||
|
||||
def test_uda_uuid_format_unrecognized(self):
|
||||
"""Verify uda_uuid.donkey formatting fails"""
|
||||
code, out, err = self.t.runError("xxx rc.report.xxx.columns:id,uda_uuid.donkey")
|
||||
self.assertEqual(err, "Unrecognized column format 'uda_uuid.donkey'\n")
|
||||
|
||||
|
||||
class TestUDAUUIDReconfiguredFromString(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Executed once before any test in the class"""
|
||||
cls.t = Task()
|
||||
cls.t.config("verbose", "nothing")
|
||||
cls.t.config("uda.uda_uuid.label", "uda_uuid")
|
||||
cls.t.config("report.xxx.columns", "uda_uuid")
|
||||
|
||||
cls.t.config("uda.uda_uuid.type", "string")
|
||||
cls.expected_str = 3 * "littlepigs"
|
||||
cls.t("add uda_uuid:{} one".format(cls.expected_str))
|
||||
cls.t.config("uda.uda_uuid.type", "uuid")
|
||||
|
||||
def test_uda_uuid_long(self):
|
||||
"""Verify formatting of 'uda_uuid.long' column"""
|
||||
code, out, err = self.t("1 xxx rc.report.xxx.columns:uda_uuid.long")
|
||||
self.assertEqual(self.expected_str, out.strip())
|
||||
|
||||
def test_uda_uuid_short(self):
|
||||
"""Verify formatting of 'uda_uuid.short' column"""
|
||||
code, out, err = self.t("1 xxx rc.report.xxx.columns:uda_uuid.short")
|
||||
self.assertEqual(self.expected_str[:8], out.strip())
|
||||
|
||||
|
||||
class TestFeature1061(TestCase):
|
||||
def setUp(self):
|
||||
"""Executed before each test in the class"""
|
||||
|
|
|
@ -293,7 +293,10 @@ int TEST_NAME(int, char**) {
|
|||
"'wonder'+0 : 'prowonderbread'+3 --> 6");
|
||||
|
||||
// Test all Lexer types.
|
||||
#define NO {"", Lexer::Type::word}
|
||||
#define NO \
|
||||
{ \
|
||||
"", Lexer::Type::word \
|
||||
}
|
||||
struct {
|
||||
const char* input;
|
||||
struct {
|
||||
|
|
|
@ -67,21 +67,23 @@ class TestBug268(TestCase):
|
|||
self.assertIn("a/b or c", out)
|
||||
|
||||
|
||||
class TestBug880(TestCase):
|
||||
class TestBug3858(TestCase):
|
||||
def setUp(self):
|
||||
"""Executed before each test in the class"""
|
||||
self.t = Task()
|
||||
|
||||
def test_backslash_at_eol(self):
|
||||
"""880: Backslash at end of description/annotation causes problems"""
|
||||
"""880: Backslashes at end of description/annotation are handled correctly"""
|
||||
self.t(r"add one\\")
|
||||
code, out, err = self.t("_get 1.description")
|
||||
self.assertEqual("one\\\n", out)
|
||||
|
||||
self.t(r"1 annotate 'two\\'")
|
||||
self.t(r"1 annotate 'two\'")
|
||||
self.t(r"1 annotate 'three\\'")
|
||||
code, out, err = self.t("info rc.verbose:nothing")
|
||||
self.assertIn("one\\\n", out)
|
||||
self.assertIn("two\\\n", out)
|
||||
self.assertIn("three\\\\\n", out)
|
||||
|
||||
|
||||
class TestBug1436(TestCase):
|
||||
|
|
|
@ -40,6 +40,7 @@ class TestTaskrc(TestCase):
|
|||
"""Executed before each test in the class"""
|
||||
self.t = Task()
|
||||
|
||||
@unittest.skip("taskrc generation requires a tty - see #3751")
|
||||
def test_default_taskrc(self):
|
||||
"""Verify that a default .taskrc is generated"""
|
||||
os.remove(self.t.taskrc)
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import unittest
|
||||
|
||||
# Ensure python finds the local simpletap module
|
||||
|
@ -61,6 +62,25 @@ class TestUndo(TestCase):
|
|||
code, out, err = self.t("_get 1.status")
|
||||
self.assertEqual(out.strip(), "pending")
|
||||
|
||||
def test_modify_multiple_tasks(self):
|
||||
"""'add' then 'done' then 'undo'"""
|
||||
self.t("add one")
|
||||
self.t("add two")
|
||||
self.t("add three")
|
||||
self.t("rc.bulk=0 1,2,3 modify +sometag")
|
||||
code, out, err = self.t("undo", input="y\n")
|
||||
# This undo output should show one tag modification for each task, possibly with some
|
||||
# modification-time updates if the modifications spanned a second boundary.
|
||||
self.assertRegex(
|
||||
out,
|
||||
"\s+".join(
|
||||
[
|
||||
r"""[0-9a-f-]{36} (Update property 'modified' from\s+'[0-9]+' to '[0-9]+'\s+)?Add tag 'sometag'\s+Add property 'tags' with value 'sometag'"""
|
||||
]
|
||||
* 3
|
||||
),
|
||||
)
|
||||
|
||||
def test_undo_en_passant(self):
|
||||
"""Verify that en-passant changes during undo are an error"""
|
||||
self.t("add one")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue