From 53eb96eac6baea584f89e25f304faf71c7dc96c7 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 23 Aug 2026 16:12:44 +0200 Subject: [PATCH 1/2] use blackbox 7 --- composer.json | 2 +- proofs/functional.php | 883 +++++++++++++++++++++--------------------- 2 files changed, 452 insertions(+), 433 deletions(-) diff --git a/composer.json b/composer.json index d033057..a49e4bb 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ }, "require-dev": { "innmind/static-analysis": "~1.3", - "innmind/black-box": "~6.5", + "innmind/black-box": "~7.0", "innmind/coding-standard": "~2.0" } } diff --git a/proofs/functional.php b/proofs/functional.php index 3813fcd..85b3528 100644 --- a/proofs/functional.php +++ b/proofs/functional.php @@ -25,41 +25,44 @@ use Innmind\BlackBox\{ Set, Tag, + Prove, }; -return static function() { - yield test( - 'Halting multiple tasks', - static function($assert) { - $expect = $assert->time(static function() { - Scheduler::of(Factory::build()) - ->sink(null) - ->with( - static fn($_, $__, $continuation) => $continuation - ->schedule(Sequence::of( - static fn($os) => $os->process()->halt(Period::second(1))->unwrap(), - static fn($os) => $os->process()->halt(Period::second(1))->unwrap(), - static fn($os) => $os->process()->halt(Period::second(1))->unwrap(), - )) - ->finish(), - ); - }); - $expect - ->inLessThan() - ->seconds(2); - $expect - ->inMoreThan() - ->seconds(1); - }, - )->tag(Tag::local, Tag::ci); +return static function(Prove $prove) { + yield $prove + ->test( + 'Halting multiple tasks', + static function($assert) { + $expect = $assert->time(static function() { + Scheduler::of(Factory::build()) + ->sink(null) + ->with( + static fn($_, $__, $continuation) => $continuation + ->schedule(Sequence::of( + static fn($os) => $os->process()->halt(Period::second(1))->unwrap(), + static fn($os) => $os->process()->halt(Period::second(1))->unwrap(), + static fn($os) => $os->process()->halt(Period::second(1))->unwrap(), + )) + ->finish(), + ); + }); + $expect + ->inLessThan() + ->seconds(2); + $expect + ->inMoreThan() + ->seconds(1); + }, + ) + ->tag(Tag::local, Tag::ci); - yield proof( - 'Carry value via the scope', - given( + yield $prove + ->proof('Carry value via the scope') + ->given( Set::type(), Set::type(), - ), - static function($assert, $initial, $modified) { + ) + ->test(static function($assert, $initial, $modified) { $returned = Scheduler::of(Factory::build()) ->sink($initial) ->with(static fn($_, $__, $continuation) => $continuation->finish()); @@ -82,15 +85,15 @@ static function($assert, $initial, $modified) { ->finish(), ); $assert->same($modified, $returned); - }, - )->tag(Tag::local, Tag::ci); + }) + ->tag(Tag::local, Tag::ci); - yield proof( - 'Retrieve the tasks results', - given( + yield $prove + ->proof('Retrieve the tasks results') + ->given( Set::type(), - ), - static function($assert, $value) { + ) + ->test(static function($assert, $value) { $values = Scheduler::of(Factory::build()) ->sink(Sequence::of()) ->with( @@ -103,386 +106,400 @@ static function($assert, $value) { ->wake(), ); $assert->same([$value], $values->toList()); - }, - )->tag(Tag::local, Tag::ci); + }) + ->tag(Tag::local, Tag::ci); + + yield $prove + ->test( + 'The scope is run asynchronously', + static function($assert) { + $expect = $assert->time(static function() { + Scheduler::of(Factory::build()) + ->sink(false) + ->with( + static fn($started, $os, $continuation) => match ([$started, $continuation->results()->size()]) { + [false, 0] => $continuation + ->schedule(Sequence::of( + static function($os) { + $_ = $os->process()->halt(Period::second(1))->unwrap(); + $_ = $os->process()->halt(Period::second(1))->unwrap(); + }, + static function($os) { + $_ = $os->process()->halt(Period::second(1))->unwrap(); + $_ = $os->process()->halt(Period::second(1))->unwrap(); + }, + static function($os) { + $_ = $os->process()->halt(Period::second(1))->unwrap(); + $_ = $os->process()->halt(Period::second(1))->unwrap(); + }, + )) + ->carryWith(true), + [true, 0] => (static function($os, $continuation) { + // this halt is executed at the same time at the + // second one in each task + $_ = $os->process()->halt(Period::second(1))->unwrap(); + + return $continuation; + })($os, $continuation), + default => $continuation->finish(), + }, + ); + }); + $expect + ->inLessThan() + ->seconds(3); + $expect + ->inMoreThan() + ->seconds(2); + }, + ) + ->tag(Tag::local, Tag::ci); + + yield $prove + ->test( + 'The scope and tasks are run asynchronously', + static function($assert) { + $expect = $assert->time(static function() use ($assert) { + $results = []; + Scheduler::of(Factory::build()) + ->sink(false) + ->with( + static function($started, $os, $continuation) use ($assert, &$results) { + if ($started) { + $_ = $os + ->process() + ->halt(Period::second(2)) + ->unwrap(); + $results[] = 'scope'; + + return $continuation->finish(); + } + + return $continuation + ->carryWith(true) + ->schedule(Sequence::of( + static function($os) use (&$results) { + // This task halts for 4 seconds because + // if less then it may sometime finish + // before the scope. (as 3-1 ~= 2s) + $_ = $os + ->process() + ->halt(Period::second(4)) + ->unwrap(); + $results[] = 'task 1'; + }, + static function($os) use (&$results) { + $_ = $os + ->process() + ->halt(Period::second(1)) + ->unwrap(); + $results[] = 'task 2'; + }, + )); + }, + ); + $assert->same( + ['task 2', 'scope', 'task 1'], + $results, + ); + }); + $expect + ->inLessThan() + ->seconds(5); + $expect + ->inMoreThan() + ->seconds(2); + }, + ) + ->tag(Tag::local, Tag::ci); + + yield $prove + ->test( + 'The scope and tasks are run asynchronously in different order', + static function($assert) { + $expect = $assert->time(static function() use ($assert) { + $results = []; + Scheduler::of(Factory::build()) + ->sink(false) + ->with( + static function($started, $os, $continuation) use ($assert, &$results) { + if ($started) { + $_ = $os + ->process() + ->halt(Period::second(3)) + ->unwrap(); + $_ = $os + ->process() + ->halt(Period::second(1)) + ->unwrap(); + $results[] = 'scope'; + + return $continuation->finish(); + } + + return $continuation + ->carryWith(true) + ->schedule(Sequence::of( + static function($os) use (&$results) { + $_ = $os + ->process() + ->halt(Period::second(2)) + ->unwrap(); + $results[] = 'task 1'; + }, + static function($os) use (&$results) { + $_ = $os + ->process() + ->halt(Period::second(1)) + ->unwrap(); + $results[] = 'task 2'; + }, + )); + }, + ); + $assert->same( + ['task 2', 'task 1', 'scope'], + $results, + ); + }); + $expect + ->inLessThan() + ->seconds(5); + $expect + ->inMoreThan() + ->seconds(2); + }, + ) + ->tag(Tag::local, Tag::ci); - yield test( - 'The scope is run asynchronously', - static function($assert) { - $expect = $assert->time(static function() { + yield $prove + ->test( + 'Streams read by lines are handled asynchronously', + static function($assert) { + $lines = []; Scheduler::of(Factory::build()) - ->sink(false) + ->sink(null) ->with( - static fn($started, $os, $continuation) => match ([$started, $continuation->results()->size()]) { - [false, 0] => $continuation + static function($_, $__, $continuation) use ($assert, &$lines) { + return $continuation ->schedule(Sequence::of( - static function($os) { - $_ = $os->process()->halt(Period::second(1))->unwrap(); - $_ = $os->process()->halt(Period::second(1))->unwrap(); - }, - static function($os) { - $_ = $os->process()->halt(Period::second(1))->unwrap(); - $_ = $os->process()->halt(Period::second(1))->unwrap(); + static function($os) use ($assert, &$lines) { + $file = $os + ->filesystem() + ->mount(Path::of('./')) + ->unwrap() + ->get(Name::of('composer.json')) + ->match( + static fn($file) => $file, + static fn() => null, + ); + $assert->not()->null($file); + $lines[] = $file + ->content() + ->lines() + ->first() + ->match( + static fn($line) => $line->toString(), + static fn() => null, + ); + $lines[] = $file + ->content() + ->lines() + ->filter(static fn($line) => !$line->str()->empty()) + ->last() + ->match( + static fn($line) => $line->toString(), + static fn() => null, + ); }, - static function($os) { - $_ = $os->process()->halt(Period::second(1))->unwrap(); - $_ = $os->process()->halt(Period::second(1))->unwrap(); + static function($os) use ($assert, &$lines) { + $file = $os + ->filesystem() + ->mount(Path::of('./')) + ->unwrap() + ->get(Name::of('LICENSE')) + ->match( + static fn($file) => $file, + static fn() => null, + ); + $assert->not()->null($file); + $lines[] = $file + ->content() + ->lines() + ->first() + ->match( + static fn($line) => $line->toString(), + static fn() => null, + ); + $lines[] = $file + ->content() + ->lines() + ->filter(static fn($line) => !$line->str()->empty()) + ->last() + ->match( + static fn($line) => $line->toString(), + static fn() => null, + ); }, )) - ->carryWith(true), - [true, 0] => (static function($os, $continuation) { - // this halt is executed at the same time at the - // second one in each task - $_ = $os->process()->halt(Period::second(1))->unwrap(); - - return $continuation; - })($os, $continuation), - default => $continuation->finish(), + ->finish(); }, ); - }); - $expect - ->inLessThan() - ->seconds(3); - $expect - ->inMoreThan() - ->seconds(2); - }, - )->tag(Tag::local, Tag::ci); + $assert->same( + ['{', 'MIT License', 'SOFTWARE.', '}'], + $lines, + ); + }, + ) + ->tag(Tag::local, Tag::ci); - yield test( - 'The scope and tasks are run asynchronously', - static function($assert) { - $expect = $assert->time(static function() use ($assert) { - $results = []; + yield $prove + ->test( + 'Streams read by chunks are handled asynchronously', + static function($assert) { + $chunks = []; Scheduler::of(Factory::build()) - ->sink(false) + ->sink(null) ->with( - static function($started, $os, $continuation) use ($assert, &$results) { - if ($started) { - $_ = $os - ->process() - ->halt(Period::second(2)) - ->unwrap(); - $results[] = 'scope'; - - return $continuation->finish(); - } - + static function($_, $__, $continuation) use ($assert, &$chunks) { return $continuation - ->carryWith(true) ->schedule(Sequence::of( - static function($os) use (&$results) { - // This task halts for 4 seconds because - // if less then it may sometime finish - // before the scope. (as 3-1 ~= 2s) - $_ = $os - ->process() - ->halt(Period::second(4)) - ->unwrap(); - $results[] = 'task 1'; + static function($os) use ($assert, &$chunks) { + $file = $os + ->filesystem() + ->mount(Path::of('./')) + ->unwrap() + ->get(Name::of('composer.lock')) + ->match( + static fn($file) => $file, + static fn() => null, + ); + $assert->not()->null($file); + $chunks[] = $file + ->content() + ->chunks() + ->last() + ->match( + static fn($chunk) => $chunk->takeEnd(5)->toString(), + static fn() => null, + ); }, - static function($os) use (&$results) { - $_ = $os - ->process() - ->halt(Period::second(1)) - ->unwrap(); - $results[] = 'task 2'; + static function($os) use ($assert, &$chunks) { + $file = $os + ->filesystem() + ->mount(Path::of('./')) + ->unwrap() + ->get(Name::of('LICENSE')) + ->match( + static fn($file) => $file, + static fn() => null, + ); + $assert->not()->null($file); + $chunks[] = $file + ->content() + ->chunks() + ->last() + ->match( + static fn($chunk) => $chunk->takeEnd(5)->toString(), + static fn() => null, + ); }, - )); + )) + ->finish(); }, ); + // since the license file is shorter it finishes first even though + // it started after reading the composer.lock file thus showing the + // chunks are read asynchronously $assert->same( - ['task 2', 'scope', 'task 1'], - $results, + ["ARE.\n", "0\"\n}\n"], + $chunks, ); - }); - $expect - ->inLessThan() - ->seconds(5); - $expect - ->inMoreThan() - ->seconds(2); - }, - )->tag(Tag::local, Tag::ci); + }, + ) + ->tag(Tag::local, Tag::ci); - yield test( - 'The scope and tasks are run asynchronously in different order', - static function($assert) { - $expect = $assert->time(static function() use ($assert) { - $results = []; - Scheduler::of(Factory::build()) - ->sink(false) + yield $prove + ->test( + 'HTTP requests are handled asynchronously', + static function($assert) { + $order = []; + Scheduler::of(Factory::build()->map(Resilient::new())) + ->sink(null) ->with( - static function($started, $os, $continuation) use ($assert, &$results) { - if ($started) { - $_ = $os - ->process() - ->halt(Period::second(3)) - ->unwrap(); - $_ = $os - ->process() - ->halt(Period::second(1)) - ->unwrap(); - $results[] = 'scope'; - - return $continuation->finish(); - } - + static function($_, $__, $continuation) use ($assert, &$order) { return $continuation - ->carryWith(true) ->schedule(Sequence::of( - static function($os) use (&$results) { + static function($os) use ($assert, &$order) { $_ = $os - ->process() - ->halt(Period::second(2)) - ->unwrap(); - $results[] = 'task 1'; + ->remote() + ->http()(Request::of( + Url::of('https://httpbin.org/delay/5'), + Method::get, + ProtocolVersion::v11, + )) + ->match( + static fn() => null, + static fn() => null, + ); + $order[] = 'first'; }, - static function($os) use (&$results) { + static function($os) use ($assert, &$order) { $_ = $os - ->process() - ->halt(Period::second(1)) - ->unwrap(); - $results[] = 'task 2'; + ->remote() + ->http()(Request::of( + Url::of('https://httpbin.org/delay/1'), + Method::get, + ProtocolVersion::v11, + )) + ->match( + static fn() => null, + static fn() => null, + ); + $order[] = 'second'; }, - )); + )) + ->finish(); }, ); - $assert->same( - ['task 2', 'task 1', 'scope'], - $results, - ); - }); - $expect - ->inLessThan() - ->seconds(5); - $expect - ->inMoreThan() - ->seconds(2); - }, - )->tag(Tag::local, Tag::ci); - yield test( - 'Streams read by lines are handled asynchronously', - static function($assert) { - $lines = []; - Scheduler::of(Factory::build()) - ->sink(null) - ->with( - static function($_, $__, $continuation) use ($assert, &$lines) { - return $continuation - ->schedule(Sequence::of( - static function($os) use ($assert, &$lines) { - $file = $os - ->filesystem() - ->mount(Path::of('./')) - ->unwrap() - ->get(Name::of('composer.json')) - ->match( - static fn($file) => $file, - static fn() => null, - ); - $assert->not()->null($file); - $lines[] = $file - ->content() - ->lines() - ->first() - ->match( - static fn($line) => $line->toString(), - static fn() => null, - ); - $lines[] = $file - ->content() - ->lines() - ->filter(static fn($line) => !$line->str()->empty()) - ->last() - ->match( - static fn($line) => $line->toString(), - static fn() => null, - ); - }, - static function($os) use ($assert, &$lines) { - $file = $os - ->filesystem() - ->mount(Path::of('./')) - ->unwrap() - ->get(Name::of('LICENSE')) - ->match( - static fn($file) => $file, - static fn() => null, - ); - $assert->not()->null($file); - $lines[] = $file - ->content() - ->lines() - ->first() - ->match( - static fn($line) => $line->toString(), - static fn() => null, - ); - $lines[] = $file - ->content() - ->lines() - ->filter(static fn($line) => !$line->str()->empty()) - ->last() - ->match( - static fn($line) => $line->toString(), - static fn() => null, - ); - }, - )) - ->finish(); - }, - ); - $assert->same( - ['{', 'MIT License', 'SOFTWARE.', '}'], - $lines, - ); - }, - )->tag(Tag::local, Tag::ci); - - yield test( - 'Streams read by chunks are handled asynchronously', - static function($assert) { - $chunks = []; - Scheduler::of(Factory::build()) - ->sink(null) - ->with( - static function($_, $__, $continuation) use ($assert, &$chunks) { - return $continuation - ->schedule(Sequence::of( - static function($os) use ($assert, &$chunks) { - $file = $os - ->filesystem() - ->mount(Path::of('./')) - ->unwrap() - ->get(Name::of('composer.lock')) - ->match( - static fn($file) => $file, - static fn() => null, - ); - $assert->not()->null($file); - $chunks[] = $file - ->content() - ->chunks() - ->last() - ->match( - static fn($chunk) => $chunk->takeEnd(5)->toString(), - static fn() => null, - ); - }, - static function($os) use ($assert, &$chunks) { - $file = $os - ->filesystem() - ->mount(Path::of('./')) - ->unwrap() - ->get(Name::of('LICENSE')) - ->match( - static fn($file) => $file, - static fn() => null, - ); - $assert->not()->null($file); - $chunks[] = $file - ->content() - ->chunks() - ->last() - ->match( - static fn($chunk) => $chunk->takeEnd(5)->toString(), - static fn() => null, - ); - }, - )) - ->finish(); - }, + $assert->same( + ['second', 'first'], + $order, ); - // since the license file is shorter it finishes first even though - // it started after reading the composer.lock file thus showing the - // chunks are read asynchronously - $assert->same( - ["ARE.\n", "0\"\n}\n"], - $chunks, - ); - }, - )->tag(Tag::local, Tag::ci); + }, + ) + ->tag(Tag::local); - yield test( - 'HTTP requests are handled asynchronously', - static function($assert) { - $order = []; - Scheduler::of(Factory::build()->map(Resilient::new())) - ->sink(null) - ->with( - static function($_, $__, $continuation) use ($assert, &$order) { - return $continuation + yield $prove + ->test( + 'Discard results', + static function($assert) { + $results = Scheduler::of(Factory::build()) + ->sink(Sequence::of()) + ->with( + static fn($all, $__, $continuation) => $continuation ->schedule(Sequence::of( - static function($os) use ($assert, &$order) { - $_ = $os - ->remote() - ->http()(Request::of( - Url::of('https://httpbin.org/delay/5'), - Method::get, - ProtocolVersion::v11, - )) - ->match( - static fn() => null, - static fn() => null, - ); - $order[] = 'first'; - }, - static function($os) use ($assert, &$order) { - $_ = $os - ->remote() - ->http()(Request::of( - Url::of('https://httpbin.org/delay/1'), - Method::get, - ProtocolVersion::v11, - )) - ->match( - static fn() => null, - static fn() => null, - ); - $order[] = 'second'; - }, - )) - ->finish(); - }, - ); - - $assert->same( - ['second', 'first'], - $order, - ); - }, - )->tag(Tag::local); - - yield test( - 'Discard results', - static function($assert) { - $results = Scheduler::of(Factory::build()) - ->sink(Sequence::of()) - ->with( - static fn($all, $__, $continuation) => $continuation - ->schedule(Sequence::of( - static fn($os) => $os->process()->halt(Period::second(1))->unwrap(), - static fn($os) => $os->process()->halt(Period::second(1))->unwrap(), - static fn($os) => $os->process()->halt(Period::second(1))->unwrap(), - )->map(Task\Discard::result(...))) - ->carryWith($all->append($continuation->results())) - ->wake(), - ); + static fn($os) => $os->process()->halt(Period::second(1))->unwrap(), + static fn($os) => $os->process()->halt(Period::second(1))->unwrap(), + static fn($os) => $os->process()->halt(Period::second(1))->unwrap(), + )->map(Task\Discard::result(...))) + ->carryWith($all->append($continuation->results())) + ->wake(), + ); - $assert->same(0, $results->size()); - }, - )->tag(Tag::local, Tag::ci); + $assert->same(0, $results->size()); + }, + ) + ->tag(Tag::local, Tag::ci); - yield proof( - 'Limit concurrency', - given( + yield $prove + ->proof('Limit concurrency') + ->given( Set::integers()->between(2, 10), Set::integers()->between(2, 10), - ), - static function($assert, $tasks, $max) { + ) + ->test(static function($assert, $tasks, $max) { $assert ->time(static function() use ($tasks, $max) { Scheduler::of(Factory::build()) @@ -507,63 +524,65 @@ static function($assert, $tasks, $max) { // +1 as the system itself takes a bit of time to run ((int) \ceil($tasks / $max)) + 1, ); - }, - )->tag(Tag::local, Tag::ci); + }) + ->tag(Tag::local, Tag::ci); - yield test( - 'Abort tasks', - static function($assert) { - $expect = $assert->time(static function() { - // the concurrency limit is here to make sure the remaining 8 - // tasks won't escape the abortion - Scheduler::of(Factory::build()) - ->limitConcurrencyTo(2) - ->sink(false) - ->with( - static function($started, $os, $continuation) { - if (!$started) { - return $continuation - ->carryWith(true) - ->schedule(Sequence::of()->pad( - 10, - static function($os) { - $continue = true; - $_ = $os - ->process() - ->signals() - ->listen( - Signal::terminate, - static function() use (&$continue) { - $continue = false; - }, - ) - ->unwrap(); - - while ($continue) { + yield $prove + ->test( + 'Abort tasks', + static function($assert) { + $expect = $assert->time(static function() { + // the concurrency limit is here to make sure the remaining 8 + // tasks won't escape the abortion + Scheduler::of(Factory::build()) + ->limitConcurrencyTo(2) + ->sink(false) + ->with( + static function($started, $os, $continuation) { + if (!$started) { + return $continuation + ->carryWith(true) + ->schedule(Sequence::of()->pad( + 10, + static function($os) { + $continue = true; $_ = $os ->process() - ->halt(Period::second(2)) + ->signals() + ->listen( + Signal::terminate, + static function() use (&$continue) { + $continue = false; + }, + ) ->unwrap(); - } - }, - )); - } - $_ = $os - ->process() - ->halt(Period::second(1)) - ->unwrap(); + while ($continue) { + $_ = $os + ->process() + ->halt(Period::second(2)) + ->unwrap(); + } + }, + )); + } - return $continuation->terminate(); - }, - ); - }); - $expect - ->inLessThan() - ->seconds(3); - $expect - ->inMoreThan() - ->seconds(1); - }, - )->tag(Tag::local, Tag::ci); + $_ = $os + ->process() + ->halt(Period::second(1)) + ->unwrap(); + + return $continuation->terminate(); + }, + ); + }); + $expect + ->inLessThan() + ->seconds(3); + $expect + ->inMoreThan() + ->seconds(1); + }, + ) + ->tag(Tag::local, Tag::ci); }; From aaa120288ae7e918cb58246e04a1c57ca7aec7bf Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 23 Aug 2026 16:14:14 +0200 Subject: [PATCH 2/2] simplify blackbox configuration --- blackbox.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/blackbox.php b/blackbox.php index 2588bdc..00553dc 100644 --- a/blackbox.php +++ b/blackbox.php @@ -10,18 +10,17 @@ }; Application::new($argv) - ->when( - \getenv('ENABLE_COVERAGE') !== false, - static fn(Application $app) => $app - ->scenariiPerProof(1) + ->map(static fn($app) => match (\getenv('BLACKBOX_ENV')) { + 'coverage' => $app ->codeCoverage( CodeCoverage::of( __DIR__.'/src/', __DIR__.'/proofs/', ) - ->dumpTo('coverage.clover') - ->enableWhen(true), - ), - ) + ->dumpTo('coverage.clover'), + ) + ->scenariiPerProof(1), + default => $app, + }) ->tryToProve(Load::everythingIn(__DIR__.'/proofs/')) ->exit();