Dependencies - #410

- Completed support for 'task 1 depends:2,-3' to manipulate the
  dependencies.
- Now supports rc.dependency.reminder to indicate when to nag about
  dependency chain violations, defaulting to on.
- Now supports rc.dependency.confirm to require confirmation before
  fixing dependency chains, defaulting to on.
- New source file dependency.cpp which implements a low-level API for
  determining dependency status, and assorted handlers for task state
  changes.
- Adds blocking tasks to the 'next' report.
- Added more dependency unit tests, changed the wording in a couple of
  them and numbered them for easy reference.
This commit is contained in:
Paul Beckingham
2010-10-03 18:52:59 -04:00
parent dea7b72b70
commit 7fdfcbacc6
11 changed files with 230 additions and 140 deletions

View File

@@ -13,6 +13,9 @@
+ Added feature #391, now the 'task color legend' command will show + Added feature #391, now the 'task color legend' command will show
samples of all the defined colors and color rules from your .taskrc samples of all the defined colors and color rules from your .taskrc
and theme. and theme.
+ Added feature #410, and now task supports dependencies between tasks
with the syntax 'task 1 depends:2' to add a dependency, or 'task 1
depends:-2' to remove a dependency.
+ Added feature #421, and now task can sync data files from two sources + Added feature #421, and now task can sync data files from two sources
via the 'merge' command. via the 'merge' command.
+ Added feature #423, now custom report filters allow rc overrides. + Added feature #423, now custom report filters allow rc overrides.

4
NEWS
View File

@@ -51,6 +51,10 @@ New configuration options in taskwarrior 1.9.3
(e.g. merge.default.uri). (e.g. merge.default.uri).
- push.*.uri to configure target locations for the push command. - push.*.uri to configure target locations for the push command.
- pull.*.uri to configure source locations for the pull command. - pull.*.uri to configure source locations for the pull command.
- dependency.confirm controls whether dependency chain repair needs to be
confirmed.
- dependency.reminder controls whether the user is nagged about dependency
chain violations.
Newly deprecated features in taskwarrior 1.9.3 Newly deprecated features in taskwarrior 1.9.3

View File

@@ -520,6 +520,10 @@ specified, taskwarrior will only show as many that will fit.
.B dependency.reminder=on .B dependency.reminder=on
Determines whether dependency chain violations generate reminders. Determines whether dependency chain violations generate reminders.
.TP
.B dependency.confirm=yes
Determines whether dependency chain repair requires confirmation.
.SS COLOR CONTROLS .SS COLOR CONTROLS
.TP .TP

View File

@@ -104,6 +104,7 @@ std::string Config::defaults =
"\n" "\n"
"# Dependency controls\n" "# Dependency controls\n"
"dependency.reminder=on # Nags on dependency chain violations\n" "dependency.reminder=on # Nags on dependency chain violations\n"
"dependency.confirmation=on # Should dependency chain repair be confirmed?\n"
"\n" "\n"
"# Urgency Coefficients\n" "# Urgency Coefficients\n"
"urgency.next.coefficient=10.0 # Urgency coefficients for 'next' special tag\n" "urgency.next.coefficient=10.0 # Urgency coefficients for 'next' special tag\n"

View File

@@ -885,14 +885,15 @@ int handleShow (std::string &outs)
"color.undo.after confirmation curses data.location dateformat " "color.undo.after confirmation curses data.location dateformat "
"dateformat.holiday dateformat.report dateformat.annotation debug " "dateformat.holiday dateformat.report dateformat.annotation debug "
"default.command default.priority default.project defaultwidth due " "default.command default.priority default.project defaultwidth due "
"dependency.reminder locale displayweeknumber export.ical.class " "dependency.confirmation dependency.reminder locale displayweeknumber "
"echo.command fontunderline locking monthsperline nag next journal.time " "export.ical.class echo.command fontunderline locking monthsperline nag "
"journal.time.start.annotation journal.time.stop.annotation project " "next journal.time journal.time.start.annotation "
"shadow.command shadow.file shadow.notify weekstart editor " "journal.time.stop.annotation project shadow.command shadow.file "
"import.synonym.id import.synonym.uuid complete.all.projects " "shadow.notify weekstart editor import.synonym.id import.synonym.uuid "
"complete.all.tags search.case.sensitive hooks active.indicator " "complete.all.projects complete.all.tags search.case.sensitive hooks "
"tag.indicator recurrence.indicator recurrence.limit list.all.projects " "active.indicator tag.indicator recurrence.indicator recurrence.limit "
"list.all.tags undo.style verbose rule.precedence.color merge.autopush " "list.all.projects list.all.tags undo.style verbose rule.precedence.color "
"merge.autopush "
#ifdef FEATURE_SHELL #ifdef FEATURE_SHELL
"shell.prompt " "shell.prompt "
#endif #endif

View File

@@ -533,12 +533,19 @@ int handleCustomReport (const std::string& report, std::string &outs)
table.setColumnJustification (columnCount, Table::left); table.setColumnJustification (columnCount, Table::left);
int row = 0; int row = 0;
std::vector <int> all; std::vector <Task> blocked;
std::vector <int> blocked_ids;
std::string deps; std::string deps;
foreach (task, tasks) foreach (task, tasks)
{ {
task->getDependencies (all); dependencyGetBlocking (*task, blocked);
join (deps, ", ", all); foreach (b, blocked)
blocked_ids.push_back (b->id);
join (deps, ",", blocked_ids);
blocked_ids.clear ();
blocked.clear ();
context.hooks.trigger ("format-depends", "depends", deps); context.hooks.trigger ("format-depends", "depends", deps);
table.addCell (row++, columnCount, deps); table.addCell (row++, columnCount, deps);
} }

View File

@@ -31,16 +31,16 @@
#include <Context.h> #include <Context.h>
#include <text.h> #include <text.h>
#include <util.h> #include <util.h>
#include <main.h>
extern Context context; extern Context context;
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
static bool followUpstream (const Task&, const Task&, const std::vector <Task>&, static bool followUpstream (const Task&, const Task&, std::vector <std::string>&);
std::vector <std::string>&);
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// A task is blocked if it depends on tasks that are pending or waiting. // A task is blocked if it depends on tasks that are pending or waiting.
bool dependencyIsBlocked (Task& task) bool dependencyIsBlocked (const Task& task)
{ {
if (task.has ("depends")) if (task.has ("depends"))
{ {
@@ -58,26 +58,25 @@ bool dependencyIsBlocked (Task& task)
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
void dependencyGetBlocked (Task& task, std::vector <Task>& blocked) void dependencyGetBlocked (const Task& task, std::vector <Task>& blocked)
{ {
std::string depends = task.get ("depends"); std::string uuid = task.get ("uuid");
if (depends != "")
{ const std::vector <Task>& all = context.tdb.getAllPending ();
const std::vector <Task>& all = context.tdb.getAllPending (); std::vector <Task>::const_iterator it;
std::vector <Task>::const_iterator it; for (it = all.begin (); it != all.end (); ++it)
for (it = all.begin (); it != all.end (); ++it) if ((it->getStatus () == Task::pending ||
if ((it->getStatus () == Task::pending || it->getStatus () == Task::waiting) &&
it->getStatus () == Task::waiting) && it->has ("depends") &&
depends.find (it->get ("uuid")) != std::string::npos) it->get ("depends").find (uuid) != std::string::npos)
blocked.push_back (*it); blocked.push_back (*it);
}
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// To be a blocking task, there must be at least one other task that depends on // To be a blocking task, there must be at least one other task that depends on
// this task, that is either pending or waiting. // this task, that is either pending or waiting.
bool dependencyIsBlocking (Task& task) bool dependencyIsBlocking (const Task& task)
{ {
std::string uuid = task.get ("uuid"); std::string uuid = task.get ("uuid");
@@ -94,18 +93,19 @@ bool dependencyIsBlocking (Task& task)
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
void dependencyGetBlocking (Task& task, std::vector <Task>& blocking) void dependencyGetBlocking (const Task& task, std::vector <Task>& blocking)
{ {
std::string uuid = task.get ("uuid"); std::string depends = task.get ("depends");
if (depends != "")
const std::vector <Task>& all = context.tdb.getAllPending (); {
std::vector <Task>::const_iterator it; const std::vector <Task>& all = context.tdb.getAllPending ();
for (it = all.begin (); it != all.end (); ++it) std::vector <Task>::const_iterator it;
if ((it->getStatus () == Task::pending || for (it = all.begin (); it != all.end (); ++it)
it->getStatus () == Task::waiting) && if ((it->getStatus () == Task::pending ||
it->has ("depends") && it->getStatus () == Task::waiting) &&
it->get ("depends").find (uuid) != std::string::npos) depends.find (it->get ("uuid")) != std::string::npos)
blocking.push_back (*it); blocking.push_back (*it);
}
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
@@ -118,12 +118,10 @@ void dependencyGetBlocking (Task& task, std::vector <Task>& blocking)
// Keep walking the chain, recording the links (a --> b, b --> c, ...) until // Keep walking the chain, recording the links (a --> b, b --> c, ...) until
// either the end of the chain is found (therefore not circular), or the chain // either the end of the chain is found (therefore not circular), or the chain
// loops and a repeat link is spotted (therefore circular). // loops and a repeat link is spotted (therefore circular).
bool dependencyIsCircular (Task& task) bool dependencyIsCircular (const Task& task)
{ {
std::vector <std::string> links; std::vector <std::string> seen;
const std::vector <Task>& all = context.tdb.getAllPending (); return followUpstream (task, task, seen);
return followUpstream (task, task, all, links);
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
@@ -132,50 +130,27 @@ bool dependencyIsCircular (Task& task)
static bool followUpstream ( static bool followUpstream (
const Task& task, const Task& task,
const Task& original, const Task& original,
const std::vector <Task>& all, std::vector <std::string>& seen)
std::vector <std::string>& links)
{ {
if (task.has ("depends")) std::vector <Task> blocking;
dependencyGetBlocking (task, blocking);
foreach (b, blocking)
{ {
std::vector <std::string> uuids; std::string link = task.get ("uuid") + " -> " + b->get ("uuid");
split (uuids, task.get ("depends"), ',');
std::vector <std::string>::iterator outer; // Have we seen this link before? If so, circularity has been detected.
for (outer = uuids.begin (); outer != uuids.end (); ++outer) if (std::find (seen.begin (), seen.end (), link) != seen.end ())
{ return true;
// Check that link has not already been seen.
// This is the actual circularity check - the rest of this function is seen.push_back (link);
// just chain-walking.
std::string link = task.get ("uuid") + " -> " + *outer;
if (std::find (links.begin (), links.end (), link) != links.end ())
return true;
links.push_back (link); // Use 'original' over '*b' if they both refer to the same task, otherwise
// '*b' is from TDB's committed list, and lacks recent modifications.
// Recurse up the chain. if (followUpstream (
std::vector <Task>::const_iterator inner; (b->get ("uuid") == original.get ("uuid") ? original : *b),
for (inner = all.begin (); inner != all.end (); ++inner) original,
{ seen))
if (*outer == inner->get ("uuid")) return true;
{
// Use the newly modified "task", not "*inner", which is the old
// unmodified version.
if (*outer == original.get ("uuid"))
{
if (followUpstream (task, original, all, links))
return true;
}
else
{
if (followUpstream (*inner, original, all, links))
return true;
}
break;
}
}
}
} }
return false; return false;
@@ -215,35 +190,49 @@ void dependencyChainOnComplete (Task& task)
std::vector <Task> blocking; std::vector <Task> blocking;
dependencyGetBlocking (task, blocking); dependencyGetBlocking (task, blocking);
std::cout << "# Task " << task.id << "\n";
foreach (t, blocking)
std::cout << "# blocking " << t->id << " " << t->get ("uuid") << "\n";
// If the task is anything but the tail end of a dependency chain. // If the task is anything but the tail end of a dependency chain.
if (blocking.size ()) if (blocking.size ())
{ {
std::vector <Task> blocked; std::vector <Task> blocked;
dependencyGetBlocked (task, blocked); dependencyGetBlocked (task, blocked);
foreach (t, blocked) // Nag about broken chain.
std::cout << "# blocked by " << t->id << " " << t->get ("uuid") << "\n"; if (context.config.getBoolean ("dependency.reminder"))
{
std::cout << "Task " << task.id << " is blocked by:\n";
foreach (b, blocking)
std::cout << " " << b->id << " " << b->get ("description") << "\n";
}
// If there are both blocking and blocked tasks, the chain is broken. // If there are both blocking and blocked tasks, the chain is broken.
if (blocked.size ()) if (blocked.size ())
{ {
// TODO Nag about broken chain. if (context.config.getBoolean ("dependency.reminder"))
std::cout << "# Chain broken - offer to repair\n";
// TODO Confirm that the chain should be repaired.
// Repair the chain - everything in blocked should now depend on
// everything in blocking, instead of task.id.
foreach (left, blocked)
{ {
left->removeDependency (task.id); std::cout << "and is blocking:\n";
foreach (b, blocked)
std::cout << " " << b->id << " " << b->get ("description") << "\n";
}
if (!context.config.getBoolean ("dependency.confirmation") ||
confirm ("Would you like the dependency chain fixed?"))
{
// Repair the chain - everything in blocked should now depend on
// everything in blocking, instead of task.id.
foreach (left, blocked)
{
left->removeDependency (task.id);
foreach (right, blocking)
left->addDependency (right->id);
}
// Now update TDB, now that the updates have all occurred.
foreach (left, blocked)
context.tdb.update (*left);
foreach (right, blocking) foreach (right, blocking)
left->addDependency (right->id); context.tdb.update (*right);
} }
} }
} }
@@ -252,27 +241,64 @@ void dependencyChainOnComplete (Task& task)
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
void dependencyChainOnStart (Task& task) void dependencyChainOnStart (Task& task)
{ {
std::stringstream out; if (context.config.getBoolean ("dependency.reminder"))
if (context.config.getBoolean ("dependency.reminder") /* &&
TODO check that task is actually blocked */)
{ {
out << "# dependencyChainScan nag! " std::vector <Task> blocking;
<< task.id dependencyGetBlocking (task, blocking);
<< " "
<< task.get ("uuid")
<< "\n";
context.footnote (out.str ()); // If the task is anything but the tail end of a dependency chain, nag about
// broken chain.
if (blocking.size ())
{
std::cout << "Task " << task.id << " is blocked by:\n";
foreach (b, blocking)
std::cout << " " << b->id << " " << b->get ("description") << "\n";
}
} }
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// Iff a dependency is being removed, is there something to do.
void dependencyChainOnModify (Task& before, Task& after) void dependencyChainOnModify (Task& before, Task& after)
{ {
// TODO Iff a dependency is being removed, is there anything to do. // TODO It is not clear that this should even happen. TBD.
/*
// Get the dependencies from before.
std::string depends = before.get ("depends");
std::vector <std::string> before_depends;
split (before_depends, depends, ',');
std::cout << "# dependencyChainOnModify before has " << before_depends.size () << "\n";
// Get the dependencies from after.
depends = after.get ("depends");
std::vector <std::string> after_depends;
split (after_depends, depends, ',');
std::cout << "# dependencyChainOnModify after has " << after_depends.size () << "\n";
// listDiff
std::vector <std::string> before_only;
std::vector <std::string> after_only;
listDiff (before_depends, after_depends, before_only, after_only);
// Any dependencies in before_only indicates that a dependency was removed.
if (before_only.size ())
{
std::cout << "# dependencyChainOnModify detected a dependency removal\n";
// before dep:2,3
// after dep:2
//
// any tasks blocked by after might should be repaired to depend on 3.
std::vector <Task> blocked;
dependencyGetBlocked (after, blocked);
foreach (b, blocked)
{
std::cout << "# dependencyChainOnModify\n";
}
}
*/
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////

View File

@@ -133,11 +133,11 @@ int handleExportiCal (std::string &);
int handleExportYAML (std::string &); int handleExportYAML (std::string &);
// dependency.cpp // dependency.cpp
bool dependencyIsBlocked (Task&); bool dependencyIsBlocked (const Task&);
void dependencyGetBlocked (Task&, std::vector <Task>&); void dependencyGetBlocked (const Task&, std::vector <Task>&);
bool dependencyIsBlocking (Task&); bool dependencyIsBlocking (const Task&);
void dependencyGetBlocking (Task&, std::vector <Task>&); void dependencyGetBlocking (const Task&, std::vector <Task>&);
bool dependencyIsCircular (Task&); bool dependencyIsCircular (const Task&);
void dependencyChainOnComplete (Task&); void dependencyChainOnComplete (Task&);
void dependencyChainOnStart (Task&); void dependencyChainOnStart (Task&);
void dependencyChainOnModify (Task&, Task&); void dependencyChainOnModify (Task&, Task&);

View File

@@ -471,11 +471,12 @@ bool nag (Task& task)
} }
// General form is "if there are no more deserving tasks", suppress the nag. // General form is "if there are no more deserving tasks", suppress the nag.
if (isOverdue ) return false; if (isOverdue ) return false;
if (pri == 'H' && !overdue ) return false; if (pri == 'H' && !overdue ) return false;
if (pri == 'M' && !overdue && !high ) return false; if (pri == 'M' && !overdue && !high ) return false;
if (pri == 'L' && !overdue && !high && !medium ) return false; if (pri == 'L' && !overdue && !high && !medium ) return false;
if (pri == ' ' && !overdue && !high && !medium && !low) return false; if (pri == ' ' && !overdue && !high && !medium && !low ) return false;
if (dependencyIsBlocking (task) && !dependencyIsBlocked (task)) return false;
// All the excuses are made, all that remains is to nag the user. // All the excuses are made, all that remains is to nag the user.
context.footnote (nagMessage); context.footnote (nagMessage);

View File

@@ -444,7 +444,7 @@ int handleInfo (std::string &outs)
// dependencies: blocked // dependencies: blocked
{ {
std::vector <Task> blocked; std::vector <Task> blocked;
dependencyGetBlocked (*task, blocked); dependencyGetBlocking (*task, blocked);
if (blocked.size ()) if (blocked.size ())
{ {
std::stringstream message; std::stringstream message;
@@ -461,7 +461,7 @@ int handleInfo (std::string &outs)
// dependencies: blocking // dependencies: blocking
{ {
std::vector <Task> blocking; std::vector <Task> blocking;
dependencyGetBlocking (*task, blocking); dependencyGetBlocked (*task, blocking);
if (blocking.size ()) if (blocking.size ())
{ {
std::stringstream message; std::stringstream message;
@@ -2633,6 +2633,22 @@ void gatherNextTasks (std::vector <Task>& tasks)
} }
} }
// blocking, not blocked
foreach (task, tasks)
{
if (dependencyIsBlocking (*task) &&
! dependencyIsBlocked (*task))
{
std::string project = task->get ("project");
if (countByProject[project] < limit && matching.find (task->id) == matching.end ())
{
++countByProject[project];
matching[task->id] = true;
filtered.push_back (*task);
}
}
}
// due:*, pri:H // due:*, pri:H
foreach (task, tasks) foreach (task, tasks)
{ {

View File

@@ -28,59 +28,71 @@
use strict; use strict;
use warnings; use warnings;
use Test::More tests => 39; use Test::More tests => 41;
# Create the rc file. # Create the rc file.
if (open my $fh, '>', 'dep.rc') if (open my $fh, '>', 'dep.rc')
{ {
print $fh "data.location=.\n"; print $fh "data.location=.\n";
print $fh "dependency.confirm=yes\n"; print $fh "dependency.confirmation=yes\n";
print $fh "report.depreport.columns=id,depends,description\n"; print $fh "report.depreport.columns=id,depends,description\n";
print $fh "report.depreport.labels=ID,Depends,Description\n"; print $fh "report.depreport.labels=ID,Depends,Description\n";
print $fh "report.depreport.filter=status:pending\n"; print $fh "report.depreport.filter=status:pending\n";
print $fh "report.depreport.sort=depends+\n"; print $fh "report.depreport.sort=depends+\n";
print $fh "nag=NAG";
close $fh; close $fh;
# [1]
ok (-r 'dep.rc', 'Created dep.rc'); ok (-r 'dep.rc', 'Created dep.rc');
} }
qx{../task rc:dep.rc add One}; qx{../task rc:dep.rc add One};
qx{../task rc:dep.rc add Two}; qx{../task rc:dep.rc add Two};
# [2]
my $output = qx{../task rc:dep.rc 1 dep:-2}; my $output = qx{../task rc:dep.rc 1 dep:-2};
like ($output, qr/Modified 0 tasks\./, 'dependencies - remove nonexistent dependency'); like ($output, qr/Modified 0 tasks\./, 'dependencies - remove nonexistent dependency');
# [3]
$output = qx{../task rc:dep.rc 1 dep:99}; $output = qx{../task rc:dep.rc 1 dep:99};
like ($output, qr/Could not create a dependency on task 99 - not found\./, 'dependencies - add dependency for nonexistent task'); like ($output, qr/Could not create a dependency on task 99 - not found\./, 'dependencies - add dependency for nonexistent task');
# [4]
$output = qx{../task rc:dep.rc 99 dep:1}; $output = qx{../task rc:dep.rc 99 dep:1};
like ($output, qr/Task 99 not found\./, 'dependencies - add dependency to nonexistent task'); like ($output, qr/Task 99 not found\./, 'dependencies - add dependency to nonexistent task');
# t 1 dep:2; t info 1 => blocked by 2 # [5,6] t 1 dep:2; t info 1 => blocked by 2
$output = qx{../task rc:dep.rc 1 dep:2; ../task rc:dep.rc info 1}; $output = qx{../task rc:dep.rc 1 dep:2; ../task rc:dep.rc info 1};
like ($output, qr/This task blocked by\s+2 Two\nUUID/, 'dependencies - trivial blocked'); like ($output, qr/This task blocked by\s+2 Two\nUUID/, 'dependencies - trivial blocked');
unlike ($output, qr/This task is blocking\n/, 'dependencies - trivial blocked'); unlike ($output, qr/This task is blocking\n/, 'dependencies - trivial blocked');
# t info 2 => blocking 1 # [7,8] t info 2 => blocking 1
$output = qx{../task rc:dep.rc info 2}; $output = qx{../task rc:dep.rc info 2};
unlike ($output, qr/This task blocked by/, 'dependencies - trivial blocking'); unlike ($output, qr/This task blocked by/, 'dependencies - trivial blocking');
like ($output, qr/This task is blocking\s+1 One\nUUID/, 'dependencies - trivial blocking'); like ($output, qr/This task is blocking\s+1 One\nUUID/, 'dependencies - trivial blocking');
# t 1 dep:2 (again) # [9] t 1 dep:2 (again)
$output = qx{../task rc:dep.rc 1 dep:2}; $output = qx{../task rc:dep.rc 1 dep:2};
like ($output, qr/Task 1 already depends on task 2\./, 'dependencies - add already existing dependency'); like ($output, qr/Task 1 already depends on task 2\./, 'dependencies - add already existing dependency');
# t 1 dep:1 => error # [10,11] t 1 dep:1 => error
$output = qx{../task rc:dep.rc 1 dep:1}; $output = qx{../task rc:dep.rc 1 dep:1};
like ($output, qr/A task cannot be dependent on itself\./, 'dependencies - cannot depend on self'); like ($output, qr/A task cannot be dependent on itself\./, 'dependencies - cannot depend on self');
unlike ($output, qr/Modified 1 task\./, 'dependencies - cannot depend on self'); unlike ($output, qr/Modified 1 task\./, 'dependencies - cannot depend on self');
# t 1 dep:2; t 2 dep:1 => error # [12,13] t 1 dep:2; t 2 dep:1 => error
$output = qx{../task rc:dep.rc 2 dep:1}; $output = qx{../task rc:dep.rc 2 dep:1};
like ($output, qr/Circular dependency detected and disallowed\./, 'dependencies - trivial circular'); like ($output, qr/Circular dependency detected and disallowed\./, 'dependencies - trivial circular');
unlike ($output, qr/Modified 1 task\./, 'dependencies - trivial circular'); unlike ($output, qr/Modified 1 task\./, 'dependencies - trivial circular');
# [14,15] t 1 dep:2; t 2 dep:3; t 1 dep:3 => not circular
qx{../task rc:dep.rc 1 dep:2};
qx{../task rc:dep.rc add Three};
qx{../task rc:dep.rc 2 dep:3};
$output = qx{../task rc:dep.rc 1 dep:3};
unlike ($output, qr/Circular dependency detected and disallowed\./, 'dependencies - diamond, non-circular');
like ($output, qr/Modified 1 task\./, 'dependencies - diamond, non-circular');
# [16]
unlink 'pending.data'; unlink 'pending.data';
ok (!-r 'pending.data', 'Removed pending.data for a fresh start'); ok (!-r 'pending.data', 'Removed pending.data for a fresh start');
@@ -92,11 +104,12 @@ qx{../task rc:dep.rc add Five};
qx{../task rc:dep.rc 5 dep:4; ../task rc:dep.rc 4 dep:3; ../task rc:dep.rc 3 dep:2; ../task rc:dep.rc 2 dep:1}; qx{../task rc:dep.rc 5 dep:4; ../task rc:dep.rc 4 dep:3; ../task rc:dep.rc 3 dep:2; ../task rc:dep.rc 2 dep:1};
# 5 dep 4 dep 3 dep 2 dep 1 dep 5 => error # [17,18] 5 dep 4 dep 3 dep 2 dep 1 dep 5 => error
$output = qx{../task rc:dep.rc 1 dep:5}; $output = qx{../task rc:dep.rc 1 dep:5};
like ($output, qr/Circular dependency detected and disallowed\./, 'dependencies - nontrivial circular'); like ($output, qr/Circular dependency detected and disallowed\./, 'dependencies - nontrivial circular');
unlike ($output, qr/Modified 1 task\./, 'dependencies - nontrivial circular'); unlike ($output, qr/Modified 1 task\./, 'dependencies - nontrivial circular');
# [19]
unlink 'pending.data'; unlink 'pending.data';
ok (!-r 'pending.data', 'Removed pending.data for a fresh start'); ok (!-r 'pending.data', 'Removed pending.data for a fresh start');
@@ -107,23 +120,28 @@ qx{../task rc:dep.rc add Four};
qx{../task rc:dep.rc add Five}; qx{../task rc:dep.rc add Five};
qx{../task rc:dep.rc add Six recurring due:tomorrow recur:daily}; qx{../task rc:dep.rc add Six recurring due:tomorrow recur:daily};
# [20]
$output = qx{../task rc:dep.rc 6 dep:5}; $output = qx{../task rc:dep.rc 6 dep:5};
unlike ($output,qr/Modified \d+ task/, 'dependencies - recurring task depending on another task'); like ($output, qr/Modified \d+ task/, 'dependencies - recurring task depending on another task');
# [21]
$output = qx{../task rc:dep.rc 5 dep:6}; $output = qx{../task rc:dep.rc 5 dep:6};
like ($output,qr/Modified \d+ task/, 'dependencies - task depending on recurring task'); like ($output, qr/Modified \d+ task/, 'dependencies - task depending on recurring task');
# t 1 dep:2,3,4; t 1 dep:-2,-4,5; t info 1 => blocked by 3,5 # [22] t 1 dep:2,3,4; t 1 dep:-2,-4,5; t info 1 => blocked by 3,5
$output = qx{../task rc:dep.rc 1 dep:2,3,4; ../task rc:dep.rc 1 dep:-2,-4,5; ../task rc:dep.rc info 1}; $output = qx{../task rc:dep.rc 1 dep:2,3,4; ../task rc:dep.rc 1 dep:-2,-4,5; ../task rc:dep.rc info 1};
like ($output, qr/This task blocked by\s+3 Three\n\s+5 Five\nThis task is blocking/, 'dependencies - multiple dependencies modified'); like ($output, qr/This task blocked by\s+3 Three\n\s+5 Five\nUUID/, 'dependencies - multiple dependencies modified');
# [23,24]
$output = qx{../task rc:dep.rc do 3,5; ../task rc:dep.rc info 1}; $output = qx{../task rc:dep.rc do 3,5; ../task rc:dep.rc info 1};
unlike ($output, qr/This task blocked by/, 'dependencies - task info reflects completed dependencies'); unlike ($output, qr/This task blocked by/, 'dependencies - task info reflects completed dependencies');
unlike ($output, qr/This task is blocking/, 'dependencies - task info reflects completed dependencies'); unlike ($output, qr/This task is blocking/, 'dependencies - task info reflects completed dependencies');
# [25]
$output = qx{../task rc:dep.rc depreport}; $output = qx{../task rc:dep.rc depreport};
like ($output, qr/\s1\s+One\s+/, 'dependencies - depends report column reflects completed dependencies'); like ($output, qr/\s1\s+One\s+/, 'dependencies - depends report column reflects completed dependencies');
# [26]
unlink 'pending.data'; unlink 'pending.data';
ok (!-r 'pending.data', 'Removed pending.data for a fresh start'); ok (!-r 'pending.data', 'Removed pending.data for a fresh start');
@@ -135,12 +153,16 @@ qx{../task rc:dep.rc add Four};
qx{../task rc:dep.rc 1 dep:3,4}; qx{../task rc:dep.rc 1 dep:3,4};
qx{../task rc:dep.rc do 2}; qx{../task rc:dep.rc do 2};
# [27]
$output = qx{../task rc:dep.rc depreport}; $output = qx{../task rc:dep.rc depreport};
like ($output, qr/\s1\s+2, 3\s+One\s+/, 'dependencies - depends report column reflects changed IDs'); like ($output, qr/\s1\s+2,3\s+One\s+/, 'dependencies - depends report column reflects changed IDs');
# [28]
qx{../task rc:dep.rc do 3};
$output = qx{../task rc:dep.rc depreport}; $output = qx{../task rc:dep.rc depreport};
like ($output, qr/\s1\s+One\s+/, 'dependencies - depends report column reflects completed dependencies'); like ($output, qr/\s1\s+2\s+One\s+/, 'dependencies - depends report column reflects completed dependencies');
# [29]
unlink 'pending.data'; unlink 'pending.data';
ok (!-r 'pending.data', 'Removed pending.data for a fresh start'); ok (!-r 'pending.data', 'Removed pending.data for a fresh start');
@@ -151,17 +173,20 @@ qx{../task rc:dep.rc add Four};
qx{../task rc:dep.rc 2 dep:1; ../task rc:dep.rc 3 dep:2; ../task rc:dep.rc 4 dep:3}; qx{../task rc:dep.rc 2 dep:1; ../task rc:dep.rc 3 dep:2; ../task rc:dep.rc 4 dep:3};
# [30,31]
$output = qx{echo y | ../task rc:dep.rc do 2}; $output = qx{echo y | ../task rc:dep.rc do 2};
like ($output, qr/fixed/, 'dependencies - user prompted to fix broken chain after completing a blocked task'); like ($output, qr/fixed/, 'dependencies - user prompted to fix broken chain after completing a blocked task');
like ($output, qr/is blocked by/, 'dependencies - user nagged for completing a blocked task');
like ($output, qr/NAG/, 'dependencies - user nagged for completing a blocked task'); # [32]
$output = qx{echo y | ../task rc:dep.rc do 1}; $output = qx{echo y | ../task rc:dep.rc do 1};
unlike ($output, qr/fixed/, 'dependencies - user not prompted to fix broken chain when the head of the chain is marked as complete'); unlike ($output, qr/fixed/, 'dependencies - user not prompted to fix broken chain when the head of the chain is marked as complete');
# [33]
$output = qx{echo y | ../task rc:dep.rc del 4}; $output = qx{echo y | ../task rc:dep.rc del 4};
unlike ($output, qr/fixed/, 'dependencies - user not prompted to fix broken chain when the tail of the chain is deleted'); unlike ($output, qr/fixed/, 'dependencies - user not prompted to fix broken chain when the tail of the chain is deleted');
# [34]
unlink 'pending.data'; unlink 'pending.data';
ok (!-r 'pending.data', 'Removed pending.data for a fresh start'); ok (!-r 'pending.data', 'Removed pending.data for a fresh start');
@@ -176,20 +201,22 @@ qx{../task rc:dep.rc 3 dep:2};
qx{../task rc:dep.rc 4 dep:3}; qx{../task rc:dep.rc 4 dep:3};
qx{../task rc:dep.rc 5 dep:4}; qx{../task rc:dep.rc 5 dep:4};
# [35]
qx{echo y | ../task rc:dep.rc do 2}; qx{echo y | ../task rc:dep.rc do 2};
$output = qx{../task rc:dep.rc depreport}; $output = qx{../task rc:dep.rc depreport};
like ($output, qr/\s1\s+One\s*\n\s2\s+1\s+Three\s*\n\s3\s+2\s+Four\s*\n\s4\s+3\s+Five/, 'dependencies - fixed chain after completing a blocked task'); like ($output, qr/\s1\s+One\s*\n\s2\s+1\s+Three\s*\n\s3\s+2\s+Four\s*\n\s4\s+3\s+Five/, 'dependencies - fixed chain after completing a blocked task');
# [36]
qx{printf "Y\nY\n" | ../task rc:dep.rc del 2}; qx{printf "Y\nY\n" | ../task rc:dep.rc del 2};
$output = qx{../task rc:dep.rc depreport}; $output = qx{../task rc:dep.rc depreport};
like ($output, qr/\s1\s+One\s*\n\s2\s+1\s+Four\s*\n\s3\s+2\s+Five/, 'dependencies - fixed chain after deleting a blocked task'); like ($output, qr/\s1\s+One\s*\n\s2\s+1\s+Four\s*\n\s3\s+2\s+Five/, 'dependencies - fixed chain after deleting a blocked task');
# [37]
qx{../task rc:dep.rc 2 dep:-1}; qx{../task rc:dep.rc 2 dep:-1};
$output = qx{../task rc:dep.rc depreport}; $output = qx{../task rc:dep.rc depreport};
like ($output, qr/\s1\s+One\s*\n\s2\s+Four\s*\n\s3\s+2\s+Five/, 'dependencies - chain should not be automatically repaired after manually removing a dependency'); like ($output, qr/\s1\s+One\s*\n\s2\s+Four\s*\n\s3\s+2\s+Five/, 'dependencies - chain should not be automatically repaired after manually removing a dependency');
# TODO - test dependency.confirm config variable # TODO - test dependency.confirmation config variable
# TODO - test undo on backing out chain gap repair # TODO - test undo on backing out chain gap repair
# TODO - test undo on backing out choice to not perform chain gap repair # TODO - test undo on backing out choice to not perform chain gap repair
# TODO - test blocked task completion nag # TODO - test blocked task completion nag