From 34126fedf894d84effd0742d3a424e0c35d26c73 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Sat, 13 Jun 2026 04:01:45 +0300 Subject: [PATCH 1/2] feat(runner): ANSI colors on by default with TTY auto-detection - Add shouldUseAnsi() static method with TTY detection via posix_isatty - Add --no-color global argument (respects no-color.org convention) - Add isAnsi() public method to Runner - Resolution precedence: --no-color > NO_COLOR env > --ansi > TTY detect - Command println/prints now use Runner's resolved ANSI value - Graceful Windows fallback via ANSICON/ConEmuANSI/TERM env checks - ANSI only auto-enables when output stream is StdOut (not in tests) Closes #50 --- WebFiori/Cli/Command.php | 22 +- WebFiori/Cli/Runner.php | 113 +- .../WebFiori/Tests/Cli/AnsiAutoDetectTest.php | 329 +++ tests/WebFiori/Tests/Cli/RunnerTest.php | 2201 +++++++++-------- 4 files changed, 1546 insertions(+), 1119 deletions(-) create mode 100644 tests/WebFiori/Tests/Cli/AnsiAutoDetectTest.php diff --git a/WebFiori/Cli/Command.php b/WebFiori/Cli/Command.php index 59fd0c9..882bc8e 100644 --- a/WebFiori/Cli/Command.php +++ b/WebFiori/Cli/Command.php @@ -876,7 +876,7 @@ public function println(string $str = '', ...$_) { if ($argsCount != 0 && gettype($_[$argsCount - 1]) == 'array') { //Last index contains formatting options. - $_[$argsCount - 1]['ansi'] = $this->isArgProvided('--ansi'); + $_[$argsCount - 1]['ansi'] = $this->isAnsiEnabled(); $str = Formatter::format($str, $_[$argsCount - 1]); } call_user_func_array([$this->getOutputStream(), 'println'], $this->_createPassArray($str, $_)); @@ -906,7 +906,7 @@ public function prints(string $str, ...$_): void { $formattingOptions = $_[$argCount - 1]; } - $formattingOptions['ansi'] = $this->isArgProvided('--ansi'); + $formattingOptions['ansi'] = $this->isAnsiEnabled(); $formattedStr = Formatter::format($str, $formattingOptions); @@ -1602,6 +1602,24 @@ private function getTerminalWidth(): int { // Default fallback return 80; } + + /** + * Checks if ANSI output is enabled for this command. + * + * Uses the Runner's resolved ANSI value if available, falls back to + * checking if --ansi argument is provided. + * + * @return bool True if ANSI output should be used. + */ + private function isAnsiEnabled(): bool { + $owner = $this->getOwner(); + + if ($owner !== null) { + return $owner->isAnsi(); + } + + return $this->isArgProvided('--ansi'); + } private function parseArgsHelper() : bool { $options = $this->getArgs(); $invalidArgsVals = []; diff --git a/WebFiori/Cli/Runner.php b/WebFiori/Cli/Runner.php index 52275a3..e566542 100644 --- a/WebFiori/Cli/Runner.php +++ b/WebFiori/Cli/Runner.php @@ -142,11 +142,16 @@ public function __construct() { ArgumentOption::OPTIONAL => true, ArgumentOption::DESCRIPTION => 'Force the use of ANSI output.' ]); + $this->addArg('--no-color', [ + ArgumentOption::OPTIONAL => true, + ArgumentOption::DESCRIPTION => 'Disable ANSI colored output.' + ]); $this->setBeforeStart(function (Runner $r) { if (count($r->getArgsVector()) == 0) { $r->setArgsVector($_SERVER['argv']); } $r->checkIsInteractive(); + $r->resolveAnsi(); }); $this->register(new HelpCommand(), ['-h']); $this->setDefaultCommand('help'); @@ -610,6 +615,18 @@ public function hasArg(string $name): bool { return false; } + /** + * Returns whether ANSI output is currently enabled. + * + * If not explicitly forced via --ansi or --no-color, this checks whether + * the output stream is a real terminal (StdOut) and applies TTY detection. + * + * @return bool True if ANSI output is enabled, false otherwise. + */ + public function isAnsi(): bool { + return $this->isAnsi; + } + /** * Check if auto-discovery is enabled. * @@ -796,8 +813,12 @@ public function runCommand(?Command $c = null, array $args = [], bool $ansi = fa } } - if ($ansi) { - $args[] = '--ansi'; + if ($ansi || in_array('--ansi', $args)) { + $this->isAnsi = true; + } + + if (in_array('--no-color', $args)) { + $this->isAnsi = false; } $this->setArgV($args); $this->setActiveCommand($c); @@ -1074,6 +1095,35 @@ public function setSignalHandler(int $signal, callable $handler): Runner { return $this; } + /** + * Determines if ANSI output should be used based on environment detection. + * + * Resolution precedence: + * 1. NO_COLOR env variable → false + * 2. posix_isatty(STDOUT) on Unix → true if TTY + * 3. Windows terminal env checks (ANSICON, ConEmuANSI, TERM) + * 4. Default → false + * + * @return bool True if ANSI should be enabled by default. + */ + public static function shouldUseAnsi(): bool { + if (getenv('NO_COLOR') !== false || isset($_SERVER['NO_COLOR'])) { + return false; + } + + if (function_exists('posix_isatty')) { + return defined('STDOUT') ? posix_isatty(STDOUT) : false; + } + + if (DIRECTORY_SEPARATOR === '\\') { + return getenv('ANSICON') !== false + || getenv('ConEmuANSI') === 'ON' + || getenv('TERM') === 'xterm'; + } + + return false; + } + /** * Start command line process. * @@ -1087,7 +1137,11 @@ public function start(): int { } if ($this->isInteractive()) { - $this->isAnsi = in_array('--ansi', $this->getArgsVector()); + if (in_array('--no-color', $this->getArgsVector())) { + $this->isAnsi = false; + } elseif (in_array('--ansi', $this->getArgsVector())) { + $this->isAnsi = true; + } $this->printMsg('Running in interactive mode.', '>>', 'blue'); $this->printMsg("Type command name or 'exit' to close.", ">>", 'blue'); $this->printMsg('', '>>', 'blue'); @@ -1126,6 +1180,7 @@ private function invokeAfterExc(): void { call_user_func_array($funcArr['func'], array_merge([$this], $funcArr['params'])); } } + /** * Preprocesses arguments to handle help patterns like 'command help' or 'command -h'. * @@ -1173,9 +1228,7 @@ private function readInteractive(): array { $argsArr = strlen($input) != 0 ? explode(' ', $input) : []; - if (in_array('--ansi', $argsArr)) { - $argsArr = array_diff($argsArr, ['--ansi']); - } + $argsArr = $this->removeAnsiArgs($argsArr); // Preprocess help patterns $argsArr = $this->preprocessHelpPattern($argsArr); @@ -1227,6 +1280,30 @@ private function registerCommandSignalHandlers(Command $c): void { } } } + /** + * Removes --ansi and --no-color flags from an arguments array. + * + * @param array $argsArr The arguments array. + * + * @return array The filtered arguments array. + */ + private function removeAnsiArgs(array $argsArr): array { + $tempArgs = []; + + foreach ($argsArr as $argName => $val) { + if (gettype($argName) == 'integer') { + if ($val != '--ansi' && $val != '--no-color') { + $tempArgs[] = $val; + } + } else { + if ($argName != '--ansi' && $argName != '--no-color') { + $tempArgs[$argName] = $val; + } + } + } + + return $tempArgs; + } private function removeCommandSignalHandlers(Command $c): void { if ($this->signalHandler !== null) { @@ -1237,6 +1314,12 @@ private function removeCommandSignalHandlers(Command $c): void { } } + private function resolveAnsi(): void { + if ($this->outputStream instanceof StdOut) { + $this->isAnsi = self::shouldUseAnsi(); + } + } + /** * Run the command line as single run. * @@ -1245,22 +1328,14 @@ private function removeCommandSignalHandlers(Command $c): void { private function run(): int { $argsArr = array_slice($this->getArgsVector(), 1); - if (in_array('--ansi', $argsArr)) { + if (in_array('--no-color', $argsArr)) { + $this->isAnsi = false; + } elseif (in_array('--ansi', $argsArr)) { $this->isAnsi = true; - $tempArgs = []; - - foreach ($argsArr as $argName => $val) { - if (gettype($argName) == 'integer') { - if ($val != '--ansi') { - $tempArgs[] = $val; - } - } else { - $tempArgs[$argName] = $val; - } - } - $argsArr = $tempArgs; } + $argsArr = $this->removeAnsiArgs($argsArr); + // Preprocess help patterns for non-interactive mode $argsArr = $this->preprocessHelpPattern($argsArr); diff --git a/tests/WebFiori/Tests/Cli/AnsiAutoDetectTest.php b/tests/WebFiori/Tests/Cli/AnsiAutoDetectTest.php new file mode 100644 index 0000000..c912c1a --- /dev/null +++ b/tests/WebFiori/Tests/Cli/AnsiAutoDetectTest.php @@ -0,0 +1,329 @@ +println('Hello', ['color' => 'red']); + + return 0; + } +} + +class AnsiAutoDetectTest extends CommandTestCase { + /** + * @test + */ + public function testShouldUseAnsiReturnsBoolean() { + $result = Runner::shouldUseAnsi(); + $this->assertIsBool($result); + } + + /** + * @test + */ + public function testShouldUseAnsiRespectsNoColorEnv() { + putenv('NO_COLOR=1'); + $this->assertFalse(Runner::shouldUseAnsi()); + putenv('NO_COLOR'); + } + + /** + * @test + */ + public function testShouldUseAnsiRespectsNoColorServer() { + $_SERVER['NO_COLOR'] = '1'; + $this->assertFalse(Runner::shouldUseAnsi()); + unset($_SERVER['NO_COLOR']); + } + + /** + * @test + */ + public function testIsAnsiDefaultFalseWithArrayOutputStream() { + $runner = new Runner(); + $runner->reset(); + $runner->setInputs([]); + + // With ArrayOutputStream, isAnsi should be false + $this->assertFalse($runner->isAnsi()); + } + + /** + * @test + */ + public function testIsAnsiTrueWhenForcedViaFlag() { + $runner = new Runner(); + $runner->reset(); + $runner->register(new AnsiAutoDetectCommand()); + $runner->setInputs([]); + $runner->runCommand(null, ['ansi-test', '--ansi']); + + $this->assertTrue($runner->isAnsi()); + } + + /** + * @test + */ + public function testIsAnsiFalseWhenNoColorFlag() { + $runner = new Runner(); + $runner->reset(); + $runner->register(new AnsiAutoDetectCommand()); + $runner->setInputs([]); + $runner->runCommand(null, ['ansi-test', '--no-color']); + + $this->assertFalse($runner->isAnsi()); + } + + /** + * @test + */ + public function testNoColorOverridesAnsi() { + // --no-color should take precedence when both are present + $runner = new Runner(); + $runner->reset(); + $runner->register(new AnsiAutoDetectCommand()); + $runner->setInputs([]); + $runner->runCommand(null, ['ansi-test', '--ansi', '--no-color']); + + $this->assertFalse($runner->isAnsi()); + } + + /** + * @test + */ + public function testOutputHasNoAnsiWhenNoColor() { + $output = $this->executeSingleCommand( + new AnsiAutoDetectCommand(), + ['--no-color'] + ); + + // Output should NOT contain ANSI escape sequences + foreach ($output as $line) { + $this->assertStringNotContainsString("\e[", $line); + } + } + + /** + * @test + */ + public function testOutputHasAnsiWhenForced() { + $output = $this->executeSingleCommand( + new AnsiAutoDetectCommand(), + ['--ansi'] + ); + + // Output should contain ANSI escape sequences + $hasAnsi = false; + + foreach ($output as $line) { + if (strpos($line, "\e[") !== false) { + $hasAnsi = true; + + break; + } + } + $this->assertTrue($hasAnsi, 'Expected ANSI codes in output when --ansi is forced'); + } + + /** + * @test + */ + public function testDefaultOutputNoAnsiInTestMode() { + // Without --ansi, in test mode (ArrayOutputStream), no ANSI + $output = $this->executeSingleCommand(new AnsiAutoDetectCommand()); + + foreach ($output as $line) { + $this->assertStringNotContainsString("\e[", $line); + } + } + + /** + * @test + */ + public function testInteractiveModeNoColorFlag() { + $runner = new Runner(); + $runner->reset(); + $runner->register(new AnsiAutoDetectCommand()); + $runner->setArgsVector(['main.php', '-i', '--no-color']); + $runner->setInputs(['ansi-test', 'exit']); + $runner->start(); + + $output = $runner->getOutput(); + + foreach ($output as $line) { + $this->assertStringNotContainsString("\e[", $line); + } + } + + /** + * @test + */ + public function testInteractiveModeAnsiFlag() { + $runner = new Runner(); + $runner->reset(); + $runner->register(new AnsiAutoDetectCommand()); + $runner->setArgsVector(['main.php', '-i', '--ansi']); + $runner->setInputs(['ansi-test', 'exit']); + $runner->start(); + + $output = $runner->getOutput(); + $hasAnsi = false; + + foreach ($output as $line) { + if (strpos($line, "\e[") !== false) { + $hasAnsi = true; + + break; + } + } + $this->assertTrue($hasAnsi, 'Expected ANSI codes in interactive mode with --ansi'); + } + + /** + * @test + */ + public function testRunMethodHandlesNoColor() { + $runner = new Runner(); + $runner->reset(); + $runner->register(new AnsiAutoDetectCommand()); + $runner->setInputs([]); + $runner->setArgsVector(['main.php', 'ansi-test', '--no-color']); + $runner->start(); + + $output = $runner->getOutput(); + + foreach ($output as $line) { + $this->assertStringNotContainsString("\e[", $line); + } + } + + /** + * @test + */ + public function testRunMethodHandlesAnsiForce() { + $runner = new Runner(); + $runner->reset(); + $runner->register(new AnsiAutoDetectCommand()); + $runner->setInputs([]); + $runner->setArgsVector(['main.php', 'ansi-test', '--ansi']); + $runner->start(); + + $output = $runner->getOutput(); + $hasAnsi = false; + + foreach ($output as $line) { + if (strpos($line, "\e[") !== false) { + $hasAnsi = true; + + break; + } + } + $this->assertTrue($hasAnsi); + } + + /** + * @test + */ + public function testResolveAnsiOnlyAppliesWithStdOut() { + $runner = new Runner(); + $runner->reset(); + + // With StdOut, resolveAnsi should apply TTY detection + $runner->setOutputStream(new StdOut()); + // Can't directly call resolveAnsi (private), but we can check behavior + // After start() with StdOut, isAnsi depends on actual TTY + // We just verify it doesn't crash + $this->assertIsBool($runner->isAnsi()); + } + + /** + * @test + */ + public function testNoColorEnvDisablesAnsi() { + putenv('NO_COLOR=1'); + + $runner = new Runner(); + $runner->reset(); + $runner->setOutputStream(new StdOut()); + $runner->register(new AnsiAutoDetectCommand()); + $runner->setInputs([]); + $runner->setArgsVector(['main.php', 'ansi-test']); + $runner->start(); + + // Even with StdOut, NO_COLOR env should disable ANSI + $output = $runner->getOutput(); + + foreach ($output as $line) { + $this->assertStringNotContainsString("\e[", $line); + } + + putenv('NO_COLOR'); + } + + /** + * @test + */ + public function testCommandIsAnsiEnabledUsesRunner() { + $runner = new Runner(); + $runner->reset(); + + $ansiDetected = false; + $command = new class extends Command { + public $ansiValue = null; + + public function __construct() { + parent::__construct('check-ansi', [], 'Check ANSI state'); + } + + public function exec(): int { + // Access owner's isAnsi through println behavior + $this->println('test', ['color' => 'red']); + + return 0; + } + }; + + $runner->register($command); + $runner->setInputs([]); + $runner->runCommand(null, ['check-ansi', '--ansi']); + + $output = $runner->getOutput(); + $hasAnsi = false; + + foreach ($output as $line) { + if (strpos($line, "\e[") !== false) { + $hasAnsi = true; + + break; + } + } + $this->assertTrue($hasAnsi); + } + + /** + * @test + */ + public function testCommandWithoutOwnerFallsBackToArgCheck() { + // Command used standalone without Runner + $command = new AnsiAutoDetectCommand(); + $command->setOutputStream(new ArrayOutputStream()); + + // Without owner, isAnsiEnabled falls back to isArgProvided + // which checks the command's own args + $command->excCommand(); + // No crash = success. Output won't have ANSI since no --ansi arg. + $this->assertTrue(true); + } +} diff --git a/tests/WebFiori/Tests/Cli/RunnerTest.php b/tests/WebFiori/Tests/Cli/RunnerTest.php index 8297d32..782033a 100644 --- a/tests/WebFiori/Tests/Cli/RunnerTest.php +++ b/tests/WebFiori/Tests/Cli/RunnerTest.php @@ -1,1098 +1,1103 @@ -reset(); - $this->assertTrue($runner->getOutputStream() instanceof StdOut); - $this->assertTrue($runner->getInputStream() instanceof StdIn); - $runner->setInputStream(new ArrayInputStream()); - $runner->setOutputStream(new ArrayOutputStream()); - $this->assertFalse($runner->getOutputStream() instanceof StdOut); - $this->assertFalse($runner->getInputStream() instanceof StdIn); - $this->assertTrue($runner->getInputStream() instanceof ArrayInputStream); - $this->assertTrue($runner->getOutputStream() instanceof ArrayOutputStream); - } - public function testIsCLI() { - $this->assertTrue(Runner::isCLI()); - } - /** - * @test - */ - public function testRunner00() { - $runner = new Runner(); - $this->assertEquals([], $runner->getOutput()); - // Help command is automatically registered - $this->assertEquals(['help'], array_keys($runner->getCommands())); - $this->assertFalse($runner->addArg(' ')); - $this->assertFalse($runner->addArg(' invalid name ')); - $this->assertInstanceOf(\WebFiori\Cli\Commands\HelpCommand::class, $runner->getDefaultCommand()); - $this->assertNull($runner->getActiveCommand()); - - $argObj = new Argument('--ansi'); - $this->assertFalse($runner->addArgument($argObj)); - - $this->assertTrue($runner->addArg('global-arg', [ - ArgumentOption::OPTIONAL => true - ])); - $this->assertEquals(2, count($runner->getArgs())); - $runner->removeArgument('--ansi'); - $this->assertEquals(1, count($runner->getArgs())); - $this->assertFalse($runner->hasArg('--ansi')); - $runner->register(new Command00()); - $this->assertEquals(2, count($runner->getCommands())); // help + super-hero - $runner->register(new Command00()); - $this->assertEquals(2, count($runner->getCommands())); // Still 2, no duplicates - $runner->setDefaultCommand('super-hero'); - $runner->setInputs([]); - $this->assertEquals(0, $runner->runCommand(null, [ - 'name' => 'Ibrahim' - ])); - $this->assertEquals([ - "Hello hero Ibrahim\n" - ], $runner->getOutput()); - } - /** - * @test - */ - public function testRunner01() { - $runner = new Runner(); - $this->assertEquals(0, $runner->getLastCommandExitStatus()); - $runner->setDefaultCommand('super-hero'); - // Since 'super-hero' is not registered, default remains the help command - $this->assertInstanceOf(\WebFiori\Cli\Commands\HelpCommand::class, $runner->getDefaultCommand()); - $runner->setInputs([]); - $this->assertEquals(-1, $runner->runCommand(null, [ - 'do-it', - '--ansi' - ])); - $this->assertEquals(-1, $runner->getLastCommandExitStatus()); - $this->assertEquals([ - "Error: The command 'do-it' is not supported.\n" - ], $runner->getOutput()); - } - /** - * @test - */ - public function testRunner02() { - $runner = new Runner(); - $runner->setDefaultCommand('super-hero'); - // Since 'super-hero' is not registered, default remains the help command - $this->assertInstanceOf(\WebFiori\Cli\Commands\HelpCommand::class, $runner->getDefaultCommand()); - $runner->setInputs([]); - $this->assertEquals(0, $runner->runCommand()); - $this->assertEquals(0, $runner->getLastCommandExitStatus()); - // Since default command is help, it will show help output instead of "No command" message - $output = $runner->getOutput(); - $this->assertNotEmpty($output); - $this->assertStringContainsString('Usage:', $output[0]); - } - /** - * @test - */ - public function testRunner03() { - $this->assertEquals([ - "Error: The following argument(s) have invalid values: 'name'\n", - "Info: Allowed values for the argument 'name':\n", - "Ibrahim\n", - "Ali\n" - ], $this->executeSingleCommand(new Command00(), [ - 'super-hero', - 'name' => 'Ok' - ])); - $this->assertEquals(-1, $this->getExitCode()); - } - /** - * @test - */ - public function testRunner04() { - $this->assertEquals([ - "\e[1;91mError: \e[0mThe following argument(s) have invalid values: 'name'\n", - "\e[1;34mInfo: \e[0mAllowed values for the argument 'name':\n", - "Ibrahim\n", - "Ali\n" - ], $this->executeSingleCommand(new Command00(), [ - 'name' => 'Ok', - '--ansi' - ])); - $this->assertEquals(-1, $this->getExitCode()); - } - /** - * @test - */ - public function testRunner05() { - $runner = new Runner(); - $runner->register(new Command00()); - // Don't register HelpCommand again - it's already automatically registered - $runner->removeArgument('--ansi'); - $runner->setDefaultCommand('help'); - $runner->setInputs([]); - $this->assertEquals(0, $runner->runCommand(null, [])); - $this->assertEquals(0, $runner->getLastCommandExitStatus()); - $this->assertEquals([ - "Usage:\n", - " command [arg1 arg2=\"val\" arg3...]\n\n", - "Available Commands:\n", - " help: Display CLI Help. To display help for specific command, use the argument \"--command\" with this command.\n", - " super-hero: A command to display hero's name.\n" - ], $runner->getOutput()); - } - /** - * @test - */ - public function testRunner06() { - - $this->assertEquals([ - "Usage:\n", - " command [arg1 arg2=\"val\" arg3...]\n\n", - "Global Arguments:\n", - " --ansi:[Optional] Force the use of ANSI output.\n", - "Available Commands:\n", - " help: Display CLI Help. To display help for specific command, use the argument \"--command\" with this command.\n", - " super-hero: A command to display hero's name.\n" - ], $this->executeMultiCommand([], [], [ - new Command00() - // Don't register HelpCommand - it's automatically registered - ], 'help')); - $this->assertEquals(0, $this->getExitCode()); - } - /** - * @test - */ - public function testRunner07() { - $runner = new Runner(); - $runner->register(new Command00()); - $runner->setDefaultCommand('help'); - $runner->setInputs([]); - $this->assertEquals(0, $runner->runCommand(new HelpCommand(), [ - '--ansi' - ])); - $this->assertEquals(0, $runner->getLastCommandExitStatus()); - $this->assertEquals([ - "\e[1;93mUsage:\e[0m\n", - " command [arg1 arg2=\"val\" arg3...]\n\n", - "\e[1;93mGlobal Arguments:\e[0m\n", - "\e[1;33m --ansi:\e[0m[Optional] Force the use of ANSI output.\n", - "\e[1;93mAvailable Commands:\e[0m\n", - "\e[1;33m help\e[0m: Display CLI Help. To display help for specific command, use the argument \"--command\" with this command.\n", - "\e[1;33m super-hero\e[0m: A command to display hero's name.\n" - ], $runner->getOutput()); - } - /** - * @test - */ - public function testRunner08() { - $runner = new Runner(); - $runner->register(new Command00()); - $runner->setInputs([]); - $this->assertEquals(0, $runner->runCommand(new HelpCommand(), [ - '--ansi', - '--command' => 'super-hero' - ])); - $this->assertEquals([ - "\e[1;33m super-hero\e[0m: A command to display hero's name.\n", - "\e[1;94m Supported Arguments:\e[0m\n", - "\e[1;33m name:\e[0m The name of the hero\n", - ], $runner->getOutput()); - } - /** - * @test - */ - public function testRunner09() { - $_SERVER['argv'] = []; - $runner = new Runner(); - $runner->removeArgument('--ansi'); - $runner->register(new Command00()); - // Don't register HelpCommand - it's automatically registered - $runner->setDefaultCommand('help'); - $runner->setInputs([]); - $runner->start(); - $this->assertEquals([ - "Usage:\n", - " command [arg1 arg2=\"val\" arg3...]\n\n", - "Available Commands:\n", - " help: Display CLI Help. To display help for specific command, use the argument \"--command\" with this command.\n", - " super-hero: A command to display hero's name.\n" - ], $runner->getOutput()); - $this->assertEquals(0, $runner->getLastCommandExitStatus()); - } - /** - * @test - */ - public function testRunner10() { - $runner = new Runner(); - $runner->register(new Command00()); - // Don't register HelpCommand - it's automatically registered - $runner->setInputs([]); - $runner->setArgsVector([ - 'entry.php', - 'help', - '--command' => 'super-hero' - ]); - $runner->start(); - $this->assertEquals([ - " super-hero: A command to display hero's name.\n", - " Supported Arguments:\n", - " name: The name of the hero\n", - ], $runner->getOutput()); - } - /** - * @test - */ - public function testRunner11() { - $runner = new Runner(); - $runner->setBeforeStart(function (Runner $r) { - $r->setArgsVector([ - 'entry.php', - 'help', - '--command' => 'super hero', - '--ansi' - ]); - $r->register(new Command00()); - // Don't register HelpCommand - it's automatically registered - $r->setInputs([]); - }); - $runner->start(); - $this->assertEquals([ - "\e[1;91mError: \e[0mCommand 'super hero' is not supported.\n" - ], $runner->getOutput()); - } - /** - * @test - */ - public function testRunner12() { - - $runner = new Runner(); - - $runner->register(new Command00()); - // Don't register HelpCommand - it's automatically registered - - $runner->setArgsVector([ - 'entry.php', - '-i', - ]); - $runner->setInputs([ - 'exit' - ]); - $runner->start(); - $this->assertEquals([ - ">> Running in interactive mode.\n", - ">> Type command name or 'exit' to close.\n", - ">> " - ], $runner->getOutput()); - $this->assertEquals(0, $runner->getLastCommandExitStatus()); - } - /** - * @test - */ - public function testRunner13() { - - $runner = new Runner(); - $runner->register(new Command00()); - // Don't register HelpCommand - it's automatically registered - - $runner->setArgsVector([ - 'entry.php', - '-i', - ]); - $runner->setInputs([ - 'help --ansi', - 'exit' - ]); - $runner->start(); - $this->assertEquals([ - ">> Running in interactive mode.\n", - ">> Type command name or 'exit' to close.\n", - ">> Usage:\n", - " command [arg1 arg2=\"val\" arg3...]\n\n", - "Global Arguments:\n", - " --ansi:[Optional] Force the use of ANSI output.\n", - "Available Commands:\n", - " help: Display CLI Help. To display help for specific command, use the argument \"--command\" with this command.\n", - " super-hero: A command to display hero's name.\n", - ">> ", - ], $runner->getOutput()); - $this->assertEquals(0, $runner->getLastCommandExitStatus()); - } - /** - * @test - */ - public function testRunner14() { - $runner = new Runner(); - - $runner->register(new Command00()); - // Don't register HelpCommand - it's automatically registered - - $runner->setArgsVector([ - 'entry.php', - '-i', - ]); - $runner->setInputs([ - 'help --ansi --command=super-hero', - 'super-hero name=Ibrahim', - 'exit' - ]); - $runner->start(); - $this->assertEquals([ - ">> Running in interactive mode.\n", - ">> Type command name or 'exit' to close.\n", - ">> super-hero: A command to display hero's name.\n", - " Supported Arguments:\n", - " name: The name of the hero\n", - ">> Hello hero Ibrahim\n", - ">> " - ], $runner->getOutput()); - } - /** - * @test - */ - public function testRunner15() { - $runner = new Runner(); - $runner->register(new Command00()); - // Don't register HelpCommand - it's automatically registered - $runner->register(new WithExceptionCommand()); - $runner->setAfterExecution(function (Runner $r) { - $r->getActiveCommand()->println('Command Exit Status: '.$r->getLastCommandExitStatus()); - }); - $runner->setArgsVector([ - 'entry.php', - '--ansi', - '-i', - ]); - $runner->setInputs([ - 'help --command=super-hero', - 'with-exception', - 'exit' - ]); - $runner->start(); - $output = $runner->getOutput(); - // Null out the stack trace content as it can vary - for ($i = 12; $i < count($output) - 2; $i++) { - if ($output[$i] !== null && strpos($output[$i], 'Command Exit Status: -1') === false && strpos($output[$i], '>> ') === false) { - $output[$i] = null; - } - } - - $this->assertEquals([ - ">> Running in interactive mode.\n", - ">> Type command name or 'exit' to close.\n", - ">>  super-hero: A command to display hero's name.\n", - " Supported Arguments:\n", - " name: The name of the hero\n", - "Command Exit Status: 0\n", - ">> Error: An exception was thrown.\n", - "Exception Message: Call to undefined method WebFiori\Tests\Cli\TestCommands\WithExceptionCommand::notExist()\n", - "Code: 0\n", - "At: ".ROOT_DIR."tests".DS."WebFiori".DS."Tests".DS."Cli".DS."TestCommands".DS."WithExceptionCommand.php\n", - "Line: 13\n", - "Stack Trace: \n\n", - null, - "Command Exit Status: -1\n", - ">> ", - ], $output); - } - /** - * @test - */ - public function testRunner16() { - $runner = new Runner(); - $runner->register(new Command01()); - $runner->setInputs([]); - $this->assertEquals(-1, $runner->runCommand(null, [ - 'show-v' - ])); - $this->assertEquals([ - "Error: The following required argument(s) are missing: 'arg-1', 'arg-2'\n" - ], $runner->getOutput()); - } - /** - * @test - */ - public function testRunner17() { - $runner = new Runner(); - $runner->register(new Command01()); - $runner->setInputs([]); - $this->assertEquals(-1, $runner->runCommand(null, [ - 'show-v', - '--ansi' - ])); - $this->assertEquals([ - "\e[1;91mError: \e[0mThe following required argument(s) are missing: 'arg-1', 'arg-2'\n" - ], $runner->getOutput()); - } - /** - * @test - */ - public function testRunner18() { - $runner = new Runner(); - $runner->register(new Command01()); - $runner->setInputs([]); - $runner->setAfterExecution(function (Runner $r) { - $r->getActiveCommand()->println('Command Exit Status: '.$r->getLastCommandExitStatus()); - }); - $this->assertEquals(0, $runner->runCommand(null, [ - 'show-v', - 'arg-1' => 'Super Cool Arg', - 'arg-2' => "First One is Coller", - ])); - $this->assertEquals([ - "System version: 1.0.0\n", - "Super Cool Arg\n", - "First One is Coller\n", - "Hello\n", - "Command Exit Status: 0\n" - ], $runner->getOutput()); - } - /** - * @test - */ - public function testRunner19() { - $runner = new Runner(); - $runner->register(new Command00()); - // Don't register HelpCommand - it's automatically registered - $runner->register(new WithExceptionCommand()); - $runner->setArgsVector([ - 'entry.php', - '-i', - ]); - $runner->setInputs([ - '', - '', - 'exit' - ]); - $this->assertEquals(0, $runner->start()); - $this->assertEquals([ - ">> Running in interactive mode.\n", - ">> Type command name or 'exit' to close.\n", - ">> No input.\n", - ">> No input.\n", - ">> " - ], $runner->getOutput()); - } - /** - * @test - */ - public function testRunner20() { - $runner = new Runner(); - $runner->register(new Command00()); - // Don't register HelpCommand - it's automatically registered - $runner->register(new WithExceptionCommand()); - $runner->setArgsVector([ - 'entry.php', - '--ansi', - ]); - $runner->setInputs([ - - ]); - $runner->start(); - //$this->assertEquals(0, $runner->start()); - // Since help command is now the default, it will show help output instead of "No command" message - $output = $runner->getOutput(); - $this->assertNotEmpty($output); - $this->assertStringContainsString('Usage:', $output[0]); - } - /** - * @test - */ - public function testRunner21() { - $runner = new Runner(); - $runner->setArgsVector([ - - ]); - $runner->setInputStream(new ArrayInputStream([ - - ])); - $runner->setOutputStream(new ArrayOutputStream()); - - $this->assertEquals([ - - ], $runner->getOutput()); - $runner->register(new Command00()); - // Don't register HelpCommand - it's automatically registered - $runner->register(new WithExceptionCommand()); - $runner->setAfterExecution(function (Runner $r) { - $r->getActiveCommand()->println('Command Exit Status: '.$r->getLastCommandExitStatus()); - }); - - $runner->setArgsVector([ - 'entry.php', - 'with-exception', - ]); - $runner->setInputs([]); - $runner->start(); - $output = $runner->getOutput(); - //Removing the trace - $output[6] = null; - $this->assertEquals([ - "Error: An exception was thrown.\n", - "Exception Message: Call to undefined method WebFiori\\Tests\\Cli\\TestCommands\\WithExceptionCommand::notExist()\n", - "Code: 0\n", - "At: ".\ROOT_DIR."tests".\DS."WebFiori".\DS."Tests".\DS."Cli".\DS."TestCommands".\DS."WithExceptionCommand.php\n", - "Line: 13\n", - "Stack Trace: \n\n", - null, - "Command Exit Status: -1\n" - ], $output); - } - public function testRunner22() { - $runner = new Runner(); - $runner->register(new Command03()); - $runner->setArgsVector([ - 'entry.php', - 'run-another', - 'arg-1' => 'Nice', - 'arg-2' => 'Cool' - ]); - $runner->setInputStream(new ArrayInputStream([ - - ])); - $runner->setOutputStream(new ArrayOutputStream()); - $exitCode = $runner->start(); - $output = $runner->getOutput(); - $this->assertEquals([ - "Running Sub Command\n", - "System version: 1.0.0\n", - "Nice\n", - "Cool\n", - "Ur\n", - "Done\n", - ], $output); - } - /** - * @test - */ - public function test00() { - $runner = new Runner(); - $runner->setInputs([]); - $runner->setArgsVector([ - - ]); - $this->assertEquals([ - - ], $runner->getOutput()); - } - /** - * Test Runner initialization and basic properties - * @test - */ - public function testRunnerInitializationEnhanced() { - $runner = new Runner(); - - // Test initial state - $this->assertNull($runner->getActiveCommand()); - $this->assertNotNull($runner->getInputStream()); - $this->assertNotNull($runner->getOutputStream()); - $this->assertEquals(0, $runner->getLastCommandExitStatus()); - $this->assertFalse($runner->isInteractive()); - } - - /** - * Test command registration with aliases - * @test - */ - public function testCommandRegistrationWithAliasesEnhanced() { - $runner = new Runner(); - $command = new TestCommand('test-cmd', [], 'Test command'); - - // Register command with aliases - $result = $runner->register($command, ['tc', 'test']); - $this->assertSame($runner, $result); // Should return self for chaining - - // Test command is registered - $this->assertSame($command, $runner->getCommandByName('test-cmd')); - - // Test aliases are registered - $this->assertTrue($runner->hasAlias('tc')); - $this->assertTrue($runner->hasAlias('test')); - $this->assertEquals('test-cmd', $runner->resolveAlias('tc')); - $this->assertEquals('test-cmd', $runner->resolveAlias('test')); - - // Test getting all aliases - $aliases = $runner->getAliases(); - $this->assertArrayHasKey('tc', $aliases); - $this->assertArrayHasKey('test', $aliases); - $this->assertEquals('test-cmd', $aliases['tc']); - $this->assertEquals('test-cmd', $aliases['test']); - } - - /** - * Test duplicate command registration - * @test - */ - public function testDuplicateCommandRegistrationEnhanced() { - $runner = new Runner(); - $command1 = new TestCommand('test-cmd', [], 'First command'); - $command2 = new TestCommand('test-cmd', [], 'Second command'); - - // Register first command - $runner->register($command1); - $this->assertSame($command1, $runner->getCommandByName('test-cmd')); - - // Register second command with same name (should replace) - $runner->register($command2); - $this->assertSame($command2, $runner->getCommandByName('test-cmd')); - } - - /** - * Test global arguments - * @test - */ - public function testGlobalArgumentsEnhanced() { - $runner = new Runner(); - - // Add global arguments - $this->assertTrue($runner->addArg('--global-arg', [ - ArgumentOption::OPTIONAL => true, - ArgumentOption::DESCRIPTION => 'Global argument' - ])); - - // Test duplicate global argument - $this->assertFalse($runner->addArg('--global-arg', [])); // Should fail - - // Test argument exists - $this->assertTrue($runner->hasArg('--global-arg')); - $this->assertFalse($runner->hasArg('--non-existent')); - - // Test removing argument - $this->assertTrue($runner->removeArgument('--global-arg')); - $this->assertFalse($runner->hasArg('--global-arg')); - - // Test removing non-existent argument - $this->assertFalse($runner->removeArgument('--non-existent')); - } - - /** - * Test arguments vector handling - * @test - */ - public function testArgumentsVectorEnhanced() { - $runner = new Runner(); - - $argsVector = ['script.php', 'command', '--arg1=value1', '--arg2', 'value2']; - $runner->setArgsVector($argsVector); - - $this->assertEquals($argsVector, $runner->getArgsVector()); - } - - /** - * Test stream handling - * @test - */ - public function testStreamHandlingEnhanced() { - $runner = new Runner(); - - // Test setting custom streams - $customInput = new ArrayInputStream(['test input']); - $customOutput = new ArrayOutputStream(); - - $result1 = $runner->setInputStream($customInput); - $this->assertSame($runner, $result1); // Should return self - $this->assertSame($customInput, $runner->getInputStream()); - - $result2 = $runner->setOutputStream($customOutput); - $this->assertSame($runner, $result2); // Should return self - $this->assertSame($customOutput, $runner->getOutputStream()); - } - - /** - * Test inputs array handling - * @test - */ - public function testInputsArrayHandlingEnhanced() { - $runner = new Runner(); - - $inputs = ['input1', 'input2', 'input3']; - $result = $runner->setInputs($inputs); - $this->assertSame($runner, $result); // Should return self - - // The inputs should be set as ArrayInputStream - $inputStream = $runner->getInputStream(); - $this->assertInstanceOf(ArrayInputStream::class, $inputStream); - } - - /** - * Test command execution - * @test - */ - public function testCommandExecutionEnhanced() { - $runner = new Runner(); - $command = new TestCommand('test-cmd'); - $output = new ArrayOutputStream(); - - $runner->register($command); - $runner->setOutputStream($output); - - // Test running command - $exitCode = $runner->runCommand($command); - $this->assertEquals(0, $exitCode); // TestCommand should return 0 - $this->assertEquals(0, $runner->getLastCommandExitStatus()); - - // Test running with arguments - $exitCode2 = $runner->runCommand($command, ['--test-arg' => 'value']); - $this->assertEquals(0, $exitCode2); - - // Test running with ANSI - $exitCode3 = $runner->runCommand($command, [], true); - $this->assertEquals(0, $exitCode3); - } - - /** - * Test sub-command execution - * @test - */ - public function testSubCommandExecutionEnhanced() { - $runner = new Runner(); - $runner->setOutputStream(new ArrayOutputStream()); - $mainCommand = new TestCommand('main-cmd'); - $subCommand = new TestCommand('sub-cmd'); - - $runner->register($mainCommand); - $runner->register($subCommand); - - // Test running sub-command - $exitCode = $runner->runCommandAsSub('sub-cmd'); - $this->assertEquals(0, $exitCode); - - // Test running non-existent sub-command - $exitCode2 = $runner->runCommandAsSub('non-existent'); - $this->assertEquals(-1, $exitCode2); - } - - /** - * Test active command management - * @test - */ - public function testActiveCommandManagementEnhanced() { - $runner = new Runner(); - $command = new TestCommand('test-cmd'); - - // Initially no active command - $this->assertNull($runner->getActiveCommand()); - - // Set active command - $result = $runner->setActiveCommand($command); - $this->assertSame($runner, $result); // Should return self - $this->assertSame($command, $runner->getActiveCommand()); - - // Clear active command - $runner->setActiveCommand(null); - $this->assertNull($runner->getActiveCommand()); - } - - /** - * Test callback functionality - * @test - */ - public function testCallbacksEnhanced() { - $runner = new Runner(); - $callbackExecuted = false; - - // Test before start callback - $beforeCallback = function() use (&$callbackExecuted) { - $callbackExecuted = true; - }; - - $result = $runner->setBeforeStart($beforeCallback); - $this->assertSame($runner, $result); // Should return self - - // Test after execution callback - $afterCallback = function($exitCode, $command) { - // Callback should receive exit code and command - $this->assertIsInt($exitCode); - }; - - $result2 = $runner->setAfterExecution($afterCallback, ['param1', 'param2']); - $this->assertSame($runner, $result2); // Should return self - } - - /** - * Test output collection - * @test - */ - public function testOutputCollectionEnhanced() { - $runner = new Runner(); - $command = new TestCommand('test-cmd'); - $output = new ArrayOutputStream(); - - $runner->register($command); - $runner->setOutputStream($output); - - // Run command to generate output - $runner->runCommand($command); - - // Test getting output - $outputArray = $runner->getOutput(); - $this->assertIsArray($outputArray); - $this->assertNotEmpty($outputArray); - } - - /** - * Test alias resolution edge cases - * @test - */ - public function testAliasResolutionEdgeCasesEnhanced() { - $runner = new Runner(); - - // Test resolving non-existent alias - $this->assertNull($runner->resolveAlias('non-existent')); - - // Test resolving actual command name (not alias) - $command = new TestCommand('test-cmd'); - $runner->register($command); - $this->assertNull($runner->resolveAlias('test-cmd')); // Should return null for actual command names - } - - /** - * Test command retrieval edge cases - * @test - */ - public function testCommandRetrievalEdgeCasesEnhanced() { - $runner = new Runner(); - - // Test getting non-existent command - $this->assertNull($runner->getCommandByName('non-existent')); - - // Test getting command by alias - $command = new TestCommand('test-cmd'); - $runner->register($command, ['tc']); - - // Should find command by alias using getCommandByName (enhanced functionality) - $this->assertSame($command, $runner->getCommandByName('tc')); - $this->assertSame($command, $runner->getCommandByName('test-cmd')); - } - - /** - * Test argument object handling - * @test - */ - public function testArgumentObjectHandlingEnhanced() { - $runner = new Runner(); - - // Test adding Argument object - $arg = new Argument('--test-arg'); - $arg->setDescription('Test argument'); - - $result = $runner->addArgument($arg); - $this->assertTrue($result); - $this->assertTrue($runner->hasArg('--test-arg')); - - // Test adding duplicate Argument object - $arg2 = new Argument('--test-arg'); - $result2 = $runner->addArgument($arg2); - $this->assertFalse($result2); // Should fail for duplicate - } - - /** - * Test interactive mode detection - * @test - */ - public function testInteractiveModeDetectionEnhanced() { - $runner = new Runner(); - - // Initially not interactive - $this->assertFalse($runner->isInteractive()); - - // Set args vector with -i flag - $runner->setArgsVector(['script.php', '-i']); - // Note: The actual interactive detection might depend on the start() method implementation - } - - /** - * Test command discovery methods (if available) - * @test - */ - public function testCommandDiscoveryMethodsEnhanced() { - $runner = new Runner(); - - // Test auto-discovery state - $this->assertFalse($runner->isAutoDiscoveryEnabled()); // Default should be false - - // Test enabling auto-discovery - $result = $runner->enableAutoDiscovery(); - $this->assertSame($runner, $result); - $this->assertTrue($runner->isAutoDiscoveryEnabled()); - - // Test disabling auto-discovery - $result2 = $runner->disableAutoDiscovery(); - $this->assertSame($runner, $result2); - $this->assertFalse($runner->isAutoDiscoveryEnabled()); - - // Test exclude patterns - $result5 = $runner->excludePattern('*Test*'); - $this->assertSame($runner, $result5); - - $result6 = $runner->excludePatterns(['*Test*', '*Mock*']); - $this->assertSame($runner, $result6); - - // Test discovery cache - $result7 = $runner->enableDiscoveryCache('test-cache.json'); - $this->assertSame($runner, $result7); - - $result8 = $runner->disableDiscoveryCache(); - $this->assertSame($runner, $result8); - - $result9 = $runner->clearDiscoveryCache(); - $this->assertSame($runner, $result9); - - // Test strict mode - $result10 = $runner->setDiscoveryStrictMode(true); - $this->assertSame($runner, $result10); - - $result11 = $runner->setDiscoveryStrictMode(false); - $this->assertSame($runner, $result11); - } - /** - * Test command help pattern in interactive mode. - * @test - */ - public function testCommandHelpInteractive() { - $runner = new Runner(); - $runner->register(new Command00()); - // Don't register HelpCommand - it's automatically registered - - $runner->setArgsVector([ - 'entry.php', - '-i', - ]); - $runner->setInputs([ - 'super-hero help', - 'exit' - ]); - $runner->start(); - - $output = $runner->getOutput(); - - // Should show help for super-hero command - $this->assertContains(">> super-hero: A command to display hero's name.\n", $output); - $this->assertContains(" Supported Arguments:\n", $output); - $this->assertEquals(0, $runner->getLastCommandExitStatus()); - } - - /** - * Test command -h pattern in interactive mode. - * @test - */ - public function testCommandDashHInteractive() { - $runner = new Runner(); - $runner->register(new Command00()); - // Don't register HelpCommand - it's automatically registered - - $runner->setArgsVector([ - 'entry.php', - '-i', - ]); - $runner->setInputs([ - 'super-hero -h', - 'exit' - ]); - $runner->start(); - - $output = $runner->getOutput(); - - // Should show help for super-hero command - $this->assertContains(">> super-hero: A command to display hero's name.\n", $output); - $this->assertContains(" Supported Arguments:\n", $output); - $this->assertEquals(0, $runner->getLastCommandExitStatus()); - } - - /** - * Test command help pattern in non-interactive mode. - * @test - */ - public function testCommandHelpNonInteractive() { - $runner = new Runner(); - $runner->register(new Command00()); - // Don't register HelpCommand - it's automatically registered - $runner->setInputs([]); - - $runner->setArgsVector([ - 'entry.php', - 'super-hero', - 'help' - ]); - $runner->start(); - - $output = $runner->getOutput(); - - // Should show help for super-hero command - $this->assertContains(" super-hero: A command to display hero's name.\n", $output); - $this->assertContains(" Supported Arguments:\n", $output); - $this->assertEquals(0, $runner->getLastCommandExitStatus()); - } - - /** - * Test command -h pattern in non-interactive mode. - * @test - */ - public function testCommandDashHNonInteractive() { - $runner = new Runner(); - $runner->register(new Command00()); - // Don't register HelpCommand - it's automatically registered - $runner->setInputs([]); - - $runner->setArgsVector([ - 'entry.php', - 'super-hero', - '-h' - ]); - $runner->start(); - - $output = $runner->getOutput(); - - // Should show help for super-hero command - $this->assertContains(" super-hero: A command to display hero's name.\n", $output); - $this->assertContains(" Supported Arguments:\n", $output); - $this->assertEquals(0, $runner->getLastCommandExitStatus()); - } - - /** - * Test that invalid command with help doesn't trigger help. - * @test - */ - public function testInvalidCommandHelp() { - $runner = new Runner(); - $runner->register(new Command00()); - // Don't register HelpCommand - it's automatically registered - - $runner->setArgsVector([ - 'entry.php', - '-i', - ]); - $runner->setInputs([ - 'invalid-command help', - 'exit' - ]); - $runner->start(); - - $output = $runner->getOutput(); - - // Should show error for invalid command, not help - $this->assertContains(">> Error: The command 'invalid-command' is not supported.\n", $output); - $this->assertEquals(-1, $runner->getLastCommandExitStatus()); - } -} +reset(); + $this->assertTrue($runner->getOutputStream() instanceof StdOut); + $this->assertTrue($runner->getInputStream() instanceof StdIn); + $runner->setInputStream(new ArrayInputStream()); + $runner->setOutputStream(new ArrayOutputStream()); + $this->assertFalse($runner->getOutputStream() instanceof StdOut); + $this->assertFalse($runner->getInputStream() instanceof StdIn); + $this->assertTrue($runner->getInputStream() instanceof ArrayInputStream); + $this->assertTrue($runner->getOutputStream() instanceof ArrayOutputStream); + } + public function testIsCLI() { + $this->assertTrue(Runner::isCLI()); + } + /** + * @test + */ + public function testRunner00() { + $runner = new Runner(); + $this->assertEquals([], $runner->getOutput()); + // Help command is automatically registered + $this->assertEquals(['help'], array_keys($runner->getCommands())); + $this->assertFalse($runner->addArg(' ')); + $this->assertFalse($runner->addArg(' invalid name ')); + $this->assertInstanceOf(\WebFiori\Cli\Commands\HelpCommand::class, $runner->getDefaultCommand()); + $this->assertNull($runner->getActiveCommand()); + + $argObj = new Argument('--ansi'); + $this->assertFalse($runner->addArgument($argObj)); + + $this->assertTrue($runner->addArg('global-arg', [ + ArgumentOption::OPTIONAL => true + ])); + $this->assertEquals(3, count($runner->getArgs())); + $runner->removeArgument('--ansi'); + $this->assertEquals(2, count($runner->getArgs())); + $this->assertFalse($runner->hasArg('--ansi')); + $runner->register(new Command00()); + $this->assertEquals(2, count($runner->getCommands())); // help + super-hero + $runner->register(new Command00()); + $this->assertEquals(2, count($runner->getCommands())); // Still 2, no duplicates + $runner->setDefaultCommand('super-hero'); + $runner->setInputs([]); + $this->assertEquals(0, $runner->runCommand(null, [ + 'name' => 'Ibrahim' + ])); + $this->assertEquals([ + "Hello hero Ibrahim\n" + ], $runner->getOutput()); + } + /** + * @test + */ + public function testRunner01() { + $runner = new Runner(); + $this->assertEquals(0, $runner->getLastCommandExitStatus()); + $runner->setDefaultCommand('super-hero'); + // Since 'super-hero' is not registered, default remains the help command + $this->assertInstanceOf(\WebFiori\Cli\Commands\HelpCommand::class, $runner->getDefaultCommand()); + $runner->setInputs([]); + $this->assertEquals(-1, $runner->runCommand(null, [ + 'do-it', + '--ansi' + ])); + $this->assertEquals(-1, $runner->getLastCommandExitStatus()); + $this->assertEquals([ + "Error: The command 'do-it' is not supported.\n" + ], $runner->getOutput()); + } + /** + * @test + */ + public function testRunner02() { + $runner = new Runner(); + $runner->setDefaultCommand('super-hero'); + // Since 'super-hero' is not registered, default remains the help command + $this->assertInstanceOf(\WebFiori\Cli\Commands\HelpCommand::class, $runner->getDefaultCommand()); + $runner->setInputs([]); + $this->assertEquals(0, $runner->runCommand()); + $this->assertEquals(0, $runner->getLastCommandExitStatus()); + // Since default command is help, it will show help output instead of "No command" message + $output = $runner->getOutput(); + $this->assertNotEmpty($output); + $this->assertStringContainsString('Usage:', $output[0]); + } + /** + * @test + */ + public function testRunner03() { + $this->assertEquals([ + "Error: The following argument(s) have invalid values: 'name'\n", + "Info: Allowed values for the argument 'name':\n", + "Ibrahim\n", + "Ali\n" + ], $this->executeSingleCommand(new Command00(), [ + 'super-hero', + 'name' => 'Ok' + ])); + $this->assertEquals(-1, $this->getExitCode()); + } + /** + * @test + */ + public function testRunner04() { + $this->assertEquals([ + "\e[1;91mError: \e[0mThe following argument(s) have invalid values: 'name'\n", + "\e[1;34mInfo: \e[0mAllowed values for the argument 'name':\n", + "Ibrahim\n", + "Ali\n" + ], $this->executeSingleCommand(new Command00(), [ + 'name' => 'Ok', + '--ansi' + ])); + $this->assertEquals(-1, $this->getExitCode()); + } + /** + * @test + */ + public function testRunner05() { + $runner = new Runner(); + $runner->register(new Command00()); + // Don't register HelpCommand again - it's already automatically registered + $runner->removeArgument('--ansi'); + $runner->removeArgument('--no-color'); + $runner->setDefaultCommand('help'); + $runner->setInputs([]); + $this->assertEquals(0, $runner->runCommand(null, [])); + $this->assertEquals(0, $runner->getLastCommandExitStatus()); + $this->assertEquals([ + "Usage:\n", + " command [arg1 arg2=\"val\" arg3...]\n\n", + "Available Commands:\n", + " help: Display CLI Help. To display help for specific command, use the argument \"--command\" with this command.\n", + " super-hero: A command to display hero's name.\n" + ], $runner->getOutput()); + } + /** + * @test + */ + public function testRunner06() { + + $this->assertEquals([ + "Usage:\n", + " command [arg1 arg2=\"val\" arg3...]\n\n", + "Global Arguments:\n", + " --ansi:[Optional] Force the use of ANSI output.\n", + " --no-color:[Optional] Disable ANSI colored output.\n", + "Available Commands:\n", + " help: Display CLI Help. To display help for specific command, use the argument \"--command\" with this command.\n", + " super-hero: A command to display hero's name.\n" + ], $this->executeMultiCommand([], [], [ + new Command00() + // Don't register HelpCommand - it's automatically registered + ], 'help')); + $this->assertEquals(0, $this->getExitCode()); + } + /** + * @test + */ + public function testRunner07() { + $runner = new Runner(); + $runner->register(new Command00()); + $runner->setDefaultCommand('help'); + $runner->setInputs([]); + $this->assertEquals(0, $runner->runCommand(new HelpCommand(), [ + '--ansi' + ])); + $this->assertEquals(0, $runner->getLastCommandExitStatus()); + $this->assertEquals([ + "\e[1;93mUsage:\e[0m\n", + " command [arg1 arg2=\"val\" arg3...]\n\n", + "\e[1;93mGlobal Arguments:\e[0m\n", + "\e[1;33m --ansi:\e[0m[Optional] Force the use of ANSI output.\n", + "\e[1;33m --no-color:\e[0m[Optional] Disable ANSI colored output.\n", + "\e[1;93mAvailable Commands:\e[0m\n", + "\e[1;33m help\e[0m: Display CLI Help. To display help for specific command, use the argument \"--command\" with this command.\n", + "\e[1;33m super-hero\e[0m: A command to display hero's name.\n" + ], $runner->getOutput()); + } + /** + * @test + */ + public function testRunner08() { + $runner = new Runner(); + $runner->register(new Command00()); + $runner->setInputs([]); + $this->assertEquals(0, $runner->runCommand(new HelpCommand(), [ + '--ansi', + '--command' => 'super-hero' + ])); + $this->assertEquals([ + "\e[1;33m super-hero\e[0m: A command to display hero's name.\n", + "\e[1;94m Supported Arguments:\e[0m\n", + "\e[1;33m name:\e[0m The name of the hero\n", + ], $runner->getOutput()); + } + /** + * @test + */ + public function testRunner09() { + $_SERVER['argv'] = []; + $runner = new Runner(); + $runner->removeArgument('--ansi'); + $runner->removeArgument('--no-color'); + $runner->register(new Command00()); + // Don't register HelpCommand - it's automatically registered + $runner->setDefaultCommand('help'); + $runner->setInputs([]); + $runner->start(); + $this->assertEquals([ + "Usage:\n", + " command [arg1 arg2=\"val\" arg3...]\n\n", + "Available Commands:\n", + " help: Display CLI Help. To display help for specific command, use the argument \"--command\" with this command.\n", + " super-hero: A command to display hero's name.\n" + ], $runner->getOutput()); + $this->assertEquals(0, $runner->getLastCommandExitStatus()); + } + /** + * @test + */ + public function testRunner10() { + $runner = new Runner(); + $runner->register(new Command00()); + // Don't register HelpCommand - it's automatically registered + $runner->setInputs([]); + $runner->setArgsVector([ + 'entry.php', + 'help', + '--command' => 'super-hero' + ]); + $runner->start(); + $this->assertEquals([ + " super-hero: A command to display hero's name.\n", + " Supported Arguments:\n", + " name: The name of the hero\n", + ], $runner->getOutput()); + } + /** + * @test + */ + public function testRunner11() { + $runner = new Runner(); + $runner->setBeforeStart(function (Runner $r) { + $r->setArgsVector([ + 'entry.php', + 'help', + '--command' => 'super hero', + '--ansi' + ]); + $r->register(new Command00()); + // Don't register HelpCommand - it's automatically registered + $r->setInputs([]); + }); + $runner->start(); + $this->assertEquals([ + "\e[1;91mError: \e[0mCommand 'super hero' is not supported.\n" + ], $runner->getOutput()); + } + /** + * @test + */ + public function testRunner12() { + + $runner = new Runner(); + + $runner->register(new Command00()); + // Don't register HelpCommand - it's automatically registered + + $runner->setArgsVector([ + 'entry.php', + '-i', + ]); + $runner->setInputs([ + 'exit' + ]); + $runner->start(); + $this->assertEquals([ + ">> Running in interactive mode.\n", + ">> Type command name or 'exit' to close.\n", + ">> " + ], $runner->getOutput()); + $this->assertEquals(0, $runner->getLastCommandExitStatus()); + } + /** + * @test + */ + public function testRunner13() { + + $runner = new Runner(); + $runner->register(new Command00()); + // Don't register HelpCommand - it's automatically registered + + $runner->setArgsVector([ + 'entry.php', + '-i', + ]); + $runner->setInputs([ + 'help --ansi', + 'exit' + ]); + $runner->start(); + $this->assertEquals([ + ">> Running in interactive mode.\n", + ">> Type command name or 'exit' to close.\n", + ">> Usage:\n", + " command [arg1 arg2=\"val\" arg3...]\n\n", + "Global Arguments:\n", + " --ansi:[Optional] Force the use of ANSI output.\n", + " --no-color:[Optional] Disable ANSI colored output.\n", + "Available Commands:\n", + " help: Display CLI Help. To display help for specific command, use the argument \"--command\" with this command.\n", + " super-hero: A command to display hero's name.\n", + ">> ", + ], $runner->getOutput()); + $this->assertEquals(0, $runner->getLastCommandExitStatus()); + } + /** + * @test + */ + public function testRunner14() { + $runner = new Runner(); + + $runner->register(new Command00()); + // Don't register HelpCommand - it's automatically registered + + $runner->setArgsVector([ + 'entry.php', + '-i', + ]); + $runner->setInputs([ + 'help --ansi --command=super-hero', + 'super-hero name=Ibrahim', + 'exit' + ]); + $runner->start(); + $this->assertEquals([ + ">> Running in interactive mode.\n", + ">> Type command name or 'exit' to close.\n", + ">> super-hero: A command to display hero's name.\n", + " Supported Arguments:\n", + " name: The name of the hero\n", + ">> Hello hero Ibrahim\n", + ">> " + ], $runner->getOutput()); + } + /** + * @test + */ + public function testRunner15() { + $runner = new Runner(); + $runner->register(new Command00()); + // Don't register HelpCommand - it's automatically registered + $runner->register(new WithExceptionCommand()); + $runner->setAfterExecution(function (Runner $r) { + $r->getActiveCommand()->println('Command Exit Status: '.$r->getLastCommandExitStatus()); + }); + $runner->setArgsVector([ + 'entry.php', + '--ansi', + '-i', + ]); + $runner->setInputs([ + 'help --command=super-hero', + 'with-exception', + 'exit' + ]); + $runner->start(); + $output = $runner->getOutput(); + // Null out the stack trace content as it can vary + for ($i = 12; $i < count($output) - 2; $i++) { + if ($output[$i] !== null && strpos($output[$i], 'Command Exit Status: -1') === false && strpos($output[$i], '>> ') === false) { + $output[$i] = null; + } + } + + $this->assertEquals([ + ">> Running in interactive mode.\n", + ">> Type command name or 'exit' to close.\n", + ">>  super-hero: A command to display hero's name.\n", + " Supported Arguments:\n", + " name: The name of the hero\n", + "Command Exit Status: 0\n", + ">> Error: An exception was thrown.\n", + "Exception Message: Call to undefined method WebFiori\Tests\Cli\TestCommands\WithExceptionCommand::notExist()\n", + "Code: 0\n", + "At: ".ROOT_DIR."tests".DS."WebFiori".DS."Tests".DS."Cli".DS."TestCommands".DS."WithExceptionCommand.php\n", + "Line: 13\n", + "Stack Trace: \n\n", + null, + "Command Exit Status: -1\n", + ">> ", + ], $output); + } + /** + * @test + */ + public function testRunner16() { + $runner = new Runner(); + $runner->register(new Command01()); + $runner->setInputs([]); + $this->assertEquals(-1, $runner->runCommand(null, [ + 'show-v' + ])); + $this->assertEquals([ + "Error: The following required argument(s) are missing: 'arg-1', 'arg-2'\n" + ], $runner->getOutput()); + } + /** + * @test + */ + public function testRunner17() { + $runner = new Runner(); + $runner->register(new Command01()); + $runner->setInputs([]); + $this->assertEquals(-1, $runner->runCommand(null, [ + 'show-v', + '--ansi' + ])); + $this->assertEquals([ + "\e[1;91mError: \e[0mThe following required argument(s) are missing: 'arg-1', 'arg-2'\n" + ], $runner->getOutput()); + } + /** + * @test + */ + public function testRunner18() { + $runner = new Runner(); + $runner->register(new Command01()); + $runner->setInputs([]); + $runner->setAfterExecution(function (Runner $r) { + $r->getActiveCommand()->println('Command Exit Status: '.$r->getLastCommandExitStatus()); + }); + $this->assertEquals(0, $runner->runCommand(null, [ + 'show-v', + 'arg-1' => 'Super Cool Arg', + 'arg-2' => "First One is Coller", + ])); + $this->assertEquals([ + "System version: 1.0.0\n", + "Super Cool Arg\n", + "First One is Coller\n", + "Hello\n", + "Command Exit Status: 0\n" + ], $runner->getOutput()); + } + /** + * @test + */ + public function testRunner19() { + $runner = new Runner(); + $runner->register(new Command00()); + // Don't register HelpCommand - it's automatically registered + $runner->register(new WithExceptionCommand()); + $runner->setArgsVector([ + 'entry.php', + '-i', + ]); + $runner->setInputs([ + '', + '', + 'exit' + ]); + $this->assertEquals(0, $runner->start()); + $this->assertEquals([ + ">> Running in interactive mode.\n", + ">> Type command name or 'exit' to close.\n", + ">> No input.\n", + ">> No input.\n", + ">> " + ], $runner->getOutput()); + } + /** + * @test + */ + public function testRunner20() { + $runner = new Runner(); + $runner->register(new Command00()); + // Don't register HelpCommand - it's automatically registered + $runner->register(new WithExceptionCommand()); + $runner->setArgsVector([ + 'entry.php', + '--ansi', + ]); + $runner->setInputs([ + + ]); + $runner->start(); + //$this->assertEquals(0, $runner->start()); + // Since help command is now the default, it will show help output instead of "No command" message + $output = $runner->getOutput(); + $this->assertNotEmpty($output); + $this->assertStringContainsString('Usage:', $output[0]); + } + /** + * @test + */ + public function testRunner21() { + $runner = new Runner(); + $runner->setArgsVector([ + + ]); + $runner->setInputStream(new ArrayInputStream([ + + ])); + $runner->setOutputStream(new ArrayOutputStream()); + + $this->assertEquals([ + + ], $runner->getOutput()); + $runner->register(new Command00()); + // Don't register HelpCommand - it's automatically registered + $runner->register(new WithExceptionCommand()); + $runner->setAfterExecution(function (Runner $r) { + $r->getActiveCommand()->println('Command Exit Status: '.$r->getLastCommandExitStatus()); + }); + + $runner->setArgsVector([ + 'entry.php', + 'with-exception', + ]); + $runner->setInputs([]); + $runner->start(); + $output = $runner->getOutput(); + //Removing the trace + $output[6] = null; + $this->assertEquals([ + "Error: An exception was thrown.\n", + "Exception Message: Call to undefined method WebFiori\\Tests\\Cli\\TestCommands\\WithExceptionCommand::notExist()\n", + "Code: 0\n", + "At: ".\ROOT_DIR."tests".\DS."WebFiori".\DS."Tests".\DS."Cli".\DS."TestCommands".\DS."WithExceptionCommand.php\n", + "Line: 13\n", + "Stack Trace: \n\n", + null, + "Command Exit Status: -1\n" + ], $output); + } + public function testRunner22() { + $runner = new Runner(); + $runner->register(new Command03()); + $runner->setArgsVector([ + 'entry.php', + 'run-another', + 'arg-1' => 'Nice', + 'arg-2' => 'Cool' + ]); + $runner->setInputStream(new ArrayInputStream([ + + ])); + $runner->setOutputStream(new ArrayOutputStream()); + $exitCode = $runner->start(); + $output = $runner->getOutput(); + $this->assertEquals([ + "Running Sub Command\n", + "System version: 1.0.0\n", + "Nice\n", + "Cool\n", + "Ur\n", + "Done\n", + ], $output); + } + /** + * @test + */ + public function test00() { + $runner = new Runner(); + $runner->setInputs([]); + $runner->setArgsVector([ + + ]); + $this->assertEquals([ + + ], $runner->getOutput()); + } + /** + * Test Runner initialization and basic properties + * @test + */ + public function testRunnerInitializationEnhanced() { + $runner = new Runner(); + + // Test initial state + $this->assertNull($runner->getActiveCommand()); + $this->assertNotNull($runner->getInputStream()); + $this->assertNotNull($runner->getOutputStream()); + $this->assertEquals(0, $runner->getLastCommandExitStatus()); + $this->assertFalse($runner->isInteractive()); + } + + /** + * Test command registration with aliases + * @test + */ + public function testCommandRegistrationWithAliasesEnhanced() { + $runner = new Runner(); + $command = new TestCommand('test-cmd', [], 'Test command'); + + // Register command with aliases + $result = $runner->register($command, ['tc', 'test']); + $this->assertSame($runner, $result); // Should return self for chaining + + // Test command is registered + $this->assertSame($command, $runner->getCommandByName('test-cmd')); + + // Test aliases are registered + $this->assertTrue($runner->hasAlias('tc')); + $this->assertTrue($runner->hasAlias('test')); + $this->assertEquals('test-cmd', $runner->resolveAlias('tc')); + $this->assertEquals('test-cmd', $runner->resolveAlias('test')); + + // Test getting all aliases + $aliases = $runner->getAliases(); + $this->assertArrayHasKey('tc', $aliases); + $this->assertArrayHasKey('test', $aliases); + $this->assertEquals('test-cmd', $aliases['tc']); + $this->assertEquals('test-cmd', $aliases['test']); + } + + /** + * Test duplicate command registration + * @test + */ + public function testDuplicateCommandRegistrationEnhanced() { + $runner = new Runner(); + $command1 = new TestCommand('test-cmd', [], 'First command'); + $command2 = new TestCommand('test-cmd', [], 'Second command'); + + // Register first command + $runner->register($command1); + $this->assertSame($command1, $runner->getCommandByName('test-cmd')); + + // Register second command with same name (should replace) + $runner->register($command2); + $this->assertSame($command2, $runner->getCommandByName('test-cmd')); + } + + /** + * Test global arguments + * @test + */ + public function testGlobalArgumentsEnhanced() { + $runner = new Runner(); + + // Add global arguments + $this->assertTrue($runner->addArg('--global-arg', [ + ArgumentOption::OPTIONAL => true, + ArgumentOption::DESCRIPTION => 'Global argument' + ])); + + // Test duplicate global argument + $this->assertFalse($runner->addArg('--global-arg', [])); // Should fail + + // Test argument exists + $this->assertTrue($runner->hasArg('--global-arg')); + $this->assertFalse($runner->hasArg('--non-existent')); + + // Test removing argument + $this->assertTrue($runner->removeArgument('--global-arg')); + $this->assertFalse($runner->hasArg('--global-arg')); + + // Test removing non-existent argument + $this->assertFalse($runner->removeArgument('--non-existent')); + } + + /** + * Test arguments vector handling + * @test + */ + public function testArgumentsVectorEnhanced() { + $runner = new Runner(); + + $argsVector = ['script.php', 'command', '--arg1=value1', '--arg2', 'value2']; + $runner->setArgsVector($argsVector); + + $this->assertEquals($argsVector, $runner->getArgsVector()); + } + + /** + * Test stream handling + * @test + */ + public function testStreamHandlingEnhanced() { + $runner = new Runner(); + + // Test setting custom streams + $customInput = new ArrayInputStream(['test input']); + $customOutput = new ArrayOutputStream(); + + $result1 = $runner->setInputStream($customInput); + $this->assertSame($runner, $result1); // Should return self + $this->assertSame($customInput, $runner->getInputStream()); + + $result2 = $runner->setOutputStream($customOutput); + $this->assertSame($runner, $result2); // Should return self + $this->assertSame($customOutput, $runner->getOutputStream()); + } + + /** + * Test inputs array handling + * @test + */ + public function testInputsArrayHandlingEnhanced() { + $runner = new Runner(); + + $inputs = ['input1', 'input2', 'input3']; + $result = $runner->setInputs($inputs); + $this->assertSame($runner, $result); // Should return self + + // The inputs should be set as ArrayInputStream + $inputStream = $runner->getInputStream(); + $this->assertInstanceOf(ArrayInputStream::class, $inputStream); + } + + /** + * Test command execution + * @test + */ + public function testCommandExecutionEnhanced() { + $runner = new Runner(); + $command = new TestCommand('test-cmd'); + $output = new ArrayOutputStream(); + + $runner->register($command); + $runner->setOutputStream($output); + + // Test running command + $exitCode = $runner->runCommand($command); + $this->assertEquals(0, $exitCode); // TestCommand should return 0 + $this->assertEquals(0, $runner->getLastCommandExitStatus()); + + // Test running with arguments + $exitCode2 = $runner->runCommand($command, ['--test-arg' => 'value']); + $this->assertEquals(0, $exitCode2); + + // Test running with ANSI + $exitCode3 = $runner->runCommand($command, [], true); + $this->assertEquals(0, $exitCode3); + } + + /** + * Test sub-command execution + * @test + */ + public function testSubCommandExecutionEnhanced() { + $runner = new Runner(); + $runner->setOutputStream(new ArrayOutputStream()); + $mainCommand = new TestCommand('main-cmd'); + $subCommand = new TestCommand('sub-cmd'); + + $runner->register($mainCommand); + $runner->register($subCommand); + + // Test running sub-command + $exitCode = $runner->runCommandAsSub('sub-cmd'); + $this->assertEquals(0, $exitCode); + + // Test running non-existent sub-command + $exitCode2 = $runner->runCommandAsSub('non-existent'); + $this->assertEquals(-1, $exitCode2); + } + + /** + * Test active command management + * @test + */ + public function testActiveCommandManagementEnhanced() { + $runner = new Runner(); + $command = new TestCommand('test-cmd'); + + // Initially no active command + $this->assertNull($runner->getActiveCommand()); + + // Set active command + $result = $runner->setActiveCommand($command); + $this->assertSame($runner, $result); // Should return self + $this->assertSame($command, $runner->getActiveCommand()); + + // Clear active command + $runner->setActiveCommand(null); + $this->assertNull($runner->getActiveCommand()); + } + + /** + * Test callback functionality + * @test + */ + public function testCallbacksEnhanced() { + $runner = new Runner(); + $callbackExecuted = false; + + // Test before start callback + $beforeCallback = function() use (&$callbackExecuted) { + $callbackExecuted = true; + }; + + $result = $runner->setBeforeStart($beforeCallback); + $this->assertSame($runner, $result); // Should return self + + // Test after execution callback + $afterCallback = function($exitCode, $command) { + // Callback should receive exit code and command + $this->assertIsInt($exitCode); + }; + + $result2 = $runner->setAfterExecution($afterCallback, ['param1', 'param2']); + $this->assertSame($runner, $result2); // Should return self + } + + /** + * Test output collection + * @test + */ + public function testOutputCollectionEnhanced() { + $runner = new Runner(); + $command = new TestCommand('test-cmd'); + $output = new ArrayOutputStream(); + + $runner->register($command); + $runner->setOutputStream($output); + + // Run command to generate output + $runner->runCommand($command); + + // Test getting output + $outputArray = $runner->getOutput(); + $this->assertIsArray($outputArray); + $this->assertNotEmpty($outputArray); + } + + /** + * Test alias resolution edge cases + * @test + */ + public function testAliasResolutionEdgeCasesEnhanced() { + $runner = new Runner(); + + // Test resolving non-existent alias + $this->assertNull($runner->resolveAlias('non-existent')); + + // Test resolving actual command name (not alias) + $command = new TestCommand('test-cmd'); + $runner->register($command); + $this->assertNull($runner->resolveAlias('test-cmd')); // Should return null for actual command names + } + + /** + * Test command retrieval edge cases + * @test + */ + public function testCommandRetrievalEdgeCasesEnhanced() { + $runner = new Runner(); + + // Test getting non-existent command + $this->assertNull($runner->getCommandByName('non-existent')); + + // Test getting command by alias + $command = new TestCommand('test-cmd'); + $runner->register($command, ['tc']); + + // Should find command by alias using getCommandByName (enhanced functionality) + $this->assertSame($command, $runner->getCommandByName('tc')); + $this->assertSame($command, $runner->getCommandByName('test-cmd')); + } + + /** + * Test argument object handling + * @test + */ + public function testArgumentObjectHandlingEnhanced() { + $runner = new Runner(); + + // Test adding Argument object + $arg = new Argument('--test-arg'); + $arg->setDescription('Test argument'); + + $result = $runner->addArgument($arg); + $this->assertTrue($result); + $this->assertTrue($runner->hasArg('--test-arg')); + + // Test adding duplicate Argument object + $arg2 = new Argument('--test-arg'); + $result2 = $runner->addArgument($arg2); + $this->assertFalse($result2); // Should fail for duplicate + } + + /** + * Test interactive mode detection + * @test + */ + public function testInteractiveModeDetectionEnhanced() { + $runner = new Runner(); + + // Initially not interactive + $this->assertFalse($runner->isInteractive()); + + // Set args vector with -i flag + $runner->setArgsVector(['script.php', '-i']); + // Note: The actual interactive detection might depend on the start() method implementation + } + + /** + * Test command discovery methods (if available) + * @test + */ + public function testCommandDiscoveryMethodsEnhanced() { + $runner = new Runner(); + + // Test auto-discovery state + $this->assertFalse($runner->isAutoDiscoveryEnabled()); // Default should be false + + // Test enabling auto-discovery + $result = $runner->enableAutoDiscovery(); + $this->assertSame($runner, $result); + $this->assertTrue($runner->isAutoDiscoveryEnabled()); + + // Test disabling auto-discovery + $result2 = $runner->disableAutoDiscovery(); + $this->assertSame($runner, $result2); + $this->assertFalse($runner->isAutoDiscoveryEnabled()); + + // Test exclude patterns + $result5 = $runner->excludePattern('*Test*'); + $this->assertSame($runner, $result5); + + $result6 = $runner->excludePatterns(['*Test*', '*Mock*']); + $this->assertSame($runner, $result6); + + // Test discovery cache + $result7 = $runner->enableDiscoveryCache('test-cache.json'); + $this->assertSame($runner, $result7); + + $result8 = $runner->disableDiscoveryCache(); + $this->assertSame($runner, $result8); + + $result9 = $runner->clearDiscoveryCache(); + $this->assertSame($runner, $result9); + + // Test strict mode + $result10 = $runner->setDiscoveryStrictMode(true); + $this->assertSame($runner, $result10); + + $result11 = $runner->setDiscoveryStrictMode(false); + $this->assertSame($runner, $result11); + } + /** + * Test command help pattern in interactive mode. + * @test + */ + public function testCommandHelpInteractive() { + $runner = new Runner(); + $runner->register(new Command00()); + // Don't register HelpCommand - it's automatically registered + + $runner->setArgsVector([ + 'entry.php', + '-i', + ]); + $runner->setInputs([ + 'super-hero help', + 'exit' + ]); + $runner->start(); + + $output = $runner->getOutput(); + + // Should show help for super-hero command + $this->assertContains(">> super-hero: A command to display hero's name.\n", $output); + $this->assertContains(" Supported Arguments:\n", $output); + $this->assertEquals(0, $runner->getLastCommandExitStatus()); + } + + /** + * Test command -h pattern in interactive mode. + * @test + */ + public function testCommandDashHInteractive() { + $runner = new Runner(); + $runner->register(new Command00()); + // Don't register HelpCommand - it's automatically registered + + $runner->setArgsVector([ + 'entry.php', + '-i', + ]); + $runner->setInputs([ + 'super-hero -h', + 'exit' + ]); + $runner->start(); + + $output = $runner->getOutput(); + + // Should show help for super-hero command + $this->assertContains(">> super-hero: A command to display hero's name.\n", $output); + $this->assertContains(" Supported Arguments:\n", $output); + $this->assertEquals(0, $runner->getLastCommandExitStatus()); + } + + /** + * Test command help pattern in non-interactive mode. + * @test + */ + public function testCommandHelpNonInteractive() { + $runner = new Runner(); + $runner->register(new Command00()); + // Don't register HelpCommand - it's automatically registered + $runner->setInputs([]); + + $runner->setArgsVector([ + 'entry.php', + 'super-hero', + 'help' + ]); + $runner->start(); + + $output = $runner->getOutput(); + + // Should show help for super-hero command + $this->assertContains(" super-hero: A command to display hero's name.\n", $output); + $this->assertContains(" Supported Arguments:\n", $output); + $this->assertEquals(0, $runner->getLastCommandExitStatus()); + } + + /** + * Test command -h pattern in non-interactive mode. + * @test + */ + public function testCommandDashHNonInteractive() { + $runner = new Runner(); + $runner->register(new Command00()); + // Don't register HelpCommand - it's automatically registered + $runner->setInputs([]); + + $runner->setArgsVector([ + 'entry.php', + 'super-hero', + '-h' + ]); + $runner->start(); + + $output = $runner->getOutput(); + + // Should show help for super-hero command + $this->assertContains(" super-hero: A command to display hero's name.\n", $output); + $this->assertContains(" Supported Arguments:\n", $output); + $this->assertEquals(0, $runner->getLastCommandExitStatus()); + } + + /** + * Test that invalid command with help doesn't trigger help. + * @test + */ + public function testInvalidCommandHelp() { + $runner = new Runner(); + $runner->register(new Command00()); + // Don't register HelpCommand - it's automatically registered + + $runner->setArgsVector([ + 'entry.php', + '-i', + ]); + $runner->setInputs([ + 'invalid-command help', + 'exit' + ]); + $runner->start(); + + $output = $runner->getOutput(); + + // Should show error for invalid command, not help + $this->assertContains(">> Error: The command 'invalid-command' is not supported.\n", $output); + $this->assertEquals(-1, $runner->getLastCommandExitStatus()); + } +} From a4cd174a8833f20640a3e9ec765205bf4a8de85f Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Sat, 13 Jun 2026 04:14:22 +0300 Subject: [PATCH 2/2] ci: show all commit types in release-please changelog Add hidden: false to all changelog-sections so commits like fix(ci), test:, docs:, refactor:, etc. appear in the changelog. --- release-please-config.json | 46 +++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/release-please-config.json b/release-please-config.json index 4ae27d7..0c623f6 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,23 +1,23 @@ -{ - "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", - "include-v-in-tag":true, - "tag-separator": "-", - "changelog-path": "CHANGELOG.md", - "changelog-sections": [ - { "type": "feat", "section": "Features" }, - { "type": "feature", "section": "Features" }, - { "type": "fix", "section": "Bug Fixes" }, - { "type": "perf", "section": "Performance Improvements" }, - { "type": "revert", "section": "Reverts" }, - { "type": "docs", "section": "Documentation" }, - { "type": "style", "section": "Styles" }, - { "type": "chore", "section": "Miscellaneous Chores" }, - { "type": "refactor", "section": "Code Refactoring" }, - { "type": "test", "section": "Testing" }, - { "type": "build", "section": "Build System" }, - { "type": "ci", "section": "Continuous Integration" }, - { "type": "ui", "section": "User Interface" }, - { "type": "database", "section": "Database Changes" }, - { "type": "email", "section": "Email Notifications Changes" } - ] -} \ No newline at end of file +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "tag-separator": "-", + "changelog-path": "CHANGELOG.md", + "changelog-sections": [ + { "type": "feat", "section": "Features", "hidden": false }, + { "type": "feature", "section": "Features", "hidden": false }, + { "type": "fix", "section": "Bug Fixes", "hidden": false }, + { "type": "perf", "section": "Performance Improvements", "hidden": false }, + { "type": "revert", "section": "Reverts", "hidden": false }, + { "type": "docs", "section": "Documentation", "hidden": false }, + { "type": "style", "section": "Styles", "hidden": false }, + { "type": "chore", "section": "Miscellaneous Chores", "hidden": false }, + { "type": "refactor", "section": "Code Refactoring", "hidden": false }, + { "type": "test", "section": "Testing", "hidden": false }, + { "type": "build", "section": "Build System", "hidden": false }, + { "type": "ci", "section": "Continuous Integration", "hidden": false }, + { "type": "ui", "section": "User Interface", "hidden": false }, + { "type": "database", "section": "Database Changes", "hidden": false }, + { "type": "email", "section": "Email Notifications Changes", "hidden": false } + ] +}