mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-07-07 20:06:36 +02:00
Test for unusual task data (#3770)
* Test for unusual task data A task might have any combination of keys and values, but Taskwarrior often assumes that only valid values can occur, and crashes otherwise. This is highly inconvenient, as it's often impossible to do anything with the invalid task -- Taskwarrior just fails without modifying it. So, this is the beginning of some testing for such invalid tasks, with the goal of making Taskwarrior due something reasonable. In general, an invalid attribute value is treated as if it was not set. This is not exhaustive, and there are likely still bugs of this sort, but as we find them we can fix and add regression tests to this script. This introduces a new test-only binary that creates a "bare" task using TaskChampion, avoiding Taskwarrior's efforts to not create "unusual" tasks. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
e1fc283da5
commit
fdb7e5e020
11 changed files with 368 additions and 41 deletions
|
@ -57,10 +57,6 @@ void TDB2::open_replica(const std::string& location, bool create_if_missing) {
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Add the new task to the replica.
|
||||
void TDB2::add(Task& task) {
|
||||
// Validate a task for addition. This is stricter than `task.validate`, as any
|
||||
// inconsistency is probably user error.
|
||||
task.validate_add();
|
||||
|
||||
// Ensure the task is consistent, and provide defaults if necessary.
|
||||
// bool argument to validate() is "applyDefault", to apply default values for
|
||||
// properties not otherwise given.
|
||||
|
|
19
src/Task.cpp
19
src/Task.cpp
|
@ -321,8 +321,8 @@ void Task::setStatus(Task::status status) {
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Determines status of a date attribute.
|
||||
Task::dateState Task::getDateState(const std::string& name) const {
|
||||
std::string value = get(name);
|
||||
if (value.length()) {
|
||||
time_t value = get_date(name);
|
||||
if (value > 0) {
|
||||
Datetime reference(value);
|
||||
Datetime now;
|
||||
Datetime today("today");
|
||||
|
@ -804,13 +804,16 @@ std::string Task::composeJSON(bool decorate /*= false*/) const {
|
|||
|
||||
// Date fields are written as ISO 8601.
|
||||
if (type == "date") {
|
||||
Datetime d(i.second);
|
||||
out << '"' << (i.first == "modification" ? "modified" : i.first)
|
||||
<< "\":\""
|
||||
// Date was deleted, do not export parsed empty string
|
||||
<< (i.second == "" ? "" : d.toISO()) << '"';
|
||||
time_t epoch = get_date(i.first);
|
||||
if (epoch != 0) {
|
||||
Datetime d(i.second);
|
||||
out << '"' << (i.first == "modification" ? "modified" : i.first)
|
||||
<< "\":\""
|
||||
// Date was deleted, do not export parsed empty string
|
||||
<< (i.second == "" ? "" : d.toISO()) << '"';
|
||||
|
||||
++attributes_written;
|
||||
++attributes_written;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -56,6 +56,11 @@ int CmdAdd::execute(std::string& output) {
|
|||
// the task is empty, but DOM references can refer to earlier parts of the
|
||||
// command line, e.g., `task add due:20110101 wait:due`.
|
||||
task.modify(Task::modReplace, true);
|
||||
|
||||
// Validate a task for addition. This is stricter than `task.validate`, as any
|
||||
// inconsistency is probably user error.
|
||||
task.validate_add();
|
||||
|
||||
Context::getContext().tdb2.add(task);
|
||||
|
||||
// Do not display ID 0, users cannot query by that
|
||||
|
|
|
@ -120,9 +120,11 @@ int CmdInfo::execute(std::string& output) {
|
|||
description += '\n' + std::string(indent, ' ') +
|
||||
Datetime(anno.first.substr(11)).toString(dateformatanno) + ' ' + anno.second;
|
||||
|
||||
row = view.addRow();
|
||||
view.set(row, 0, "Description");
|
||||
view.set(row, 1, description, c);
|
||||
if (task.has("description")) {
|
||||
row = view.addRow();
|
||||
view.set(row, 0, "Description");
|
||||
view.set(row, 1, description, c);
|
||||
}
|
||||
|
||||
// status
|
||||
row = view.addRow();
|
||||
|
@ -215,64 +217,76 @@ int CmdInfo::execute(std::string& output) {
|
|||
}
|
||||
|
||||
// entry
|
||||
row = view.addRow();
|
||||
view.set(row, 0, "Entered");
|
||||
Datetime dt(task.get_date("entry"));
|
||||
std::string entry = dt.toString(dateformat);
|
||||
if (task.has("entry") && task.get_date("entry")) {
|
||||
row = view.addRow();
|
||||
view.set(row, 0, "Entered");
|
||||
Datetime dt(task.get_date("entry"));
|
||||
std::string entry = dt.toString(dateformat);
|
||||
|
||||
std::string age;
|
||||
auto created = task.get("entry");
|
||||
if (created.length()) {
|
||||
Datetime dt(strtoll(created.c_str(), nullptr, 10));
|
||||
age = Duration(now - dt).formatVague();
|
||||
std::string age;
|
||||
auto created = task.get("entry");
|
||||
if (created.length()) {
|
||||
Datetime dt(strtoll(created.c_str(), nullptr, 10));
|
||||
age = Duration(now - dt).formatVague();
|
||||
}
|
||||
|
||||
view.set(row, 1, entry + " (" + age + ')');
|
||||
}
|
||||
|
||||
view.set(row, 1, entry + " (" + age + ')');
|
||||
auto validDate = [&](const char* prop) {
|
||||
if (!task.has(prop)) {
|
||||
return false;
|
||||
}
|
||||
if (task.get_date(prop) == 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// wait
|
||||
if (task.has("wait")) {
|
||||
if (validDate("wait")) {
|
||||
row = view.addRow();
|
||||
view.set(row, 0, "Waiting until");
|
||||
view.set(row, 1, Datetime(task.get_date("wait")).toString(dateformat));
|
||||
}
|
||||
|
||||
// scheduled
|
||||
if (task.has("scheduled")) {
|
||||
if (validDate("scheduled")) {
|
||||
row = view.addRow();
|
||||
view.set(row, 0, "Scheduled");
|
||||
view.set(row, 1, Datetime(task.get_date("scheduled")).toString(dateformat));
|
||||
}
|
||||
|
||||
// start
|
||||
if (task.has("start")) {
|
||||
if (validDate("start")) {
|
||||
row = view.addRow();
|
||||
view.set(row, 0, "Start");
|
||||
view.set(row, 1, Datetime(task.get_date("start")).toString(dateformat));
|
||||
}
|
||||
|
||||
// due (colored)
|
||||
if (task.has("due")) {
|
||||
if (validDate("due")) {
|
||||
row = view.addRow();
|
||||
view.set(row, 0, "Due");
|
||||
view.set(row, 1, Datetime(task.get_date("due")).toString(dateformat));
|
||||
}
|
||||
|
||||
// end
|
||||
if (task.has("end")) {
|
||||
if (validDate("end")) {
|
||||
row = view.addRow();
|
||||
view.set(row, 0, "End");
|
||||
view.set(row, 1, Datetime(task.get_date("end")).toString(dateformat));
|
||||
}
|
||||
|
||||
// until
|
||||
if (task.has("until")) {
|
||||
if (validDate("until")) {
|
||||
row = view.addRow();
|
||||
view.set(row, 0, "Until");
|
||||
view.set(row, 1, Datetime(task.get_date("until")).toString(dateformat));
|
||||
}
|
||||
|
||||
// modified
|
||||
if (task.has("modified")) {
|
||||
if (validDate("modified")) {
|
||||
row = view.addRow();
|
||||
view.set(row, 0, "Last modified");
|
||||
|
||||
|
@ -632,7 +646,11 @@ std::optional<std::string> CmdInfo::formatForInfo(const std::vector<Operation>&
|
|||
} else {
|
||||
// Record the last start time for later duration calculation.
|
||||
if (prop == "start") {
|
||||
last_start = Datetime(value.value()).toEpoch();
|
||||
try {
|
||||
last_start = Datetime(value.value()).toEpoch();
|
||||
} catch (std::string) {
|
||||
// ignore invalid dates
|
||||
}
|
||||
}
|
||||
|
||||
out << format("{1} set to '{2}'.", Lexer::ucFirst(prop),
|
||||
|
|
|
@ -119,7 +119,7 @@ void CmdModify::checkConsistency(Task &before, Task &after) {
|
|||
throw std::string("You cannot remove the recurrence from a recurring task.");
|
||||
|
||||
if ((before.getStatus() == Task::pending) && (after.getStatus() == Task::pending) &&
|
||||
(after.get("end") != ""))
|
||||
(before.get("end") == "") && (after.get("end") != ""))
|
||||
throw format("Could not modify task {1}. You cannot set an end date on a pending task.",
|
||||
before.identifier(true));
|
||||
}
|
||||
|
|
|
@ -51,7 +51,12 @@ std::string renderAttribute(const std::string& name, const std::string& value,
|
|||
if (Context::getContext().columns.find(name) != Context::getContext().columns.end()) {
|
||||
Column* col = Context::getContext().columns[name];
|
||||
if (col && col->type() == "date" && value != "") {
|
||||
Datetime d((time_t)strtoll(value.c_str(), nullptr, 10));
|
||||
int64_t epoch = strtoll(value.c_str(), nullptr, 10);
|
||||
// Do not try to render an un-parseable value.
|
||||
if (epoch == 0) {
|
||||
return value;
|
||||
}
|
||||
Datetime d((time_t)epoch);
|
||||
if (format == "") return d.toString(Context::getContext().config.get("dateformat"));
|
||||
|
||||
return d.toString(format);
|
||||
|
|
|
@ -288,9 +288,11 @@ std::optional<Datetime> getNextRecurrence(Datetime& current, std::string& period
|
|||
else if (unicodeLatinDigit(period[0]) && period[period.length() - 1] == 'q') {
|
||||
int increment = strtol(period.substr(0, period.length() - 1).c_str(), nullptr, 10);
|
||||
|
||||
if (increment <= 0)
|
||||
throw format("Recurrence period '{1}' is equivalent to {2} and hence invalid.", period,
|
||||
increment);
|
||||
if (increment <= 0) {
|
||||
Context::getContext().footnote(format(
|
||||
"Recurrence period '{1}' is equivalent to {2} and hence invalid.", period, increment));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
m += 3 * increment;
|
||||
while (m > 12) {
|
||||
|
@ -346,8 +348,11 @@ std::optional<Datetime> getNextRecurrence(Datetime& current, std::string& period
|
|||
// Add the period to current, and we're done.
|
||||
std::string::size_type idx = 0;
|
||||
Duration p;
|
||||
if (!p.parse(period, idx))
|
||||
throw std::string(format("The recurrence value '{1}' is not valid.", period));
|
||||
if (!p.parse(period, idx)) {
|
||||
Context::getContext().footnote(
|
||||
format("Warning: The recurrence value '{1}' is not valid.", period));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return checked_add_datetime(current, p.toTime_t());
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue