From c9967c20e2b87ceca1dcc26e8171603f02d5f3ff Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 6 Nov 2024 07:39:39 -0500 Subject: [PATCH] Restore support for` task info` journal (#3671) This support was removed before Taskwarrior-3.x, and is now restored, including the original tests removed in ddd367232e2dbd7b7666a680e10af8a4094f3f03 --- src/CMakeLists.txt | 1 + src/Operation.cpp | 73 ++++++++++++++ src/Operation.h | 88 +++++++++++++++++ src/TDB2.cpp | 3 +- src/Task.cpp | 94 +----------------- src/Task.h | 11 +-- src/commands/CmdInfo.cpp | 164 ++++++++++++++++++++++++++++++++ src/commands/CmdInfo.h | 7 ++ src/taskchampion-cpp/src/lib.rs | 27 ++++++ test/CMakeLists.txt | 1 + test/info.test.py | 3 + test/tw-1999.test.py | 62 ++++++++++++ test/tw-2514.test.sh | 5 +- 13 files changed, 438 insertions(+), 101 deletions(-) create mode 100644 src/Operation.cpp create mode 100644 src/Operation.h create mode 100755 test/tw-1999.test.py diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 15530c776..9bb7f2e38 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -13,6 +13,7 @@ add_library (task STATIC CLI2.cpp CLI2.h Filter.cpp Filter.h Hooks.cpp Hooks.h Lexer.cpp Lexer.h + Operation.cpp Operation.h TDB2.cpp TDB2.h Task.cpp Task.h Variant.cpp Variant.h diff --git a/src/Operation.cpp b/src/Operation.cpp new file mode 100644 index 000000000..4bc4ecc48 --- /dev/null +++ b/src/Operation.cpp @@ -0,0 +1,73 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2006 - 2024, Tomas Babej, Paul Beckingham, Federico Hernandez. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// https://www.opensource.org/licenses/mit-license.php +// +//////////////////////////////////////////////////////////////////////////////// + +#include +// cmake.h include header must come first + +#include +#include + +#include + +//////////////////////////////////////////////////////////////////////////////// +Operation::Operation(const tc::Operation& op) : op(&op) {} + +//////////////////////////////////////////////////////////////////////////////// +std::vector Operation::operations(const rust::Vec& operations) { + return {operations.begin(), operations.end()}; +} + +//////////////////////////////////////////////////////////////////////////////// +Operation& Operation::operator=(const Operation& other) { + op = other.op; + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +bool Operation::operator<(Operation& other) const { + if (is_create()) { + return !other.is_create(); + } else if (is_update()) { + if (other.is_create()) { + return false; + } else if (other.is_update()) { + return get_timestamp() < other.get_timestamp(); + } else { + return true; + } + } else if (is_delete()) { + if (other.is_create() || other.is_update() || other.is_delete()) { + return false; + } else { + return true; + } + } else if (is_undo_point()) { + return !other.is_undo_point(); + } + return false; // not reachable +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/Operation.h b/src/Operation.h new file mode 100644 index 000000000..707dffd89 --- /dev/null +++ b/src/Operation.h @@ -0,0 +1,88 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2006 - 2024, Tomas Babej, Paul Beckingham, Federico Hernandez. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// https://www.opensource.org/licenses/mit-license.php +// +//////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDED_OPERATIOn +#define INCLUDED_OPERATIOn + +#include + +#include +#include + +// Representation of a TaskChampion operation. +// +// This class wraps `tc::Operation&` and thus cannot outlive that underlying +// type. +class Operation { + public: + explicit Operation(const tc::Operation &); + + Operation(const Operation &other) = default; + Operation &operator=(const Operation &other); + + // Create a vector of Operations given the result of `Replica::get_undo_operations` or + // `Replica::get_task_operations`. The resulting vector must not outlive the input `rust::Vec`. + static std::vector operations(const rust::Vec &); + + // Methods from the underlying `tc::Operation`. + bool is_create() const { return op->is_create(); } + bool is_update() const { return op->is_update(); } + bool is_delete() const { return op->is_delete(); } + bool is_undo_point() const { return op->is_undo_point(); } + std::string get_uuid() const { return std::string(op->get_uuid().to_string()); } + ::rust::Vec<::tc::PropValuePair> get_old_task() const { return op->get_old_task(); }; + std::string get_property() const { + std::string value; + op->get_property(value); + return value; + } + std::optional get_value() const { + std::optional value{std::string()}; + if (!op->get_value(value.value())) { + value = std::nullopt; + } + return value; + } + std::optional get_old_value() const { + std::optional old_value{std::string()}; + if (!op->get_old_value(old_value.value())) { + old_value = std::nullopt; + } + return old_value; + } + time_t get_timestamp() const { return static_cast(op->get_timestamp()); } + + // Define a partial order on Operations: + // - Create < Update < Delete < UndoPoint + // - Given two updates, sort by timestamp + bool operator<(Operation &other) const; + + private: + const tc::Operation *op; +}; + +#endif +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/TDB2.cpp b/src/TDB2.cpp index 2b01bc3b5..d7fd81017 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -244,8 +244,7 @@ void TDB2::revert() { //////////////////////////////////////////////////////////////////////////////// bool TDB2::confirm_revert(rust::Vec& undo_ops) { - // TODO Use show_diff rather than this basic listing of operations, though - // this might be a worthy undo.style itself. + // TODO: convert to Operation and use that type for display, similar to CmdInfo. // Count non-undo operations int ops_count = 0; diff --git a/src/Task.cpp b/src/Task.cpp index 6c51dedbe..72cf41975 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -1243,14 +1243,14 @@ void Task::fixTagsAttribute() { bool Task::isTagAttr(const std::string& attr) { return attr.compare(0, 4, "tag_") == 0; } //////////////////////////////////////////////////////////////////////////////// -const std::string Task::tag2Attr(const std::string& tag) const { +std::string Task::tag2Attr(const std::string& tag) { std::stringstream tag_attr; tag_attr << "tag_" << tag; return tag_attr.str(); } //////////////////////////////////////////////////////////////////////////////// -const std::string Task::attr2Tag(const std::string& attr) const { +std::string Task::attr2Tag(const std::string& attr) { assert(isTagAttr(attr)); return attr.substr(4); } @@ -1271,14 +1271,14 @@ void Task::fixDependsAttribute() { bool Task::isDepAttr(const std::string& attr) { return attr.compare(0, 4, "dep_") == 0; } //////////////////////////////////////////////////////////////////////////////// -const std::string Task::dep2Attr(const std::string& tag) const { +std::string Task::dep2Attr(const std::string& tag) { std::stringstream tag_attr; tag_attr << "dep_" << tag; return tag_attr.str(); } //////////////////////////////////////////////////////////////////////////////// -const std::string Task::attr2Dep(const std::string& attr) const { +std::string Task::attr2Dep(const std::string& attr) { assert(isDepAttr(attr)); return attr.substr(4); } @@ -2151,92 +2151,6 @@ std::string Task::diff(const Task& after) const { return out.str(); } -//////////////////////////////////////////////////////////////////////////////// -// Similar to diff, but formatted for inclusion in the output of the info command -std::string Task::diffForInfo(const Task& after, const std::string& dateformat, - long& last_timestamp, const long current_timestamp) const { - // Attributes are all there is, so figure the different attribute names - // between before and after. - std::vector beforeAtts; - for (auto& att : data) beforeAtts.push_back(att.first); - - std::vector afterAtts; - for (auto& att : after.data) afterAtts.push_back(att.first); - - std::vector beforeOnly; - std::vector afterOnly; - listDiff(beforeAtts, afterAtts, beforeOnly, afterOnly); - - // Now start generating a description of the differences. - std::stringstream out; - for (auto& name : beforeOnly) { - if (isAnnotationAttr(name)) { - out << format("Annotation '{1}' deleted.\n", get(name)); - } else if (isTagAttr(name)) { - out << format("Tag '{1}' deleted.\n", attr2Tag(name)); - } else if (isDepAttr(name)) { - out << format("Dependency on '{1}' deleted.\n", attr2Dep(name)); - } else if (name == "depends" || name == "tags") { - // do nothing for legacy attributes - } else if (name == "start") { - Datetime started(get("start")); - Datetime stopped; - - if (after.has("end")) - // Task was marked as finished, use end time - stopped = Datetime(after.get("end")); - else - // Start attribute was removed, use modification time - stopped = Datetime(current_timestamp); - - out << format("{1} deleted (duration: {2}).", Lexer::ucFirst(name), - Duration(stopped - started).format()) - << "\n"; - } else { - out << format("{1} deleted.\n", Lexer::ucFirst(name)); - } - } - - for (auto& name : afterOnly) { - if (isAnnotationAttr(name)) { - out << format("Annotation of '{1}' added.\n", after.get(name)); - } else if (isTagAttr(name)) { - out << format("Tag '{1}' added.\n", attr2Tag(name)); - } else if (isDepAttr(name)) { - out << format("Dependency on '{1}' added.\n", attr2Dep(name)); - } else if (name == "depends" || name == "tags") { - // do nothing for legacy attributes - } else { - if (name == "start") last_timestamp = current_timestamp; - - out << format("{1} set to '{2}'.", Lexer::ucFirst(name), - renderAttribute(name, after.get(name), dateformat)) - << "\n"; - } - } - - for (auto& name : beforeAtts) - if (name != "uuid" && name != "modified" && get(name) != after.get(name) && get(name) != "" && - after.get(name) != "") { - if (name == "depends" || name == "tags") { - // do nothing for legacy attributes - } else if (isTagAttr(name) || isDepAttr(name)) { - // ignore new attributes - } else if (isAnnotationAttr(name)) { - out << format("Annotation changed to '{1}'.\n", after.get(name)); - } else - out << format("{1} changed from '{2}' to '{3}'.", Lexer::ucFirst(name), - renderAttribute(name, get(name), dateformat), - renderAttribute(name, after.get(name), dateformat)) - << "\n"; - } - - // Shouldn't just say nothing. - if (out.str().length() == 0) out << "No changes made.\n"; - - return out.str(); -} - //////////////////////////////////////////////////////////////////////////////// // Similar to diff, but formatted as a side-by-side table for an Undo preview Table Task::diffForUndoSide(const Task& after) const { diff --git a/src/Task.h b/src/Task.h index f36e79b83..1f63b7f5a 100644 --- a/src/Task.h +++ b/src/Task.h @@ -164,6 +164,11 @@ class Task { void substitute(const std::string&, const std::string&, const std::string&); #endif + static std::string tag2Attr(const std::string&); + static std::string attr2Tag(const std::string&); + static std::string dep2Attr(const std::string&); + static std::string attr2Dep(const std::string&); + void validate_add(); void validate(bool applyDefault = true); @@ -176,8 +181,6 @@ class Task { #endif std::string diff(const Task& after) const; - std::string diffForInfo(const Task& after, const std::string& dateformat, long& last_timestamp, - const long current_timestamp) const; Table diffForUndoSide(const Task& after) const; Table diffForUndoPatch(const Task& after, const Datetime& lastChange) const; @@ -190,10 +193,6 @@ class Task { void validate_before(const std::string&, const std::string&); const std::string encode(const std::string&) const; const std::string decode(const std::string&) const; - const std::string tag2Attr(const std::string&) const; - const std::string attr2Tag(const std::string&) const; - const std::string dep2Attr(const std::string&) const; - const std::string attr2Dep(const std::string&) const; void fixDependsAttribute(); void fixTagsAttribute(); diff --git a/src/commands/CmdInfo.cpp b/src/commands/CmdInfo.cpp index 5795409f6..cfb3263e8 100644 --- a/src/commands/CmdInfo.cpp +++ b/src/commands/CmdInfo.cpp @@ -33,13 +33,16 @@ #include #include #include +#include #include #include #include #include #include +#include #include +#include #include //////////////////////////////////////////////////////////////////////////////// @@ -477,9 +480,68 @@ int CmdInfo::execute(std::string& output) { urgencyDetails.set(row, 5, rightJustify(format(task.urgency(), 4, 4), 6)); } + // Create a third table, containing undo log change details. + Table journal; + setHeaderUnderline(journal); + + if (Context::getContext().config.getBoolean("obfuscate")) journal.obfuscate(); + if (Context::getContext().config.getBoolean("color")) journal.forceColor(); + + journal.width(Context::getContext().getWidth()); + journal.add("Date"); + journal.add("Modification"); + + if (Context::getContext().config.getBoolean("journal.info")) { + auto& replica = Context::getContext().tdb2.replica(); + tc::Uuid tcuuid = tc::uuid_from_string(uuid); + auto tcoperations = replica->get_task_operations(tcuuid); + auto operations = Operation::operations(tcoperations); + + // Sort by type (Create < Update < Delete < UndoPoint) and then by timestamp. + std::sort(operations.begin(), operations.end()); + + long last_timestamp = 0; + for (size_t i = 0; i < operations.size(); i++) { + auto& op = operations[i]; + + // Only display updates -- creation and deletion aren't interesting. + if (!op.is_update()) { + continue; + } + + // Group operations that occur within 1s of this one. This is a heuristic + // for operations performed in the same `task` invocation, and allows e.g., + // `task done end:-2h` to take the updated `end` value into account. It also + // groups these events into a single "row" of the table for better layout. + size_t group_start = i; + for (i++; i < operations.size(); i++) { + auto& op2 = operations[i]; + if (!op2.is_update() || op2.get_timestamp() - op.get_timestamp() > 1) { + break; + } + } + size_t group_end = i; + i--; + + std::optional msg = + formatForInfo(operations, group_start, group_end, dateformat, last_timestamp); + + if (!msg) { + continue; + } + + int row = journal.addRow(); + Datetime timestamp(op.get_timestamp()); + journal.set(row, 0, timestamp.toString(dateformat)); + journal.set(row, 1, *msg); + } + } + out << optionalBlankLine() << view.render() << '\n'; if (urgencyDetails.rows() > 0) out << urgencyDetails.render() << '\n'; + + if (journal.rows() > 0) out << journal.render() << '\n'; } output = out.str(); @@ -502,3 +564,105 @@ void CmdInfo::urgencyTerm(Table& view, const std::string& label, float measure, } //////////////////////////////////////////////////////////////////////////////// +std::optional CmdInfo::formatForInfo(const std::vector& operations, + size_t group_start, size_t group_end, + const std::string& dateformat, long& last_start) { + std::stringstream out; + for (auto i = group_start; i < group_end; i++) { + auto& operation = operations[i]; + assert(operation.is_update()); + + // Extract the parts of the Update operation. + std::string prop = operation.get_property(); + std::optional value = operation.get_value(); + std::optional old_value = operation.get_old_value(); + Datetime timestamp(operation.get_timestamp()); + + // Never care about modifying the modification time, or the legacy properties `depends` and + // `tags`. + if (prop == "modified" || prop == "depends" || prop == "tags") { + continue; + } + + // Handle property deletions + if (!value && old_value) { + if (Task::isAnnotationAttr(prop)) { + out << format("Annotation '{1}' deleted.\n", *old_value); + } else if (Task::isTagAttr(prop)) { + out << format("Tag '{1}' deleted.\n", Task::attr2Tag(prop)); + } else if (Task::isDepAttr(prop)) { + out << format("Dependency on '{1}' deleted.\n", Task::attr2Dep(prop)); + } else if (prop == "start") { + Datetime started(last_start); + Datetime stopped = timestamp; + + // If any update in this group sets the `end` property, use that instead of the + // timestamp deleting the `start` property as the stop time. + // See https://github.com/GothenburgBitFactory/taskwarrior/issues/2514 + for (auto i = group_start; i < group_end; i++) { + auto& op = operations[i]; + assert(op.is_update()); + if (op.get_property() == "end") { + try { + stopped = op.get_value().value(); + } catch (std::string) { + // Fall back to the 'start' timestamp if its value is un-parseable. + stopped = op.get_timestamp(); + } + } + } + + out << format("{1} deleted (duration: {2}).", Lexer::ucFirst(prop), + Duration(stopped - started).format()) + << "\n"; + } else { + out << format("{1} deleted.\n", Lexer::ucFirst(prop)); + } + } + + // Handle property additions. + if (value && !old_value) { + if (Task::isAnnotationAttr(prop)) { + out << format("Annotation of '{1}' added.\n", *value); + } else if (Task::isTagAttr(prop)) { + out << format("Tag '{1}' added.\n", Task::attr2Tag(prop)); + } else if (Task::isDepAttr(prop)) { + out << format("Dependency on '{1}' added.\n", Task::attr2Dep(prop)); + } else { + // Record the last start time for later duration calculation. + if (prop == "start") { + last_start = Datetime(value.value()).toEpoch(); + } + + out << format("{1} set to '{2}'.", Lexer::ucFirst(prop), + renderAttribute(prop, *value, dateformat)) + << "\n"; + } + } + + // Handle property changes. + if (value && old_value) { + if (Task::isTagAttr(prop) || Task::isDepAttr(prop)) { + // Dependencies and tags do not have meaningful values. + } else if (Task::isAnnotationAttr(prop)) { + out << format("Annotation changed to '{1}'.\n", *value); + } else { + // Record the last start time for later duration calculation. + if (prop == "start") { + last_start = Datetime(value.value()).toEpoch(); + } + + out << format("{1} changed from '{2}' to '{3}'.", Lexer::ucFirst(prop), + renderAttribute(prop, *old_value, dateformat), + renderAttribute(prop, *value, dateformat)) + << "\n"; + } + } + } + + if (out.str().length() == 0) return std::nullopt; + + return out.str(); +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/commands/CmdInfo.h b/src/commands/CmdInfo.h index 06516e920..196d8e8b0 100644 --- a/src/commands/CmdInfo.h +++ b/src/commands/CmdInfo.h @@ -28,9 +28,12 @@ #define INCLUDED_CMDINFO #include +#include #include +#include #include +#include class CmdInfo : public Command { public: @@ -39,6 +42,10 @@ class CmdInfo : public Command { private: void urgencyTerm(Table&, const std::string&, float, float) const; + // Format a group of update operations for display in `task info`. + std::optional formatForInfo(const std::vector& operations, + size_t group_start, size_t group_end, + const std::string& dateformat, long& last_start); }; #endif diff --git a/src/taskchampion-cpp/src/lib.rs b/src/taskchampion-cpp/src/lib.rs index 2fc2f92df..829394f63 100644 --- a/src/taskchampion-cpp/src/lib.rs +++ b/src/taskchampion-cpp/src/lib.rs @@ -133,6 +133,9 @@ mod ffi { /// Get an existing task by its UUID. fn get_task_data(&mut self, uuid: Uuid) -> Result; + /// Get the operations for a task task by its UUID. + fn get_task_operations(&mut self, uuid: Uuid) -> Result>; + /// Return the operations back to and including the last undo point, or since the last sync if /// no undo point is found. fn get_undo_operations(&mut self) -> Result>; @@ -529,6 +532,10 @@ impl Replica { Ok(self.0.get_task_data(uuid.into())?.into()) } + fn get_task_operations(&mut self, uuid: ffi::Uuid) -> Result, CppError> { + Ok(from_tc_operations(self.0.get_task_operations(uuid.into())?)) + } + fn get_undo_operations(&mut self) -> Result, CppError> { Ok(from_tc_operations(self.0.get_undo_operations()?)) } @@ -921,6 +928,26 @@ mod test { assert_eq!(t.take().get_uuid(), uuid); } + #[test] + fn get_task_operations() { + cxx::let_cxx_string!(prop = "prop"); + cxx::let_cxx_string!(value = "value"); + let mut rep = new_replica_in_memory().unwrap(); + + let uuid = uuid_v4(); + assert!(rep.get_task_operations(uuid).unwrap().is_empty()); + + let mut operations = new_operations(); + let mut t = create_task(uuid, &mut operations); + t.update(&prop, &value, &mut operations); + rep.commit_operations(operations).unwrap(); + + let ops = rep.get_task_operations(uuid).unwrap(); + assert_eq!(ops.len(), 2); + assert!(ops[0].is_create()); + assert!(ops[1].is_update()); + } + #[test] fn task_properties() { cxx::let_cxx_string!(prop = "prop"); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b6813661e..0de39af75 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -174,6 +174,7 @@ set (pythonTests timesheet.test.py tw-1379.test.py tw-1837.test.py + tw-1999.test.py tw-20.test.py tw-2575.test.py tw-262.test.py diff --git a/test/info.test.py b/test/info.test.py index 4d6945936..79ffe80fa 100755 --- a/test/info.test.py +++ b/test/info.test.py @@ -98,6 +98,9 @@ class TestInfoCommand(TestCase): self.assertRegex(out, r"Urgency\s+\d+(\.\d+)?") self.assertRegex(out, r"Priority\s+H") + self.assertRegex(out, r"Annotation of 'bar' added\.") + self.assertRegex(out, r"Tag 'tag' added\.") + self.assertRegex(out, r"tatus set to 'recurring'\.") self.assertIn("project", out) self.assertIn("active", out) self.assertIn("annotations", out) diff --git a/test/tw-1999.test.py b/test/tw-1999.test.py new file mode 100755 index 000000000..47bee92d8 --- /dev/null +++ b/test/tw-1999.test.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +############################################################################### +# +# Copyright 2006 - 2021, Tomas Babej, Paul Beckingham, Federico Hernandez. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# https://www.opensource.org/licenses/mit-license.php +# +############################################################################### + +import sys +import os +import unittest + +# Ensure python finds the local simpletap module +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from basetest import Task, TestCase + + +class TestBug1999(TestCase): + """Bug 1999: Taskwarrior reports wrong active time""" + + def setUp(self): + self.t = Task() + + def test_correct_active_time(self): + """Ensure correct active time locally""" + desc = "Testing task" + self.t(("add", desc)) + self.t(("start", "1")) + self.t.faketime("+10m") + self.t(("stop", "1")) + + code, out, err = self.t(("info", "1")) + self.assertRegex(out, "duration: 0:10:0[0-5]") + + +if __name__ == "__main__": + from simpletap import TAPTestRunner + + unittest.main(testRunner=TAPTestRunner()) + +# vim: ai sts=4 et sw=4 ft=python diff --git a/test/tw-2514.test.sh b/test/tw-2514.test.sh index 71343ed6e..4033bde81 100755 --- a/test/tw-2514.test.sh +++ b/test/tw-2514.test.sh @@ -6,7 +6,6 @@ task add Something I did yesterday task 1 mod start:yesterday+18h task 1 done end:yesterday+20h -# this does not work without journal.info # Check that 2 hour interval is reported by task info -#task info | grep -F "Start deleted" -#[[ ! -z `task info | grep -F "Start deleted (duration: 2:00:00)."` ]] +task info | grep -F "Start deleted" +[[ ! -z `task info | grep -F "Start deleted (duration: 2:00:00)."` ]]