Skip to content

Commit fdb7e5e

Browse files
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>
1 parent e1fc283 commit fdb7e5e

11 files changed

+370
-43
lines changed

Diff for: src/TDB2.cpp

-4
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,6 @@ void TDB2::open_replica(const std::string& location, bool create_if_missing) {
5757
////////////////////////////////////////////////////////////////////////////////
5858
// Add the new task to the replica.
5959
void TDB2::add(Task& task) {
60-
// Validate a task for addition. This is stricter than `task.validate`, as any
61-
// inconsistency is probably user error.
62-
task.validate_add();
63-
6460
// Ensure the task is consistent, and provide defaults if necessary.
6561
// bool argument to validate() is "applyDefault", to apply default values for
6662
// properties not otherwise given.

Diff for: src/Task.cpp

+12-9
Original file line numberDiff line numberDiff line change
@@ -321,8 +321,8 @@ void Task::setStatus(Task::status status) {
321321
////////////////////////////////////////////////////////////////////////////////
322322
// Determines status of a date attribute.
323323
Task::dateState Task::getDateState(const std::string& name) const {
324-
std::string value = get(name);
325-
if (value.length()) {
324+
time_t value = get_date(name);
325+
if (value > 0) {
326326
Datetime reference(value);
327327
Datetime now;
328328
Datetime today("today");
@@ -804,13 +804,16 @@ std::string Task::composeJSON(bool decorate /*= false*/) const {
804804

805805
// Date fields are written as ISO 8601.
806806
if (type == "date") {
807-
Datetime d(i.second);
808-
out << '"' << (i.first == "modification" ? "modified" : i.first)
809-
<< "\":\""
810-
// Date was deleted, do not export parsed empty string
811-
<< (i.second == "" ? "" : d.toISO()) << '"';
812-
813-
++attributes_written;
807+
time_t epoch = get_date(i.first);
808+
if (epoch != 0) {
809+
Datetime d(i.second);
810+
out << '"' << (i.first == "modification" ? "modified" : i.first)
811+
<< "\":\""
812+
// Date was deleted, do not export parsed empty string
813+
<< (i.second == "" ? "" : d.toISO()) << '"';
814+
815+
++attributes_written;
816+
}
814817
}
815818

816819
/*

Diff for: src/commands/CmdAdd.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ int CmdAdd::execute(std::string& output) {
5656
// the task is empty, but DOM references can refer to earlier parts of the
5757
// command line, e.g., `task add due:20110101 wait:due`.
5858
task.modify(Task::modReplace, true);
59+
60+
// Validate a task for addition. This is stricter than `task.validate`, as any
61+
// inconsistency is probably user error.
62+
task.validate_add();
63+
5964
Context::getContext().tdb2.add(task);
6065

6166
// Do not display ID 0, users cannot query by that

Diff for: src/commands/CmdInfo.cpp

+40-22
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,11 @@ int CmdInfo::execute(std::string& output) {
120120
description += '\n' + std::string(indent, ' ') +
121121
Datetime(anno.first.substr(11)).toString(dateformatanno) + ' ' + anno.second;
122122

123-
row = view.addRow();
124-
view.set(row, 0, "Description");
125-
view.set(row, 1, description, c);
123+
if (task.has("description")) {
124+
row = view.addRow();
125+
view.set(row, 0, "Description");
126+
view.set(row, 1, description, c);
127+
}
126128

127129
// status
128130
row = view.addRow();
@@ -215,64 +217,76 @@ int CmdInfo::execute(std::string& output) {
215217
}
216218

217219
// entry
218-
row = view.addRow();
219-
view.set(row, 0, "Entered");
220-
Datetime dt(task.get_date("entry"));
221-
std::string entry = dt.toString(dateformat);
222-
223-
std::string age;
224-
auto created = task.get("entry");
225-
if (created.length()) {
226-
Datetime dt(strtoll(created.c_str(), nullptr, 10));
227-
age = Duration(now - dt).formatVague();
220+
if (task.has("entry") && task.get_date("entry")) {
221+
row = view.addRow();
222+
view.set(row, 0, "Entered");
223+
Datetime dt(task.get_date("entry"));
224+
std::string entry = dt.toString(dateformat);
225+
226+
std::string age;
227+
auto created = task.get("entry");
228+
if (created.length()) {
229+
Datetime dt(strtoll(created.c_str(), nullptr, 10));
230+
age = Duration(now - dt).formatVague();
231+
}
232+
233+
view.set(row, 1, entry + " (" + age + ')');
228234
}
229235

230-
view.set(row, 1, entry + " (" + age + ')');
236+
auto validDate = [&](const char* prop) {
237+
if (!task.has(prop)) {
238+
return false;
239+
}
240+
if (task.get_date(prop) == 0) {
241+
return false;
242+
}
243+
return true;
244+
};
231245

232246
// wait
233-
if (task.has("wait")) {
247+
if (validDate("wait")) {
234248
row = view.addRow();
235249
view.set(row, 0, "Waiting until");
236250
view.set(row, 1, Datetime(task.get_date("wait")).toString(dateformat));
237251
}
238252

239253
// scheduled
240-
if (task.has("scheduled")) {
254+
if (validDate("scheduled")) {
241255
row = view.addRow();
242256
view.set(row, 0, "Scheduled");
243257
view.set(row, 1, Datetime(task.get_date("scheduled")).toString(dateformat));
244258
}
245259

246260
// start
247-
if (task.has("start")) {
261+
if (validDate("start")) {
248262
row = view.addRow();
249263
view.set(row, 0, "Start");
250264
view.set(row, 1, Datetime(task.get_date("start")).toString(dateformat));
251265
}
252266

253267
// due (colored)
254-
if (task.has("due")) {
268+
if (validDate("due")) {
255269
row = view.addRow();
256270
view.set(row, 0, "Due");
257271
view.set(row, 1, Datetime(task.get_date("due")).toString(dateformat));
258272
}
259273

260274
// end
261-
if (task.has("end")) {
275+
if (validDate("end")) {
262276
row = view.addRow();
263277
view.set(row, 0, "End");
264278
view.set(row, 1, Datetime(task.get_date("end")).toString(dateformat));
265279
}
266280

267281
// until
268-
if (task.has("until")) {
282+
if (validDate("until")) {
269283
row = view.addRow();
270284
view.set(row, 0, "Until");
271285
view.set(row, 1, Datetime(task.get_date("until")).toString(dateformat));
272286
}
273287

274288
// modified
275-
if (task.has("modified")) {
289+
if (validDate("modified")) {
276290
row = view.addRow();
277291
view.set(row, 0, "Last modified");
278292

@@ -632,7 +646,11 @@ std::optional<std::string> CmdInfo::formatForInfo(const std::vector<Operation>&
632646
} else {
633647
// Record the last start time for later duration calculation.
634648
if (prop == "start") {
635-
last_start = Datetime(value.value()).toEpoch();
649+
try {
650+
last_start = Datetime(value.value()).toEpoch();
651+
} catch (std::string) {
652+
// ignore invalid dates
653+
}
636654
}
637655

638656
out << format("{1} set to '{2}'.", Lexer::ucFirst(prop),

Diff for: src/commands/CmdModify.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ void CmdModify::checkConsistency(Task &before, Task &after) {
119119
throw std::string("You cannot remove the recurrence from a recurring task.");
120120

121121
if ((before.getStatus() == Task::pending) && (after.getStatus() == Task::pending) &&
122-
(after.get("end") != ""))
122+
(before.get("end") == "") && (after.get("end") != ""))
123123
throw format("Could not modify task {1}. You cannot set an end date on a pending task.",
124124
before.identifier(true));
125125
}

Diff for: src/feedback.cpp

+6-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ std::string renderAttribute(const std::string& name, const std::string& value,
5151
if (Context::getContext().columns.find(name) != Context::getContext().columns.end()) {
5252
Column* col = Context::getContext().columns[name];
5353
if (col && col->type() == "date" && value != "") {
54-
Datetime d((time_t)strtoll(value.c_str(), nullptr, 10));
54+
int64_t epoch = strtoll(value.c_str(), nullptr, 10);
55+
// Do not try to render an un-parseable value.
56+
if (epoch == 0) {
57+
return value;
58+
}
59+
Datetime d((time_t)epoch);
5560
if (format == "") return d.toString(Context::getContext().config.get("dateformat"));
5661

5762
return d.toString(format);

Diff for: src/recur.cpp

+10-5
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,11 @@ std::optional<Datetime> getNextRecurrence(Datetime& current, std::string& period
288288
else if (unicodeLatinDigit(period[0]) && period[period.length() - 1] == 'q') {
289289
int increment = strtol(period.substr(0, period.length() - 1).c_str(), nullptr, 10);
290290

291-
if (increment <= 0)
292-
throw format("Recurrence period '{1}' is equivalent to {2} and hence invalid.", period,
293-
increment);
291+
if (increment <= 0) {
292+
Context::getContext().footnote(format(
293+
"Recurrence period '{1}' is equivalent to {2} and hence invalid.", period, increment));
294+
return std::nullopt;
295+
}
294296

295297
m += 3 * increment;
296298
while (m > 12) {
@@ -346,8 +348,11 @@ std::optional<Datetime> getNextRecurrence(Datetime& current, std::string& period
346348
// Add the period to current, and we're done.
347349
std::string::size_type idx = 0;
348350
Duration p;
349-
if (!p.parse(period, idx))
350-
throw std::string(format("The recurrence value '{1}' is not valid.", period));
351+
if (!p.parse(period, idx)) {
352+
Context::getContext().footnote(
353+
format("Warning: The recurrence value '{1}' is not valid.", period));
354+
return std::nullopt;
355+
}
351356

352357
return checked_add_datetime(current, p.toTime_t());
353358
}

Diff for: test/CMakeLists.txt

+10
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ set (pythonTests
188188
undo.test.py
189189
unicode.test.py
190190
unique.test.py
191+
unusual_task.test.py
191192
upgrade.test.py
192193
urgency.test.py
193194
urgency_inherit.test.py
@@ -207,6 +208,15 @@ foreach (python_Test ${pythonTests})
207208
)
208209
endforeach(python_Test)
209210

211+
# Create a `make_tc_task` binary, used for unusual_task.test.py. In order to build this
212+
# for the tests, it's added as a dependency of `test_runner`.
213+
add_executable (make_tc_task make_tc_task.cpp)
214+
target_link_libraries (make_tc_task task commands columns libshared task commands columns libshared task commands columns libshared ${TASK_LIBRARIES})
215+
if (DARWIN)
216+
target_link_libraries (make_tc_task "-framework CoreFoundation -framework Security -framework SystemConfiguration")
217+
endif (DARWIN)
218+
add_dependencies(test_runner make_tc_task)
219+
210220
# -- Shell tests
211221

212222
set (shell_SRCS

Diff for: test/basetest/utils.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@
2828
# Directory relative to basetest module location
2929
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
3030

31+
# From the CMAKE value of the same name. This is substituted at configure.
32+
CMAKE_BINARY_DIR = os.path.abspath("${CMAKE_BINARY_DIR}")
33+
3134
# Location of binary files (usually the src/ folder)
32-
BIN_PREFIX = os.path.abspath(os.path.join("${CMAKE_BINARY_DIR}", "src"))
35+
BIN_PREFIX = os.path.abspath(os.path.join(CMAKE_BINARY_DIR, "src"))
3336

3437
# Default location of test hooks
3538
DEFAULT_HOOK_PATH = os.path.abspath(

Diff for: test/make_tc_task.cpp

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
////////////////////////////////////////////////////////////////////////////////
2+
//
3+
// Copyright 2025, Dustin J. Mitchell
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included
13+
// in all copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18+
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
// SOFTWARE.
22+
//
23+
// https://www.opensource.org/licenses/mit-license.php
24+
//
25+
////////////////////////////////////////////////////////////////////////////////
26+
27+
#include <cmake.h>
28+
// cmake.h include header must come first
29+
30+
#include <CmdInfo.h>
31+
#include <main.h>
32+
#include <stdlib.h>
33+
#include <taskchampion-cpp/lib.h>
34+
#include <test.h>
35+
#include <util.h>
36+
37+
#include <iostream>
38+
#include <limits>
39+
40+
#include "format.h"
41+
42+
namespace {
43+
44+
////////////////////////////////////////////////////////////////////////////////
45+
int usage() {
46+
std::cerr << "USAGE: make_tc_task DATADIR KEY=VALUE ..\n";
47+
return 1;
48+
}
49+
50+
} // namespace
51+
52+
////////////////////////////////////////////////////////////////////////////////
53+
int main(int argc, char **argv) {
54+
if (!--argc) {
55+
return usage();
56+
}
57+
char *datadir = *++argv;
58+
59+
auto replica = tc::new_replica_on_disk(datadir, true);
60+
auto uuid = tc::uuid_v4();
61+
auto operations = tc::new_operations();
62+
auto task = tc::create_task(uuid, operations);
63+
64+
while (--argc) {
65+
std::string arg = *++argv;
66+
size_t eq_idx = arg.find('=');
67+
if (eq_idx == std::string::npos) {
68+
return usage();
69+
}
70+
std::string property = arg.substr(0, eq_idx);
71+
std::string value = arg.substr(eq_idx + 1);
72+
task->update(property, value, operations);
73+
}
74+
replica->commit_operations(std::move(operations));
75+
76+
std::cout << static_cast<std::string>(uuid.to_string()) << "\n";
77+
return 0;
78+
}
79+
80+
////////////////////////////////////////////////////////////////////////////////

0 commit comments

Comments
 (0)