From f58809f5e44c0107e9b1bc6857b0097c138a7c51 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 23 Nov 2025 17:00:42 +0100 Subject: [PATCH 1/9] require php 8.4 --- .github/workflows/ci.yml | 10 ++++------ CHANGELOG.md | 6 ++++++ composer.json | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 189105d..779f162 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,13 +4,11 @@ on: [push, pull_request] jobs: blackbox: - uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@main + uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@next coverage: - uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@main + uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@next secrets: inherit psalm: - uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@main + uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@next cs: - uses: innmind/github-workflows/.github/workflows/cs.yml@main - with: - php-version: '8.2' + uses: innmind/github-workflows/.github/workflows/cs.yml@next diff --git a/CHANGELOG.md b/CHANGELOG.md index 6522d61..ba2b67c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Changed + +- Requires PHP `8.4` + ## 4.0.0 - 2025-07-13 ### Added diff --git a/composer.json b/composer.json index db664a8..54a1080 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "issues": "http://github.com/Innmind/CLI/issues" }, "require": { - "php": "~8.2", + "php": "~8.4", "innmind/io": "~3.2", "innmind/immutable": "~5.16", "innmind/url": "~4.0", From 64ce9d3eae70eadb1f2a7ab9609f91e8724c833c Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 23 Nov 2025 17:06:04 +0100 Subject: [PATCH 2/9] update dependencies --- composer.json | 26 ++++++++++++++----- tests/Command/Pattern/OptionFlagTest.php | 2 +- tests/Command/Pattern/OptionWithValueTest.php | 8 +++--- .../Command/Pattern/OptionalArgumentTest.php | 4 +-- .../Command/Pattern/RequiredArgumentTest.php | 2 +- tests/Command/PatternTest.php | 2 +- tests/Question/ChoiceQuestionTest.php | 2 +- 7 files changed, 30 insertions(+), 16 deletions(-) diff --git a/composer.json b/composer.json index 54a1080..1a231d7 100644 --- a/composer.json +++ b/composer.json @@ -16,12 +16,26 @@ }, "require": { "php": "~8.4", - "innmind/io": "~3.2", - "innmind/immutable": "~5.16", - "innmind/url": "~4.0", - "innmind/operating-system": "~6.0", - "innmind/stack-trace": "~4.0", - "innmind/validation": "~2.0" + "innmind/io": "dev-next", + "innmind/immutable": "dev-next", + "innmind/url": "dev-next", + "innmind/operating-system": "dev-next", + "innmind/stack-trace": "dev-next", + "innmind/validation": "dev-next", + "innmind/server-status": "dev-next", + "innmind/http-transport": "dev-next", + "innmind/file-watch": "dev-next", + "innmind/signals": "dev-next", + "innmind/http": "dev-next", + "innmind/server-control": "dev-next", + "innmind/filesystem": "dev-next", + "innmind/time-warp": "dev-next", + "innmind/media-type": "dev-next", + "innmind/ip": "dev-next", + "innmind/time-continuum": "dev-next", + "innmind/graphviz": "dev-next", + "innmind/colour": "dev-next", + "formal/access-layer": "dev-next" }, "autoload": { "psr-4": { diff --git a/tests/Command/Pattern/OptionFlagTest.php b/tests/Command/Pattern/OptionFlagTest.php index 476d439..069f582 100644 --- a/tests/Command/Pattern/OptionFlagTest.php +++ b/tests/Command/Pattern/OptionFlagTest.php @@ -77,7 +77,7 @@ public function testParse() ); $this->assertSame(['watev', 'bar', '--unknown', 'baz'], $arguments->toList()); - $this->assertCount(1, $options); + $this->assertSame(1, $options->size()); $this->assertSame('', $options->get('foo')->match( static fn($value) => $value, static fn() => null, diff --git a/tests/Command/Pattern/OptionWithValueTest.php b/tests/Command/Pattern/OptionWithValueTest.php index a63c5df..d711257 100644 --- a/tests/Command/Pattern/OptionWithValueTest.php +++ b/tests/Command/Pattern/OptionWithValueTest.php @@ -77,7 +77,7 @@ public function testParseShortOption() ); $this->assertSame(['watev', 'bar'], $arguments->toList()); - $this->assertCount(1, $options); + $this->assertSame(1, $options->size()); $this->assertSame('baz', $options->get('foo')->match( static fn($value) => $value, static fn() => null, @@ -97,7 +97,7 @@ public function testParseShortOptionWithValueInNextArgument() ); $this->assertSame(['watev', 'bar'], $arguments->toList()); - $this->assertCount(1, $options); + $this->assertSame(1, $options->size()); $this->assertSame('baz', $options->get('foo')->match( static fn($value) => $value, static fn() => null, @@ -117,7 +117,7 @@ public function testParseShortOptionWithValueInNextArgumentButNoNextValue() ); $this->assertSame(['watev'], $arguments->toList()); - $this->assertCount(1, $options); + $this->assertSame(1, $options->size()); $this->assertSame('', $options->get('foo')->match( static fn($value) => $value, static fn() => null, @@ -137,7 +137,7 @@ public function testParseLongOption() ); $this->assertSame(['watev', 'bar'], $arguments->toList()); - $this->assertCount(1, $options); + $this->assertSame(1, $options->size()); $this->assertSame('baz', $options->get('foo')->match( static fn($value) => $value, static fn() => null, diff --git a/tests/Command/Pattern/OptionalArgumentTest.php b/tests/Command/Pattern/OptionalArgumentTest.php index 302a84c..d9da036 100644 --- a/tests/Command/Pattern/OptionalArgumentTest.php +++ b/tests/Command/Pattern/OptionalArgumentTest.php @@ -79,7 +79,7 @@ public function testParse(): BlackBox\Proof Map::of(), ); - $this->assertCount(1, $parsedArguments); + $this->assertSame(1, $parsedArguments->size()); $this->assertSame($strings[0], $parsedArguments->get('foo')->match( static fn($value) => $value, static fn() => null, @@ -104,7 +104,7 @@ public function testParseWhenNoMoreArguments() Map::of(), ); - $this->assertCount(0, $parsedArguments); + $this->assertSame(0, $parsedArguments->size()); $this->assertNull($parsedArguments->get('foo')->match( static fn($value) => $value, static fn() => null, diff --git a/tests/Command/Pattern/RequiredArgumentTest.php b/tests/Command/Pattern/RequiredArgumentTest.php index 0d84f80..240cca8 100644 --- a/tests/Command/Pattern/RequiredArgumentTest.php +++ b/tests/Command/Pattern/RequiredArgumentTest.php @@ -80,7 +80,7 @@ public function testParse(): BlackBox\Proof Map::of(), ); - $this->assertCount(1, $parsedArguments); + $this->assertSame(1, $parsedArguments->size()); $this->assertSame($strings[0], $parsedArguments->get('foo')->match( static fn($value) => $value, static fn() => null, diff --git a/tests/Command/PatternTest.php b/tests/Command/PatternTest.php index 33e9fd0..4bcfe77 100644 --- a/tests/Command/PatternTest.php +++ b/tests/Command/PatternTest.php @@ -67,6 +67,6 @@ public function testParse() $this->assertSame('first', $arguments->get('foo')); $this->assertSame('second', $arguments->get('bar')); - $this->assertCount(0, $arguments->pack()); + $this->assertSame(0, $arguments->pack()->size()); } } diff --git a/tests/Question/ChoiceQuestionTest.php b/tests/Question/ChoiceQuestionTest.php index 123d6ea..35b0f04 100644 --- a/tests/Question/ChoiceQuestionTest.php +++ b/tests/Question/ChoiceQuestionTest.php @@ -37,7 +37,7 @@ public function testInvoke() ); $this->assertInstanceOf(Map::class, $response); - $this->assertCount(2, $response); + $this->assertSame(2, $response->size()); $this->assertSame('bar', $response->get('foo')->match( static fn($value) => $value, static fn() => null, From 75640581919656a4b987231784de5f4809a995b9 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 23 Nov 2025 17:10:38 +0100 Subject: [PATCH 3/9] remove unused exceptions --- fixtures/thrower.php | 3 +-- src/Exception/EachRowMustBeOfSameSize.php | 8 -------- src/Exception/LogicException.php | 8 -------- tests/MainTest.php | 6 +++--- 4 files changed, 4 insertions(+), 21 deletions(-) delete mode 100644 src/Exception/EachRowMustBeOfSameSize.php delete mode 100644 src/Exception/LogicException.php diff --git a/fixtures/thrower.php b/fixtures/thrower.php index 99ff478..be3515b 100755 --- a/fixtures/thrower.php +++ b/fixtures/thrower.php @@ -7,7 +7,6 @@ use Innmind\CLI\{ Main, Environment, - Exception\LogicException, }; use Innmind\OperatingSystem\OperatingSystem; use Innmind\Immutable\Attempt; @@ -15,6 +14,6 @@ new class extends Main { protected function main(Environment $env, OperatingSystem $os): Attempt { - throw new LogicException('waaat'); + throw new \LogicException('waaat'); } }; diff --git a/src/Exception/EachRowMustBeOfSameSize.php b/src/Exception/EachRowMustBeOfSameSize.php deleted file mode 100644 index 32dc35e..0000000 --- a/src/Exception/EachRowMustBeOfSameSize.php +++ /dev/null @@ -1,8 +0,0 @@ -assertCount(6, $output); $this->assertSame( - "Innmind\CLI\Exception\LogicException(waaat, 0)", + 'LogicException(waaat, 0)', $output[0]->toString(), ); $this->assertSame( - "$cwd/fixtures/thrower.php:18", + "$cwd/fixtures/thrower.php:17", $output[1]->toString(), ); $this->assertSame( @@ -133,7 +133,7 @@ public function testThrow() $output[3]->substring(-28 - \strlen($cwd))->toString(), ); $this->assertSame( - "Innmind\CLI\Main->__construct() at $cwd/fixtures/thrower.php:15", + "Innmind\CLI\Main->__construct() at $cwd/fixtures/thrower.php:14", $output[4]->toString(), ); $this->assertSame( From 65769cee4488e72cab661b19abd32b6e2a1437fa Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 23 Nov 2025 17:29:05 +0100 Subject: [PATCH 4/9] fix generating valid values --- tests/Command/UsageTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Command/UsageTest.php b/tests/Command/UsageTest.php index a82483b..1a74a97 100644 --- a/tests/Command/UsageTest.php +++ b/tests/Command/UsageTest.php @@ -135,6 +135,10 @@ public function testDoesntMatchWhenOwnNameDoesntExplicitlyStartWithSubset(): Bla Set::integers()->between(1, 10), Set::integers()->between(1, 10), ) + ->filter(static fn($name, $start, $shrink) => !\str_starts_with( + $name, + \mb_substr($name, $start, $shrink), + )) ->prove(function($name, $start, $shrink) { $usage = Usage::parse($name); $shrunk = \mb_substr($name, $start, $shrink); From 422b998ac0668f3f3d74727184a9b2efed58a404 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 23 Nov 2025 17:30:29 +0100 Subject: [PATCH 5/9] remove support for unicode in commands name --- CHANGELOG.md | 1 + tests/Command/UsageTest.php | 19 ++----------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba2b67c..b7ed22e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changed - Requires PHP `8.4` +- Commands name only support alphanumerical characters (use unicode at your own risk) ## 4.0.0 - 2025-07-13 diff --git a/tests/Command/UsageTest.php b/tests/Command/UsageTest.php index 1a74a97..257ced4 100644 --- a/tests/Command/UsageTest.php +++ b/tests/Command/UsageTest.php @@ -160,23 +160,8 @@ private function names(): Set return Set::strings() ->madeOf( Set::strings() - ->unicode() - ->char() - ->filter( - static fn($char) => !\in_array( - $char, - [ - ':', - ' ', - "\n", - "\r", - \chr(11), - \chr(0), - "\t", - ], - true, - ), - ), + ->chars() + ->alphanumerical(), ) ->between(1, 10); } From 4a45fde8fb1590430d3714725efe633ebb0f7b48 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 23 Nov 2025 17:50:04 +0100 Subject: [PATCH 6/9] make Environment a final class --- CHANGELOG.md | 1 + src/Environment.php | 110 ++++++++++++++++++-- src/Environment/GlobalEnvironment.php | 10 +- src/Environment/Implementation.php | 68 ++++++++++++ src/Environment/InMemory.php | 51 +++------ src/Main.php | 2 +- tests/CommandsTest.php | 84 ++++++++++----- tests/Environment/GlobalEnvironmentTest.php | 11 +- tests/Question/ChoiceQuestionTest.php | 11 +- tests/Question/QuestionTest.php | 11 +- 10 files changed, 267 insertions(+), 92 deletions(-) create mode 100644 src/Environment/Implementation.php diff --git a/CHANGELOG.md b/CHANGELOG.md index b7ed22e..b679979 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Requires PHP `8.4` - Commands name only support alphanumerical characters (use unicode at your own risk) +- `Innmind\CLI\Environment` is now a final class, all previous implementations are now flagged as internal ## 4.0.0 - 2025-07-13 diff --git a/src/Environment.php b/src/Environment.php index b840b9f..2089f29 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -3,7 +3,13 @@ namespace Innmind\CLI; -use Innmind\CLI\Environment\ExitCode; +use Innmind\CLI\Environment\{ + ExitCode, + Implementation, + GlobalEnvironment, + InMemory, +}; +use Innmind\IO\IO; use Innmind\Url\Path; use Innmind\Immutable\{ Map, @@ -16,48 +22,130 @@ /** * @psalm-immutable */ -interface Environment +final class Environment { + private function __construct( + private Implementation $implementation, + ) { + } + + /** + * @internal + */ + public static function global(IO $io): self + { + return new self(GlobalEnvironment::of($io)); + } + + /** + * @internal + * + * @param list $input + * @param list $arguments + * @param list $variables + */ + public static function inMemory( + array $input, + bool $interactive, + array $arguments, + array $variables, + string $workingDirectory, + ): self { + return new self(InMemory::of( + $input, + $interactive, + $arguments, + $variables, + $workingDirectory, + )); + } + /** * True if the environment running the script is an interactive terminal */ - public function interactive(): bool; + public function interactive(): bool + { + return $this->implementation->interactive(); + } /** * @param ?positive-int $length * * @return array{Attempt, self} */ - public function read(?int $length = null): array; + public function read(?int $length = null): array + { + [$read, $implementation] = $this->implementation->read($length); + + return [$read, new self($implementation)]; + } /** * @return Attempt */ - public function output(Str $data): Attempt; + public function output(Str $data): Attempt + { + return $this + ->implementation + ->output($data) + ->map(static fn($implementation) => new self($implementation)); + } /** * @return Attempt */ - public function error(Str $data): Attempt; + public function error(Str $data): Attempt + { + return $this + ->implementation + ->error($data) + ->map(static fn($implementation) => new self($implementation)); + } /** * @return Sequence */ - public function arguments(): Sequence; + public function arguments(): Sequence + { + return $this->implementation->arguments(); + } /** * @return Map */ - public function variables(): Map; + public function variables(): Map + { + return $this->implementation->variables(); + } /** * @param int<0, 254> $code */ - public function exit(int $code): self; + public function exit(int $code): self + { + return new self($this->implementation->exit($code)); + } /** * @return Maybe */ - public function exitCode(): Maybe; - public function workingDirectory(): Path; + public function exitCode(): Maybe + { + return $this->implementation->exitCode(); + } + + public function workingDirectory(): Path + { + return $this->implementation->workingDirectory(); + } + + /** + * @internal + * + * @return Sequence + */ + public function outputted(): Sequence + { + return $this->implementation->outputted(); + } } diff --git a/src/Environment/GlobalEnvironment.php b/src/Environment/GlobalEnvironment.php index e17db19..c8f1f8e 100644 --- a/src/Environment/GlobalEnvironment.php +++ b/src/Environment/GlobalEnvironment.php @@ -3,7 +3,6 @@ namespace Innmind\CLI\Environment; -use Innmind\CLI\Environment; use Innmind\TimeContinuum\Period; use Innmind\IO\{ IO, @@ -21,8 +20,9 @@ /** * @psalm-immutable + * @internal */ -final class GlobalEnvironment implements Environment +final class GlobalEnvironment implements Implementation { /** * @param Output<'stdout'> $output @@ -180,4 +180,10 @@ public function workingDirectory(): Path { return $this->workingDirectory; } + + #[\Override] + public function outputted(): Sequence + { + return Sequence::of(); + } } diff --git a/src/Environment/Implementation.php b/src/Environment/Implementation.php new file mode 100644 index 0000000..369585d --- /dev/null +++ b/src/Environment/Implementation.php @@ -0,0 +1,68 @@ +, self} + */ + public function read(?int $length = null): array; + + /** + * @return Attempt + */ + public function output(Str $data): Attempt; + + /** + * @return Attempt + */ + public function error(Str $data): Attempt; + + /** + * @return Sequence + */ + public function arguments(): Sequence; + + /** + * @return Map + */ + public function variables(): Map; + + /** + * @param int<0, 254> $code + */ + public function exit(int $code): self; + + /** + * @return Maybe + */ + public function exitCode(): Maybe; + public function workingDirectory(): Path; + + /** + * @return Sequence + */ + public function outputted(): Sequence; +} diff --git a/src/Environment/InMemory.php b/src/Environment/InMemory.php index 467b0ae..fefb21f 100644 --- a/src/Environment/InMemory.php +++ b/src/Environment/InMemory.php @@ -3,7 +3,6 @@ namespace Innmind\CLI\Environment; -use Innmind\CLI\Environment; use Innmind\Url\Path; use Innmind\Immutable\{ Sequence, @@ -16,21 +15,20 @@ /** * Use this implementation for tests only * @psalm-immutable + * @internal */ -final class InMemory implements Environment +final class InMemory implements Implementation { /** * @param Sequence $input - * @param Sequence $output - * @param Sequence $error + * @param Sequence $outputted * @param Sequence $arguments * @param Map $variables * @param Maybe $exitCode */ private function __construct( private Sequence $input, - private Sequence $output, - private Sequence $error, + private Sequence $outputted, private bool $interactive, private Sequence $arguments, private Map $variables, @@ -57,7 +55,6 @@ public static function of( return new self( Sequence::of(...$input)->map(static fn($string) => Str::of($string, Str\Encoding::ascii)), Sequence::of(), - Sequence::of(), $interactive, Sequence::of(...$arguments), Map::of(...$variables), @@ -95,8 +92,7 @@ public function read(?int $length = null): array $data->attempt(static fn() => new \LogicException('No input data specified')), new self( $input, - $this->output, - $this->error, + $this->outputted, $this->interactive, $this->arguments, $this->variables, @@ -111,8 +107,10 @@ public function output(Str $data): Attempt { return Attempt::result(new self( $this->input, - ($this->output)($data->toEncoding(Str\Encoding::ascii)), - $this->error, + ($this->outputted)([ + $data->toEncoding(Str\Encoding::ascii), + 'output', + ]), $this->interactive, $this->arguments, $this->variables, @@ -126,8 +124,10 @@ public function error(Str $data): Attempt { return Attempt::result(new self( $this->input, - $this->output, - ($this->error)($data->toEncoding(Str\Encoding::ascii)), + ($this->outputted)([ + $data->toEncoding(Str\Encoding::ascii), + 'error', + ]), $this->interactive, $this->arguments, $this->variables, @@ -153,8 +153,7 @@ public function exit(int $code): self { return new self( $this->input, - $this->output, - $this->error, + $this->outputted, $this->interactive, $this->arguments, $this->variables, @@ -175,25 +174,9 @@ public function workingDirectory(): Path return $this->workingDirectory; } - /** - * @return list - */ - public function outputs(): array - { - return $this - ->output - ->map(static fn($string) => $string->toString()) - ->toList(); - } - - /** - * @return list - */ - public function errors(): array + #[\Override] + public function outputted(): Sequence { - return $this - ->error - ->map(static fn($string) => $string->toString()) - ->toList(); + return $this->outputted; } } diff --git a/src/Main.php b/src/Main.php index 32e8b92..7568279 100644 --- a/src/Main.php +++ b/src/Main.php @@ -25,7 +25,7 @@ final public function __construct(?Config $config = null) { $config ??= Config::new(); $os = Factory::build($config); - $env = Environment\GlobalEnvironment::of($config->io()); + $env = Environment::global($config->io()); try { $env = $this->main($env, $os)->unwrap(); diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 7afc51a..d5ddb8a 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -55,7 +55,7 @@ public function usage(): Usage return Usage::parse('watch container [output] --foo'); } }); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['bin/console', 'foo', '--foo', 'bar'], @@ -105,7 +105,7 @@ public function usage(): Usage } }, ); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['bin/console', 'watch', 'foo', '--foo', 'bar'], @@ -145,7 +145,7 @@ public function usage(): Usage } }, ); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['bin/console', 'watch', 'foo', '--foo', 'bar'], @@ -185,7 +185,7 @@ public function usage(): Usage } }, ); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['bin/console', 'foo'], @@ -225,7 +225,7 @@ public function usage(): Usage } }, ); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['bin/console', 'w'], @@ -265,7 +265,7 @@ public function usage(): Usage } }, ); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['bin/console', 'f:b:b'], @@ -305,7 +305,7 @@ public function usage(): Usage } }, ); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['bin/console', 'bar'], @@ -324,7 +324,11 @@ public function usage(): Usage " foo \n", " watch \n", ], - $env->errors(), + $env + ->outputted() + ->filter(static fn($pair) => $pair[1] === 'error') + ->map(static fn($pair) => $pair[0]->toString()) + ->toList(), ); } @@ -354,7 +358,7 @@ public function usage(): Usage } }, ); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['bin/console', 'ba'], @@ -373,7 +377,11 @@ public function usage(): Usage " bar \n", " baz \n", ], - $env->errors(), + $env + ->outputted() + ->filter(static fn($pair) => $pair[1] === 'error') + ->map(static fn($pair) => $pair[0]->toString()) + ->toList(), ); } @@ -396,7 +404,7 @@ public function usage(): Usage USAGE); } }); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['bin/console'], @@ -412,7 +420,11 @@ public function usage(): Usage )); $this->assertSame( ['usage: bin/console watch container [output] --foo --help --no-interaction'."\n\nFoo\n\nBar\n"], - $env->errors(), + $env + ->outputted() + ->filter(static fn($pair) => $pair[1] === 'error') + ->map(static fn($pair) => $pair[0]->toString()) + ->toList(), ); } @@ -429,7 +441,7 @@ public function usage(): Usage return Usage::parse('watch container [output] --foo'); } }); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['bin/console', 'foo', '--foo', 'bar'], @@ -461,7 +473,7 @@ public function usage(): Usage USAGE); } }); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['bin/console', '--help'], @@ -477,7 +489,11 @@ public function usage(): Usage )); $this->assertSame( ['usage: bin/console watch container [output] --foo --help --no-interaction'."\n\nFoo\n\nBar\n"], - $env->outputs(), + $env + ->outputted() + ->filter(static fn($pair) => $pair[1] === 'output') + ->map(static fn($pair) => $pair[0]->toString()) + ->toList(), ); } @@ -507,7 +523,7 @@ public function usage(): Usage } }, ); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['bin/console', 'help'], @@ -526,7 +542,11 @@ public function usage(): Usage " foo Description\n", " watch Watch dependency injection\n", ], - $env->outputs(), + $env + ->outputted() + ->filter(static fn($pair) => $pair[1] === 'output') + ->map(static fn($pair) => $pair[0]->toString()) + ->toList(), ); } @@ -556,7 +576,7 @@ public function usage(): Usage } }, ); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['bin/console'], @@ -572,7 +592,11 @@ public function usage(): Usage )); $this->assertSame( [" foo \n", " watch \n"], - $env->errors(), + $env + ->outputted() + ->filter(static fn($pair) => $pair[1] === 'error') + ->map(static fn($pair) => $pair[0]->toString()) + ->toList(), ); } @@ -603,7 +627,7 @@ public function usage(): Usage } }, ); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['bin/console', 'help'], @@ -622,7 +646,11 @@ public function usage(): Usage " foo \n", " watch \n", ], - $env->outputs(), + $env + ->outputted() + ->filter(static fn($pair) => $pair[1] === 'output') + ->map(static fn($pair) => $pair[0]->toString()) + ->toList(), ); } @@ -647,7 +675,7 @@ public function usage(): Usage } }, ); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['bin/console', '--help'], @@ -670,7 +698,11 @@ public function usage(): Usage USAGE, ], - $env->outputs(), + $env + ->outputted() + ->filter(static fn($pair) => $pair[1] === 'output') + ->map(static fn($pair) => $pair[0]->toString()) + ->toList(), ); } @@ -713,7 +745,7 @@ public function usage(): Usage } }; })); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['bin/console', 'watch', 'foo', '--foo', 'bar'], @@ -784,7 +816,7 @@ public function usage(): Usage } }; })); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['bin/console', 'ba'], @@ -856,7 +888,7 @@ public function usage(): Usage } }; })); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['bin/console', 'unknown'], diff --git a/tests/Environment/GlobalEnvironmentTest.php b/tests/Environment/GlobalEnvironmentTest.php index 108bc66..7b4b71b 100644 --- a/tests/Environment/GlobalEnvironmentTest.php +++ b/tests/Environment/GlobalEnvironmentTest.php @@ -3,10 +3,7 @@ namespace Tests\Innmind\CLI\Environment; -use Innmind\CLI\{ - Environment\GlobalEnvironment, - Environment, -}; +use Innmind\CLI\Environment\GlobalEnvironment; use Innmind\IO\IO; use Innmind\Url\Path; use Innmind\Immutable\{ @@ -25,11 +22,6 @@ public function setUp(): void $this->env = GlobalEnvironment::of(IO::fromAmbientAuthority()); } - public function testInterface() - { - $this->assertInstanceOf(Environment::class, $this->env); - } - public function testInteractive() { // can't prove via a test that the env can be interactive as tests are @@ -63,7 +55,6 @@ public function testExitCode() { $this->assertEquals(Maybe::nothing(), $this->env->exitCode()); $env = $this->env->exit(1); - $this->assertInstanceOf(Environment::class, $env); $this->assertSame(1, $env->exitCode()->match( static fn($code) => $code->toInt(), static fn() => null, diff --git a/tests/Question/ChoiceQuestionTest.php b/tests/Question/ChoiceQuestionTest.php index 35b0f04..75197db 100644 --- a/tests/Question/ChoiceQuestionTest.php +++ b/tests/Question/ChoiceQuestionTest.php @@ -22,7 +22,7 @@ public function testInvoke() (2, 3) ('bar', 3), ); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [' foo, ', "2\n"], true, [], @@ -55,7 +55,10 @@ public function testInvoke() "[bar] 3\n", '> ', ], - $env->outputs(), + $env + ->outputted() + ->map(static fn($pair) => $pair[0]->toString()) + ->toList(), ); } @@ -63,7 +66,7 @@ public function testReturnNothingWhenEnvNonInteractive() { $question = ChoiceQuestion::of('watev', Map::of()); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], false, [], @@ -83,7 +86,7 @@ public function testReturnNothingWhenOptionToSpecifyNoInteractionIsRequired() { $question = ChoiceQuestion::of('watev', Map::of()); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['foo', '--no-interaction', 'bar'], diff --git a/tests/Question/QuestionTest.php b/tests/Question/QuestionTest.php index 9f0bd1b..f968ea1 100644 --- a/tests/Question/QuestionTest.php +++ b/tests/Question/QuestionTest.php @@ -15,7 +15,7 @@ class QuestionTest extends TestCase public function testInvoke() { $question = Question::of('message'); - $env = Environment\InMemory::of( + $env = Environment::inMemory( ['f', "oo\n"], true, [], @@ -33,7 +33,10 @@ public function testInvoke() $this->assertSame('foo', $response->toString()); $this->assertSame( ['message '], - $env->outputs(), + $env + ->outputted() + ->map(static fn($pair) => $pair[0]->toString()) + ->toList(), ); } @@ -41,7 +44,7 @@ public function testReturnNothingWhenEnvNonInteractive() { $question = Question::of('watev'); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], false, [], @@ -61,7 +64,7 @@ public function testReturnNothingWhenOptionToSpecifyNoInteractionIsRequired() { $question = Question::of('watev'); - $env = Environment\InMemory::of( + $env = Environment::inMemory( [], true, ['foo', '--no-interaction', 'bar'], From 3208873111bcc0376019a38bd06889c841dcf8bb Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 23 Nov 2025 17:50:19 +0100 Subject: [PATCH 7/9] fix generating valid scenarii --- tests/Command/UsageTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Command/UsageTest.php b/tests/Command/UsageTest.php index 257ced4..1f36da0 100644 --- a/tests/Command/UsageTest.php +++ b/tests/Command/UsageTest.php @@ -55,6 +55,7 @@ public function testMatchesItsOwnName(): BlackBox\Proof $this->names(), ) ->filter(static fn($a, $b) => $a !== $b) + ->filter(static fn($a, $b) => !\str_starts_with($a, $b)) ->prove(function($a, $b) { $usage = Usage::parse($a); From 136b2b0f7f984ac427e8824e84bb9d7cdf3d4b61 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Feb 2026 11:46:30 +0100 Subject: [PATCH 8/9] tag dependencies --- .github/workflows/ci.yml | 8 ++++---- CHANGELOG.md | 1 + composer.json | 28 +++++++-------------------- src/Command/Usage.php | 4 ++-- src/Environment/GlobalEnvironment.php | 2 +- tests/Command/Pattern/InputsTest.php | 2 +- tests/CommandsTest.php | 2 +- tests/MainTest.php | 6 +++--- 8 files changed, 20 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 779f162..2f3eecb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,11 +4,11 @@ on: [push, pull_request] jobs: blackbox: - uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@next + uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@main coverage: - uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@next + uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@main secrets: inherit psalm: - uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@next + uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@main cs: - uses: innmind/github-workflows/.github/workflows/cs.yml@next + uses: innmind/github-workflows/.github/workflows/cs.yml@main diff --git a/CHANGELOG.md b/CHANGELOG.md index b679979..8aba27e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Requires PHP `8.4` - Commands name only support alphanumerical characters (use unicode at your own risk) - `Innmind\CLI\Environment` is now a final class, all previous implementations are now flagged as internal +- Requires `innmind/operating-system:~7.0` ## 4.0.0 - 2025-07-13 diff --git a/composer.json b/composer.json index 1a231d7..07ca13b 100644 --- a/composer.json +++ b/composer.json @@ -16,26 +16,12 @@ }, "require": { "php": "~8.4", - "innmind/io": "dev-next", - "innmind/immutable": "dev-next", - "innmind/url": "dev-next", - "innmind/operating-system": "dev-next", - "innmind/stack-trace": "dev-next", - "innmind/validation": "dev-next", - "innmind/server-status": "dev-next", - "innmind/http-transport": "dev-next", - "innmind/file-watch": "dev-next", - "innmind/signals": "dev-next", - "innmind/http": "dev-next", - "innmind/server-control": "dev-next", - "innmind/filesystem": "dev-next", - "innmind/time-warp": "dev-next", - "innmind/media-type": "dev-next", - "innmind/ip": "dev-next", - "innmind/time-continuum": "dev-next", - "innmind/graphviz": "dev-next", - "innmind/colour": "dev-next", - "formal/access-layer": "dev-next" + "innmind/io": "~4.0", + "innmind/immutable": "~6.0", + "innmind/url": "~5.0", + "innmind/operating-system": "~7.0", + "innmind/stack-trace": "~5.0", + "innmind/validation": "~3.0" }, "autoload": { "psr-4": { @@ -48,7 +34,7 @@ } }, "require-dev": { - "innmind/static-analysis": "^1.2.1", + "innmind/static-analysis": "~1.3", "innmind/black-box": "^6.4.1", "innmind/coding-standard": "~2.0" } diff --git a/src/Command/Usage.php b/src/Command/Usage.php index fb61bb7..84b7483 100644 --- a/src/Command/Usage.php +++ b/src/Command/Usage.php @@ -358,7 +358,7 @@ public function toString(): string ->arguments ->map(static fn($argument) => ' '.$argument->toString()) ->map(Str::of(...)) - ->fold(new Concat) + ->fold(Concat::monoid) ->toString(); if ($this->pack->unwrap()) { @@ -369,7 +369,7 @@ public function toString(): string ->options ->map(static fn($argument) => ' '.$argument->toString()) ->map(Str::of(...)) - ->fold(new Concat) + ->fold(Concat::monoid) ->toString(); $string .= ' --help --no-interaction'; diff --git a/src/Environment/GlobalEnvironment.php b/src/Environment/GlobalEnvironment.php index c8f1f8e..8c55ff2 100644 --- a/src/Environment/GlobalEnvironment.php +++ b/src/Environment/GlobalEnvironment.php @@ -3,7 +3,7 @@ namespace Innmind\CLI\Environment; -use Innmind\TimeContinuum\Period; +use Innmind\Time\Period; use Innmind\IO\{ IO, Streams\Stream\Read, diff --git a/tests/Command/Pattern/InputsTest.php b/tests/Command/Pattern/InputsTest.php index 2511fdc..66eb447 100644 --- a/tests/Command/Pattern/InputsTest.php +++ b/tests/Command/Pattern/InputsTest.php @@ -18,6 +18,6 @@ public function testThrowWhenPatternNotRecognized() $this->expectException(PatternNotRecognized::class); $this->expectExceptionMessage('_foo_'); - (new Inputs)(Usage::of('name'), Str::of('_foo_'))->unwrap(); + $_ = (new Inputs)(Usage::of('name'), Str::of('_foo_'))->unwrap(); } } diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index d5ddb8a..bb9afba 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -451,7 +451,7 @@ public function usage(): Usage $this->expectException(\Exception::class); - $run($env)->unwrap(); + $_ = $run($env)->unwrap(); } public function testDisplayUsageWhenHelpOptionFound() diff --git a/tests/MainTest.php b/tests/MainTest.php index 05a81f8..842b4ff 100644 --- a/tests/MainTest.php +++ b/tests/MainTest.php @@ -72,7 +72,7 @@ public function testEcho() $process ->output() ->map(static fn($chunk) => $chunk->data()) - ->fold(new Concat) + ->fold(Concat::monoid) ->toString(), ); } @@ -88,7 +88,7 @@ public function testThrow() ->withEnvironment('PATH', $_SERVER['PATH']), ) ->unwrap(); - $process->output()->foreach(function($chunk): void { + $_ = $process->output()->foreach(function($chunk): void { $this->assertSame(Type::error, $chunk->type()); }); @@ -97,7 +97,7 @@ public function testThrow() $process ->output() ->map(static fn($chunk) => $chunk->data()) - ->fold(new Concat) + ->fold(Concat::monoid) ->toString(), ) ->split("\n") From ff14ea8e3b9386ca088805ed6c259b0f2968733f Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 8 Feb 2026 11:47:45 +0100 Subject: [PATCH 9/9] add extensive CI --- .github/workflows/extensive.yml | 12 ++++++++++++ blackbox.php | 4 ++++ 2 files changed, 16 insertions(+) create mode 100644 .github/workflows/extensive.yml diff --git a/.github/workflows/extensive.yml b/.github/workflows/extensive.yml new file mode 100644 index 0000000..257f139 --- /dev/null +++ b/.github/workflows/extensive.yml @@ -0,0 +1,12 @@ +name: Extensive CI + +on: + push: + tags: + - '*' + paths: + - '.github/workflows/extensive.yml' + +jobs: + blackbox: + uses: innmind/github-workflows/.github/workflows/extensive.yml@main diff --git a/blackbox.php b/blackbox.php index 5c3ddce..486f49b 100644 --- a/blackbox.php +++ b/blackbox.php @@ -10,6 +10,10 @@ }; Application::new($argv) + ->when( + \getenv('BLACKBOX_SET_SIZE') !== false, + static fn(Application $app) => $app->scenariiPerProof((int) \getenv('BLACKBOX_SET_SIZE')), + ) ->disableMemoryLimit() ->when( \getenv('ENABLE_COVERAGE') !== false,