From 8d5473274e10b36263c17b511711d71d72ab25f1 Mon Sep 17 00:00:00 2001 From: Aleks-Daniel Jakimenko-Aleksejev Date: Sat, 6 Oct 2018 16:56:48 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=A5=F0=9F=A4=AF=20Great=20refactoring,?= =?UTF-8?q?=20many=20tests,=20IO=20task=20rework,=20etc.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Affected tickets: * Resolves #7: current task is now passed into the block (for both normal and IO tasks), with tests. * Resolves #12: using `run` in sink context now explodes properly (tested for both normal and IO tasks). * Resolves #13: no more shell injection through filenames, tested with extra tests for filenames starting with `--`. * Resolves #16: there are now helper functions to test sakefiles without the need to `.execute` tasks from within the test file. It works by creating a temp directory with a given Sakefile and running `sake` as external command (stdout, stderr, exit code and signals are checked). * Resolves #18: you can now pass an IO object instead of a Str and it will automatically dispatch to the right sub. * There is some groundwork for #17, and maybe it already works. I can't tell if it does because there are no tests for it yet. * Also some minimal groundwork for #9 (parallel execution). * Additionally, issue #14 should be more approachable now (because deps are now resolved). * Issue #15 (‘default’ task) was reworked a bit (with no notable functional changes). This commit splits Sake.pm6 into separate files for convenience. The hierarchy is perhaps not entirely right, but now it is much easier to refactor it further with all the tests. Because almost all lines were touched, this commit introduces some major code style changes (to my personal preference, e.g. unicode quotes). I'm not insisting on that style, so if anybody cares enough it should be possible to submit a PR with unicode stuff autoreplaced. I wish this commit was split into multiple digestable commits, but it was a single refactoring effort that I ended up shelving for a few months anyway, so I'm happy that it goes in at all. --- bin/sake | 19 ++--- lib/Sake.pm6 | 104 +++++++++++---------------- lib/Sake/Task.pm6 | 72 +++++++++++++++++++ lib/Sake/Task/IO.pm6 | 48 +++++++++++++ lib/Sake/TaskStore.pm6 | 19 +++++ t/00-basic.t | 26 ------- t/00-original-task.t | 26 +++++++ t/01-original-file.t | 34 +++++++++ t/10-dispatch.t | 159 +++++++++++++++++++++++++++++++++++++++++ t/20-signatures.t | 40 +++++++++++ t/21-tasks.t | 19 +++++ t/22-files.t | 30 ++++++++ t/23-deps.t | 44 ++++++++++++ t/24-task-task.t | 18 +++++ t/25-parallel.t | 14 ++++ t/27-default.t | 24 +++++++ t/28-current-task.t | 18 +++++ t/30-help.t | 14 ++++ t/31-customfile.t | 13 ++++ t/35-security.t | 20 ++++++ t/38-graph.t | 12 ++++ t/lib/SakeTester.pm6 | 34 +++++++++ 22 files changed, 711 insertions(+), 96 deletions(-) create mode 100644 lib/Sake/Task.pm6 create mode 100644 lib/Sake/Task/IO.pm6 create mode 100644 lib/Sake/TaskStore.pm6 delete mode 100644 t/00-basic.t create mode 100644 t/00-original-task.t create mode 100644 t/01-original-file.t create mode 100644 t/10-dispatch.t create mode 100644 t/20-signatures.t create mode 100644 t/21-tasks.t create mode 100644 t/22-files.t create mode 100644 t/23-deps.t create mode 100644 t/24-task-task.t create mode 100644 t/25-parallel.t create mode 100644 t/27-default.t create mode 100644 t/28-current-task.t create mode 100644 t/30-help.t create mode 100644 t/31-customfile.t create mode 100644 t/35-security.t create mode 100644 t/38-graph.t create mode 100644 t/lib/SakeTester.pm6 diff --git a/bin/sake b/bin/sake index a397b77..e88155d 100755 --- a/bin/sake +++ b/bin/sake @@ -3,13 +3,16 @@ use v6; use Sake; -sub MAIN (*@tasks, :$file = 'Sakefile') { - die "Could not find file $file!" unless $file.IO ~~ :e; - EVALFILE $file; - if @tasks < 1 { - note ‘No tasks given! Did you mean one of these?’; - note .indent: 4 for %Sake::TASKS.keys.sort; - exit 1 +sub MAIN(*@tasks, + :$file = ‘Sakefile’, + :$force = False, + ) { + + if !$file.IO.e { + note “Could not find file $file!”; + exit 2 } - for @tasks -> $t { execute($t); } + EVALFILE $file; + sake-precheck :$force; + execute @tasks || ‘default’ } diff --git a/lib/Sake.pm6 b/lib/Sake.pm6 index b7d09e6..5ea1203 100644 --- a/lib/Sake.pm6 +++ b/lib/Sake.pm6 @@ -1,73 +1,53 @@ -unit module Sake; - -our %TASKS; - -class Sake-Task { - has $.name = !!! 'name required'; - has @.deps; # dependencies - has &.body = !!! 'body required'; # code to execute for this task - has &.cond = { True }; # only execute when True - - method execute { - return unless self.cond.(); - for self.deps -> $d { execute($d); } - .(self) with self.body; - } - -} - -sub execute($task) is export { - if %TASKS{$task}:exists { - sink %TASKS{$task}.execute; - } else { - # TODO something more awesome here - $*ERR.say("No task named $task...skipping"); - } +use Sake::Task; +use Sake::TaskStore; +use Sake::Task::IO; + +sub EXPORT { + %( + Sake::Task::EXPORT::DEFAULT::, + Sake::Task::IO::EXPORT::DEFAULT::, + ) } -proto sub task(|) is export { * } - -my sub make-task($name, &body, :@deps=[], :&cond={True}) { - die "Duplicate task $name!" if %TASKS{~$name}; - %TASKS{~$name} = Sake-Task.new(:$name, :&body, :@deps, :&cond); -} +unit module Sake; -multi sub task(Str $name, &body) { - make-task($name, &body); +multi execute(Str $task) { + if %TASKS{$task}:!exists { + note “Task “$task” does not exist”; + note did-you-mean; + exit 2 + } + execute %TASKS{$task} } -multi sub task(Pair $name-deps, &body?) { - my ($name,$deps) := $name-deps.kv; # unpack name and dependencies - my @deps = $deps.list; # so that A => B and A => both work - return make-task($name, &body, :@deps); +multi execute(Sake::Task $task) { + my $result = $task.execute; + $result ~~ Promise + ?? await $result + !! $result } - -proto sub file(|) is export { * } - -my sub touch (Str $filename) { - run ‘touch’, ‘--’, $filename; +multi execute(*@tasks) is export { + my @non-existent = @tasks.grep: { %TASKS{$_}:!exists }; + if @non-existent { + note “Task “$_” does not exist” for @non-existent; + note did-you-mean; + exit 2 + } + (execute $_ for @tasks) } -multi sub file(Str $name, &body) { - return make-task( - $name, - { &body($_); touch $name }, - :cond(sub { $name.path !~~ :e; }) - ) -} +sub sake-precheck(:$force = False) is export { + my @errors = gather resolve-deps; + if @errors { + .note for @errors; -multi sub file(Pair $name-deps, &body) { - my ($name,$deps) := $name-deps.kv; # unpack name and dependencies - my @deps = $deps.list; # so that A => B and A => both work - my $cond = { - my $f = $name.path; - !($f ~~ :e) || $f.modified < all(map { $_.modified }, grep { $_ ~~ IO::Path }, @deps); - }; - return make-task( - $name, - { &body($_); touch $name }, - :@deps, - :cond($cond) - ) + if $force { + note ‘’; + note ‘Continuing with errors because of the --force flag’; + } else { + note ‘Exiting. Use --force flag if you want to ignore these errors’; + exit 1 + } + } } diff --git a/lib/Sake/Task.pm6 b/lib/Sake/Task.pm6 new file mode 100644 index 0000000..5a6a313 --- /dev/null +++ b/lib/Sake/Task.pm6 @@ -0,0 +1,72 @@ +use Sake::TaskStore; + +unit class Sake::Task; + +has $.name = !!! ‘name required’; +has @.deps; #= Task dependencies +has &.body = !!! ‘body required’; #= Code to execute for this task +has &.cond = { True }; #= Condition for task execution +has $.modification-time = -∞; +has $.ready = Promise.new; +has $.callframe; + +#| Executes the task, even if it was already executed. +method execute { + if $.ready.status !~~ Planned { + note “Warning: re-executing task “$.name” per your request” + } + resolve-deps self, :live; # in case the task was added later + await @.deps».readify; + + if $.cond() { + with self.body { + sink .signature.ACCEPTS(\(self)) + ?? .(self) + !! .(); + } + $!modification-time = now; + } + + $.ready.keep: $.modification-time unless $.ready.status ~~ Kept; +} + +method readify { + # TODO race conditions + self.execute unless $.ready.status ~~ Kept; + $.ready +} + + +multi resolve-deps($task, :$live = False) is export { + $task.deps .= map: { + do if $_ ~~ Sake::Task { + $_ # already resolved + } elsif %TASKS{$_}:exists { + %TASKS{$_} + } else { + my $msg = “Task $task.name() depends on $_ but no such task was found”; + $live ?? note $msg !! take $msg; + Empty + } + } +} + +multi resolve-deps is export { + my @errors = gather { resolve-deps $_ for %TASKS.values } + if @errors > 0 { # TODO what if it's another error + take $_ for @errors; + take did-you-mean + } +} + + + +proto sub task(|) is export {*} + +multi sub task(Str $name, &body?) { + make-task $name, &body, type => Sake::Task +} + +multi sub task(Pair (Str :key($name), :value($deps)), &body?) { + make-task $name, &body, type => Sake::Task, deps => $deps.list +} diff --git a/lib/Sake/Task/IO.pm6 b/lib/Sake/Task/IO.pm6 new file mode 100644 index 0000000..32adece --- /dev/null +++ b/lib/Sake/Task/IO.pm6 @@ -0,0 +1,48 @@ +use Sake::Task; +use Sake::TaskStore; + +unit class Sake::Task::IO is Sake::Task; + +method modification-time { + $.name.IO.e + ?? $.name.IO.modified + !! -∞ +} + +method execute { + resolve-deps self, :live; # in case the task was added later + await @.deps».readify; + + if $.cond() { + with self.body { + my $last-dep = @.deps».modification-time.max; + $last-dep = now if $last-dep == -∞; + sink .(self) if $.modification-time < $last-dep; + } + + my &touch = -> $filename { run , $filename }; + touch $.name.IO; + } + $.ready.keep: $.modification-time unless $.ready; +} + + + +multi sub task(IO $path, &body?) is export { + make-task $path, &body, type => Sake::Task::IO +} + +multi sub task(Pair (IO :key($path), :value($deps)), &body?) is export { + make-task $path, &body, deps => $deps.list, type => Sake::Task::IO +} + + +proto sub file(|) is export {*} + +multi sub file(IO() $path, &body?) { + task $path, &body +} + +multi sub file(Pair (IO() :key($path), :value($deps)), &body?) { + task $path => $deps, &body +} diff --git a/lib/Sake/TaskStore.pm6 b/lib/Sake/TaskStore.pm6 new file mode 100644 index 0000000..c552743 --- /dev/null +++ b/lib/Sake/TaskStore.pm6 @@ -0,0 +1,19 @@ +unit module Sake::TaskStore; + +our %TASKS is export; + +sub make-task($name, &body, + :@deps=[], :&cond={True}, :$type) is export { + if %TASKS{$name}:exists { + note “Duplicate task $name”; + exit 2 + } + %TASKS{~$name} = $type.new: :$name, :&body, :@deps, + :&cond, callframe => callframe +} + +sub did-you-mean is export { + return “\nNo tasks were defined” if %TASKS == 0; + “\nDid you mean one of these?\n” + ~ %TASKS.keys.sort».indent(4).join: “\n” +} diff --git a/t/00-basic.t b/t/00-basic.t deleted file mode 100644 index ef5821e..0000000 --- a/t/00-basic.t +++ /dev/null @@ -1,26 +0,0 @@ -use v6; -use Test; -use Sake; - -plan 5; - -#eval_lives_ok("use Sake;", "module can be used without error"); - -my $x = ""; - -my $t = task "fred", { $x = "meth"; } -$t.execute; -is($x,"meth", "can execute task via method"); - -$x = ""; -execute("fred"); -is($x,"meth", "can execute task by name"); - -(task "dino" => "fred", { $x = "sd"; }).execute; -is($x,"sd", "single dependency works fine"); - -(task "bedrock" => , { $x = "md"; }).execute; -is($x,"md", "multiple dependencies work fine"); - -(task "wilma" => ).execute; -is($x,"meth", "body is optional"); diff --git a/t/00-original-task.t b/t/00-original-task.t new file mode 100644 index 0000000..063b8cf --- /dev/null +++ b/t/00-original-task.t @@ -0,0 +1,26 @@ +use v6; +use lib ; +use Sake; +use Test; + +plan 5; + +my $x = ‘’; + +my $t = task ‘fred’, { $x = ‘meth’ } +$t.execute; +is $x, ‘meth’, ‘can execute task via method’; + +$x = ‘’; +execute ‘fred’; +is $x, ‘meth’, ‘can execute task by name’; + +(task ‘dino’ => ‘fred’, { $x = ‘sd’ }).execute; +is $x, ‘sd’, ‘single dependency works fine’; + +(task ‘bedrock’ => , { $x = ‘md’ }).execute; +is $x, ‘md’, ‘multiple dependencies work fine’; + +task ‘clear-headed-fred’, { $x = ‘again’ } +(task ‘wilma’ => ).execute; +is $x, ‘again’, ‘body is optional’; diff --git a/t/01-original-file.t b/t/01-original-file.t new file mode 100644 index 0000000..52ad016 --- /dev/null +++ b/t/01-original-file.t @@ -0,0 +1,34 @@ +use v6; +use lib ; +use Sake; +use Test; + +plan 11; + +my $x = ‘’; + +my $t = file ‘fred’, { $x = ‘meth’ } +$t.execute; +is $x, ‘meth’, ‘can execute file task via method’; +ok ‘fred’.IO.e, ‘file exists’; + +$x = ‘’; +‘fred’.IO.unlink; + +execute ‘fred’; +is $x, ‘meth’, ‘can execute file task by name’; +ok ‘fred’.IO.e, ‘file exists’; + +(file ‘dino’ => ‘fred’, { $x = ‘sd’ }).execute; +is $x, ‘sd’, ‘single file dependency works fine’; +ok ‘dino’.IO.e, ‘file exists’; + +(file ‘bedrock’ => , { $x = ‘md’ }).execute; +is $x, ‘md’, ‘multiple file dependencies work fine’; +ok ‘bedrock’.IO.e, ‘file exists’; + +file ‘clear-headed-fred’, { $x = ‘again’ } +(file ‘wilma’ => ).execute; +is $x, ‘again’, ‘body is optional’; +ok ‘clear-headed-fred’.IO.e, ‘file exists’; +ok ‘wilma’.IO.e, ‘file exists’; diff --git a/t/10-dispatch.t b/t/10-dispatch.t new file mode 100644 index 0000000..61b3ad5 --- /dev/null +++ b/t/10-dispatch.t @@ -0,0 +1,159 @@ +use v6; +use lib ; +use Sake; +use SakeTester; +use Test; + +plan 6 × 3; + +my $t; + + +# Normal tasks + +subtest ‘task with no deps and no block’, { + plan 4; + lives-ok { $t = task ‘no-dep-no-block’ }, ‘lives’; + is-deeply $t.deps, [], ‘correct deps’; + nok $t.body.defined, ‘correct body’; + is $t.^name, ‘Sake::Task’, ‘correct task type’; +} +subtest ‘task with no deps and a block’, { + plan 4; + lives-ok { $t = task ‘no-dep-block’, { 111 } }, ‘lives’; + is-deeply $t.deps, [], ‘correct deps’; + is-deeply $t.body.(), 111, ‘correct body’; + is $t.^name, ‘Sake::Task’, ‘correct task type’; +} + + +subtest ‘task with one dep and no block’, { + plan 4; + lives-ok { $t = task ‘str-dep-no-block’ => ‘one’ }, ‘lives’; + is-deeply $t.deps, [‘one’,], ‘correct deps’; + nok $t.body.defined, ‘correct body’; + is $t.^name, ‘Sake::Task’, ‘correct task type’; +} +subtest ‘task with one dep and a block’, { + plan 4; + lives-ok { $t = task ‘str-dep-block’ => ‘two’, { 222 } }, ‘lives’; + is-deeply $t.deps, [‘two’,], ‘correct deps’; + is-deeply $t.body.(), 222, ‘correct body’; + is $t.^name, ‘Sake::Task’, ‘correct task type’; +} + + +subtest ‘task with deps and no block’, { + plan 4; + lives-ok { $t = task ‘list-dep-no-block’ => }, ‘lives’; + is-deeply $t.deps, [‘foo’, ‘bar’], ‘correct deps’; + nok $t.body.defined, ‘correct body’; + is $t.^name, ‘Sake::Task’, ‘correct task type’; +} +subtest ‘task with deps and a block’, { + plan 4; + lives-ok { $t = task ‘list-dep-block’ => , {9} }, ‘lives’; + is-deeply $t.deps, [‘foo’, ‘bar’], ‘correct deps’; + is-deeply $t.body.(), 9, ‘correct body’; + is $t.^name, ‘Sake::Task’, ‘correct task type’; +} + + +# IO tasks + +subtest ‘IO task with no deps and no block’, { + plan 4; + lives-ok { $t = task ‘no-dep-no-blockIO’.IO }, ‘lives’; + is-deeply $t.deps, [], ‘correct deps’; + nok $t.body.defined, ‘correct body’; + is $t.^name, ‘Sake::Task::IO’, ‘correct task type’; +} +subtest ‘IO task with no deps and a block’, { + plan 4; + lives-ok { $t = task ‘no-dep-blockIO’.IO, { 111 } }, ‘lives’; + is-deeply $t.deps, [], ‘correct deps’; + is-deeply $t.body.(), 111, ‘correct body’; + is $t.^name, ‘Sake::Task::IO’, ‘correct task type’; +} + + +subtest ‘IO task with one dep and no block’, { + plan 4; + lives-ok { $t = task ‘str-dep-no-blockIO’.IO => ‘one’ }, ‘lives’; + is-deeply $t.deps, [‘one’,], ‘correct deps’; + nok $t.body.defined, ‘correct body’; + is $t.^name, ‘Sake::Task::IO’, ‘correct task type’; +} +subtest ‘IO task with one dep and a block’, { + plan 4; + lives-ok { $t = task ‘str-dep-blockIO’.IO => ‘two’, { 222 } }, ‘lives’; + is-deeply $t.deps, [‘two’,], ‘correct deps’; + is-deeply $t.body.(), 222, ‘correct body’; + is $t.^name, ‘Sake::Task::IO’, ‘correct task type’; +} + + +subtest ‘IO task with deps and no block’, { + plan 4; + lives-ok { $t = task ‘list-dep-no-blockIO’.IO => }, ‘lives’; + is-deeply $t.deps, [‘foo’, ‘bar’], ‘correct deps’; + nok $t.body.defined, ‘correct body’; + is $t.^name, ‘Sake::Task::IO’, ‘correct task type’; +} +subtest ‘IO task with deps and a block’, { + plan 4; + lives-ok { $t = task ‘list-dep-blockIO’.IO => , {9} }, ‘lives’; + is-deeply $t.deps, [‘foo’, ‘bar’], ‘correct deps’; + is-deeply $t.body.(), 9, ‘correct body’; + is $t.^name, ‘Sake::Task::IO’, ‘correct task type’; +} + + +# `file` IO tasks + +subtest ‘`file` IO task with no deps and no block’, { + plan 4; + lives-ok { $t = file ‘no-dep-no-block-file’ }, ‘lives’; + is-deeply $t.deps, [], ‘correct deps’; + nok $t.body.defined, ‘correct body’; + is $t.^name, ‘Sake::Task::IO’, ‘correct task type’; +} +subtest ‘`file` IO task with no deps and a block’, { + plan 4; + lives-ok { $t = file ‘no-dep-block-file’, { 111 } }, ‘lives’; + is-deeply $t.deps, [], ‘correct deps’; + is-deeply $t.body.(), 111, ‘correct body’; + is $t.^name, ‘Sake::Task::IO’, ‘correct task type’; +} + + +subtest ‘`file` IO task with one dep and no block’, { + plan 4; + lives-ok { $t = file ‘str-dep-no-block-file’ => ‘one’ }, ‘lives’; + is-deeply $t.deps, [‘one’,], ‘correct deps’; + nok $t.body.defined, ‘correct body’; + is $t.^name, ‘Sake::Task::IO’, ‘correct task type’; +} +subtest ‘`file` IO task with one dep and a block’, { + plan 4; + lives-ok { $t = file ‘str-dep-block-file’ => ‘two’, { 222 } }, ‘lives’; + is-deeply $t.deps, [‘two’,], ‘correct deps’; + is-deeply $t.body.(), 222, ‘correct body’; + is $t.^name, ‘Sake::Task::IO’, ‘correct task type’; +} + + +subtest ‘`file` IO task with deps and no block’, { + plan 4; + lives-ok { $t = file ‘list-dep-no-block-file’ => }, ‘lives’; + is-deeply $t.deps, [‘foo’, ‘bar’], ‘correct deps’; + nok $t.body.defined, ‘correct body’; + is $t.^name, ‘Sake::Task::IO’, ‘correct task type’; +} +subtest ‘`file` IO task with deps and a block’, { + plan 4; + lives-ok { $t = file ‘list-dep-block-file’ => , {9} }, ‘lives’; + is-deeply $t.deps, [‘foo’, ‘bar’], ‘correct deps’; + is-deeply $t.body.(), 9, ‘correct body’; + is $t.^name, ‘Sake::Task::IO’, ‘correct task type’; +} diff --git a/t/20-signatures.t b/t/20-signatures.t new file mode 100644 index 0000000..fc5e8ba --- /dev/null +++ b/t/20-signatures.t @@ -0,0 +1,40 @@ +use v6; +use lib ; +use Sake; +use SakeTester; +use Test; + +plan 7; + +given make-sake-directory 「task ‘foo’, { put 42 }」 { + test-run ‘just a block’, + , :out(“42\n”) +} + +given make-sake-directory 「task ‘foo’, -> $ { put 43 }」 { + test-run ‘one param’, + , :out(“43\n”) +} +given make-sake-directory 「task ‘foo’, sub ($) { put 44 }」 { + test-run ‘one param (sub)’, + , :out(“44\n”) +} + +given make-sake-directory 「task ‘foo’, -> { put 45 }」 { + test-run ‘no params’, + , :out(“45\n”) +} +given make-sake-directory 「task ‘foo’, sub { put 46 }」 { + test-run ‘no params (sub)’, + , :out(“46\n”) +} + +# Issue #7 +given make-sake-directory 「task ‘foo’, -> $task { put $task.name }」 { + test-run ‘task is passed’, + , :out(“foo\n”) +} +given make-sake-directory 「task ‘foo’, sub ($task) { put $task.name }」 { + test-run ‘task is passed (sub)’, + , :out(“foo\n”) +} diff --git a/t/21-tasks.t b/t/21-tasks.t new file mode 100644 index 0000000..9c76b4e --- /dev/null +++ b/t/21-tasks.t @@ -0,0 +1,19 @@ +use v6; +use lib ; +use Sake; +use SakeTester; +use Test; + +plan 2; + +given make-sake-directory 「task ‘foo’, { put ‘hello’ }」 { + test-run ‘simple task’, + , :out(“hello\n”) +} + +# Issue #12 +given make-sake-directory 「task ‘foo’, { run ‘false’ }」 { + test-run ‘failed Proc’, + , :out(‘’), :1exitcode, + :err(/‘The spawned command 'false' exited unsuccessfully’/) +} diff --git a/t/22-files.t b/t/22-files.t new file mode 100644 index 0000000..8e8a162 --- /dev/null +++ b/t/22-files.t @@ -0,0 +1,30 @@ +use v6; +use lib ; +use Sake; +use SakeTester; +use Test; + +plan 6; + +given make-sake-directory 「task ‘foo’.IO, { put ‘hello’ }」 { + test-run ‘simple IO task’, + , :out(“hello\n”); + ok .add(‘foo’).e, ‘file was touched’; +} + +given make-sake-directory 「file ‘foo’, { put ‘hello’ }」 { + test-run ‘simple IO task (file sub)’, + , :out(“hello\n”); + ok .add(‘foo’).e, ‘file was touched (file sub)’; +} + +# TODO test modification time + +# Issue #12 +given make-sake-directory 「task ‘foo’.IO, { run ‘false’ }」 { + test-run ‘failed Proc’, + , :out(‘’), :1exitcode, + :err(/‘The spawned command 'false' exited unsuccessfully’/); + + ok .add(‘foo’).e.not, ‘file not touched with failed Procs’; +} diff --git a/t/23-deps.t b/t/23-deps.t new file mode 100644 index 0000000..c81c99c --- /dev/null +++ b/t/23-deps.t @@ -0,0 +1,44 @@ +use v6; +use lib ; +use Sake; +use SakeTester; +use Test; + +plan 4; + +given make-sake-directory 「 +task ‘A’, { say ‘A!’ } +task ‘B’ => ‘A’, { say ‘B!’ } +」 { + test-run ‘one basic dep’, , :out(“A!\nB!\n”); +} + +given make-sake-directory 「 +task ‘A’, { say ‘A!’ } +task ‘B’ => ‘A’, { say ‘B!’ } +task ‘C’ => ‘B’, { say ‘C!’ } +task ‘D’ => ‘C’, { say ‘D!’ } +」 { + test-run ‘dependecy chain’, , :out(“A!\nB!\nC!\nD!\n”); +} + +given make-sake-directory 「 +task ‘A’, { say ‘A!’ } +task ‘B’, { say ‘B!’ } +task ‘C’ => , { say ‘C!’ } +」 { + test-run ‘multiple dependencies’, , :out(“A!\nB!\nC!\n”); +} + +given make-sake-directory 「 +task ‘F’ => , { say ‘F!’ } +task ‘B’, { say ‘B!’ } +task ‘D’ => , { say ‘D!’ } +task ‘A’, { say ‘A!’ } +task ‘E’, { say ‘E!’ } +task ‘C’ => , { say ‘C!’ } +」 { + test-run ‘shuffled dep tree’, , :out(“A!\nB!\nC!\nD!\nE!\nF!\n”); +} + +# TODO IO tasks? diff --git a/t/24-task-task.t b/t/24-task-task.t new file mode 100644 index 0000000..aad9fb6 --- /dev/null +++ b/t/24-task-task.t @@ -0,0 +1,18 @@ +use v6; +use lib ; +use Sake; +use SakeTester; +use Test; + +plan 1; + +given make-sake-directory 「 +task ‘foo’, { put ‘foo!’ } +task ‘bar’, { put ‘bar!’ } +task ‘baz’, { put ‘baz!’ } +」 { + test-run ‘multiple’, + , :out(“foo!\nbar!\nbaz!\n”) +} + +# TODO test and fix tasks in different order diff --git a/t/25-parallel.t b/t/25-parallel.t new file mode 100644 index 0000000..f682008 --- /dev/null +++ b/t/25-parallel.t @@ -0,0 +1,14 @@ +use v6; +use lib ; +use Sake; +use SakeTester; +use Test; + +plan :skip-all(‘NYI’); + +# TODO this file is just a placeholder + +#given make-sake-directory 「task ‘foo’, { put ‘hello’ }」 { +# test-run ‘simple task’, +# , :out(“hello\n”) +#} diff --git a/t/27-default.t b/t/27-default.t new file mode 100644 index 0000000..65bb7b1 --- /dev/null +++ b/t/27-default.t @@ -0,0 +1,24 @@ +use v6; +use lib ; +use Sake; +use SakeTester; +use Test; + +plan 2; + +given make-sake-directory 「 + task ‘first’, { put ‘first’ } + task ‘default’, { put ‘default ok’ } +」 { + test-run ‘no task implies default’, + , :out(“default ok\n”) +} + +given make-sake-directory 「 + task ‘first’, { put ‘first’ } +」 { + test-run ‘no task implies default’, + , :out(‘’), + :err(*), # TODO check & fix the error message + :2exitcode +} diff --git a/t/28-current-task.t b/t/28-current-task.t new file mode 100644 index 0000000..e7ecea7 --- /dev/null +++ b/t/28-current-task.t @@ -0,0 +1,18 @@ +use v6; +use lib ; +use Sake; +use SakeTester; +use Test; + +plan 2; + +# Issue #7 +given make-sake-directory 「task ‘foo’, { say .name }」 { + test-run ‘current task is passed to the block’, + , :out(“foo\n”) +} +skip ‘What should be done with IO task names?’, 1; +#given make-sake-directory 「task ‘foo’.IO, { say .name }」 { +# test-run ‘current IO task is passed to the block’, +# , :out(“foo\n”) +#} diff --git a/t/30-help.t b/t/30-help.t new file mode 100644 index 0000000..4711320 --- /dev/null +++ b/t/30-help.t @@ -0,0 +1,14 @@ +use v6; +use lib ; +use Sake; +use SakeTester; +use Test; + +plan :skip-all(‘NYI’); + +# TODO this file is a placeholder, see Issue #22 + +#given make-sake-directory 「task ‘foo’, { put ‘hello’ }」 { +# test-run ‘simple task’, +# , :out(“hello\n”) +#} diff --git a/t/31-customfile.t b/t/31-customfile.t new file mode 100644 index 0000000..2eb1c43 --- /dev/null +++ b/t/31-customfile.t @@ -0,0 +1,13 @@ +use v6; +use lib ; +use Sake; +use SakeTester; +use Test; + +plan 1; + +given make-sake-directory :filename(‘customfile’), + 「task ‘foo’, { put ‘hello’ }」 { + test-run ‘custom sakefile name’, + , :out(“hello\n”) +} diff --git a/t/35-security.t b/t/35-security.t new file mode 100644 index 0000000..f2c91c2 --- /dev/null +++ b/t/35-security.t @@ -0,0 +1,20 @@ +use v6; +use lib ; +use Sake; +use SakeTester; +use Test; + +plan 4; + +# Issue #13 +given make-sake-directory 「task 「`echo foo`」.IO, { put ‘hello’ }」 { + test-run ‘shell injection does not work’, + (‘sake’, 「`echo foo`」), :out(“hello\n”); + ok .add(「`echo foo`」).e, ‘file was touched’; +} + +given make-sake-directory 「task 「--foo」.IO, { put ‘hello’ }」 { + test-run ‘filenames are not interpreted as parameters’, + (, 「--foo」), :out(“hello\n”); + ok .add(「--foo」).e, ‘file was touched’; +} diff --git a/t/38-graph.t b/t/38-graph.t new file mode 100644 index 0000000..42910cd --- /dev/null +++ b/t/38-graph.t @@ -0,0 +1,12 @@ +use v6; +use lib ; +use Sake; +use SakeTester; +use Test; + +plan :skip-all(‘NYI’); + +#given make-sake-directory 「task ‘foo’, { put ‘hello’ }」 { +# test-run ‘simple task’, +# , :out(“hello\n”) +#} diff --git a/t/lib/SakeTester.pm6 b/t/lib/SakeTester.pm6 new file mode 100644 index 0000000..c26b915 --- /dev/null +++ b/t/lib/SakeTester.pm6 @@ -0,0 +1,34 @@ +unit module SakeTester; + +use File::Temp; +use Test; + +sub make-sake-directory($contents, :$filename = ‘Sakefile’) is export { + my $dir = tempdir.IO; + $dir.add($filename).spurt: $contents; + $dir +} + +sub test-run($description, *@args, + :$out = ‘’, :$err = ‘’, + :$exitcode = 0, + :$cwd is copy, + :$env is copy = %*ENV, + *%rest) is export { + + $cwd //= CALLERS::(‘$_’); + # TODO ↓ any better way ? + $env = %(|$env, + PATH => “{$*CWD.add: ‘bin’}:{$env // ‘’}”, + PERL6LIB => “{$*CWD.add: ‘lib’},{$env // ‘’}”, + ); + + subtest $description, { + plan 4; + my $proc = run @args, :$env, :$cwd, :out, :err, |%rest; + cmp-ok $proc.out.slurp, &[~~], $out, ‘stdout is correct’; + cmp-ok $proc.err.slurp, &[~~], $err, ‘stderr is correct’; + cmp-ok $proc.exitcode, &[~~], $exitcode, ‘exitcode is correct’; + cmp-ok $proc.signal, &[==], 0, ‘signal is 0’; + } +}