diff --git a/.gitignore b/.gitignore index 36ce007..2e8fcfc 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ test/* tests/clover.xml cache/commands.json *.Identifier +/test2 +/home/ibrahim/cli/test2 diff --git a/WebFiori/Cli/Commands/InitAppCommand.php b/WebFiori/Cli/Commands/InitAppCommand.php index b89ecb4..459080b 100644 --- a/WebFiori/Cli/Commands/InitAppCommand.php +++ b/WebFiori/Cli/Commands/InitAppCommand.php @@ -31,7 +31,11 @@ public function exec(): int { } if (defined('ROOT_DIR')) { - $appPath = ROOT_DIR.DIRECTORY_SEPARATOR.$dirName; + $path = ROOT_DIR; + if ($path[strlen($path) - 1] != DIRECTORY_SEPARATOR) { + $path .= DIRECTORY_SEPARATOR; + } + $appPath = $path.$dirName; } else { $appPath = getcwd().DIRECTORY_SEPARATOR.$dirName; } @@ -44,7 +48,7 @@ public function exec(): int { $this->success('App created successfully.'); return 0; - } catch (\Exception $ex) { + } catch (\Throwable $ex) { $this->error('Unable to initialize due to an exception:'); $this->println($ex->getCode().' - '.$ex->getMessage()); diff --git a/WebFiori/Cli/KeysMap.php b/WebFiori/Cli/KeysMap.php index bdd6449..289fe6f 100644 --- a/WebFiori/Cli/KeysMap.php +++ b/WebFiori/Cli/KeysMap.php @@ -97,6 +97,31 @@ public static function read(InputStream $stream, $bytes = 1) : string { */ public static function readAndTranslate(InputStream $stream) : string { $keypress = $stream->read(); + + // Handle EOF + if ($keypress === '') { + return ''; + } + + // Handle escape sequences (multi-byte) - kept for future use + if ($keypress === "\033") { + try { + // Read the next character to see if it's part of an escape sequence + $next = $stream->read(); + if ($next === '[') { + // Read the final character of the sequence + $final = $stream->read(); + $sequence = $keypress . $next . $final; + return self::map($sequence); + } else { + // Not a complete escape sequence, return ESC + return self::map($keypress); + } + } catch (\Exception $e) { + // If we can't read more bytes, just return ESC + return self::map($keypress); + } + } return self::map($keypress); } @@ -134,18 +159,14 @@ private static function appendChar($ch, &$input) { if ($ch == 'BACKSPACE' && strlen($input) > 0) { $input = substr($input, 0, strlen($input) - 1); } else if ($ch == 'ESC') { - $input .= "\e"; + // Ignore ESC key } else if ($ch == "CR") { // Do nothing - don't add CR to input } else if ($ch == "LF") { // Do nothing - don't add LF to input (readLine should not include line ending) - } else if ($ch == 'DOWN') { - // read history; - //$input .= ' '; - } else if ($ch == 'UP') { - // read history; - //$input .= ' '; - } else if ($ch != 'CR' && $ch != 'LF') { + } else if ($ch == 'DOWN' || $ch == 'UP' || $ch == 'LEFT' || $ch == 'RIGHT') { + // Ignore arrow keys - kept for future navigation features + } else { if ($ch == 'SPACE') { $input .= ' '; } else { diff --git a/tests/WebFiori/Tests/Cli/AliasingIntegrationTest.php b/tests/WebFiori/Tests/Cli/AliasingIntegrationTest.php index 04cf849..d38a02f 100644 --- a/tests/WebFiori/Tests/Cli/AliasingIntegrationTest.php +++ b/tests/WebFiori/Tests/Cli/AliasingIntegrationTest.php @@ -28,7 +28,7 @@ public function testAliasingWithHelpCommand() { $runner->register($aliasCommand); // Test help for command via direct name (not alias, as help might not resolve aliases) - $runner->setArgsVector(['script.php', 'help', '--command-name=alias-test']); + $runner->setArgsVector(['script.php', 'help', '--command=alias-test']); $exitCode = $runner->start(); $output = $runner->getOutputStream()->getOutputArray(); diff --git a/tests/WebFiori/Tests/Cli/InitAppCommandTest.php b/tests/WebFiori/Tests/Cli/InitAppCommandTest.php index 5fef869..b465e80 100644 --- a/tests/WebFiori/Tests/Cli/InitAppCommandTest.php +++ b/tests/WebFiori/Tests/Cli/InitAppCommandTest.php @@ -42,10 +42,16 @@ public function test01() { '--dir' => "test\0a" ]); $this->assertEquals(-1, $r->start()); + $output = $r->getOutput(); + // Check key elements instead of exact match due to binary string representation differences + $this->assertCount(4, $output); + $this->assertStringContainsString('Creating new app at', $output[0]); + $this->assertStringContainsString('Creating "test', $output[1]); + $this->assertStringContainsString('Error: Unable to initialize', $output[2]); + $this->assertStringContainsString('null bytes', $output[3]); } /** * @test - * @depends test01 */ public function test02() { $r = new Runner(); @@ -57,12 +63,20 @@ public function test02() { 'init', '--dir' => 'test' ]); - $appPath = ROOT_DIR.DS.'test'; + // Cleanup existing files + $appPath = ROOT_DIR.'test'; + if (file_exists($appPath)) { + unlink(ROOT_DIR.DS.'test'.DS.'main.php'); + unlink(ROOT_DIR.DS.'test'.DS.'HelloCommand.php'); + unlink(ROOT_DIR.DS.'test'.DS.'main'); + rmdir(ROOT_DIR.DS.'test'); + } $this->assertEquals(0, $r->start()); $this->assertEquals([ "Creating new app at \"$appPath\" ...\n", "Creating \"test/main.php\"...\n", - "Creating \"test/test\"...\n", + "Creating \"test/main\"...\n", + "Creating \"test/HelloCommand.php\"...\n", "Success: App created successfully.\n" ], $r->getOutput()); } @@ -80,18 +94,22 @@ public function test03() { 'init', '--dir' => 'test' ]); + // Don't cleanup - this test expects files to exist $this->assertEquals(0, $r->start()); - $appPath = ROOT_DIR.DS.'test'; + $appPath = ROOT_DIR.'test'; $this->assertEquals([ "Creating new app at \"$appPath\" ...\n", "Creating \"test/main.php\"...\n", "Warning: File main.php already exist!\n", - "Creating \"test/test\"...\n", - "Warning: File test already exist!\n", + "Creating \"test/main\"...\n", + "Warning: File main already exist!\n", + "Creating \"test/HelloCommand.php\"...\n", + "Warning: File HelloCommand.php already exist!\n", "Success: App created successfully.\n" ], $r->getOutput()); unlink(ROOT_DIR.DS.'test'.DS.'main.php'); - unlink(ROOT_DIR.DS.'test'.DS.'test'); + unlink(ROOT_DIR.DS.'test'.DS.'HelloCommand.php'); + unlink(ROOT_DIR.DS.'test'.DS.'main'); rmdir(ROOT_DIR.DS.'test'); } /** @@ -108,16 +126,25 @@ public function test04() { '--dir' => 'test2', '--entry' => 'bang' ]); + // Cleanup existing files + $appPath = ROOT_DIR.'test2'; + if (file_exists($appPath)) { + unlink($appPath.DS.'main.php'); + unlink($appPath.DS.'bang'); + unlink($appPath.DS.'HelloCommand.php'); + rmdir($appPath); + } $this->assertEquals(0, $r->start()); - $appPath = ROOT_DIR.DS.'test2'; $this->assertEquals([ "Creating new app at \"$appPath\" ...\n", "Creating \"test2/main.php\"...\n", "Creating \"test2/bang\"...\n", + "Creating \"test2/HelloCommand.php\"...\n", "Success: App created successfully.\n" ], $r->getOutput()); unlink($appPath.DS.'main.php'); unlink($appPath.DS.'bang'); + unlink($appPath.DS.'HelloCommand.php'); rmdir($appPath); } } diff --git a/tests/WebFiori/Tests/Cli/KeysMapTest.php b/tests/WebFiori/Tests/Cli/KeysMapTest.php index 3bb713a..eda39c3 100644 --- a/tests/WebFiori/Tests/Cli/KeysMapTest.php +++ b/tests/WebFiori/Tests/Cli/KeysMapTest.php @@ -4,39 +4,77 @@ use PHPUnit\Framework\TestCase; use WebFiori\Cli\KeysMap; use WebFiori\Cli\Streams\ArrayInputStream; + /** - * Description of KeysMapTest - * - * @author Ibrahim + * Test cases for KeysMap class arrow key handling. */ class KeysMapTest extends TestCase { + + /** + * Test that arrow key escape sequences are properly detected. + */ + public function testArrowKeyDetection() { + // Test UP arrow + $inputStream = new ArrayInputStream(["\033[A"]); + $result = KeysMap::readAndTranslate($inputStream); + $this->assertEquals('UP', $result); + + // Test DOWN arrow + $inputStream = new ArrayInputStream(["\033[B"]); + $result = KeysMap::readAndTranslate($inputStream); + $this->assertEquals('DOWN', $result); + + // Test RIGHT arrow + $inputStream = new ArrayInputStream(["\033[C"]); + $result = KeysMap::readAndTranslate($inputStream); + $this->assertEquals('RIGHT', $result); + + // Test LEFT arrow + $inputStream = new ArrayInputStream(["\033[D"]); + $result = KeysMap::readAndTranslate($inputStream); + $this->assertEquals('LEFT', $result); + } + + /** + * Test that arrow keys don't appear in readline input. + */ + public function testArrowKeysIgnoredInReadLine() { + // Input with arrow keys mixed with regular text + $inputStream = new ArrayInputStream(["\033[A\033[Bhello\033[C\033[D\n"]); + $result = KeysMap::readLine($inputStream); + + // Should only contain "hello", arrow keys should be ignored + $this->assertEquals('hello', $result); + } + /** - * @test + * Test that regular characters still work normally. */ - public function test00() { - $stream = new ArrayInputStream([ - chr(27) // ESC character - ]); - $this->assertEquals("ESC", KeysMap::readAndTranslate($stream)); + public function testRegularCharacters() { + $inputStream = new ArrayInputStream(["hello world\n"]); + $result = KeysMap::readLine($inputStream); + + $this->assertEquals('hello world', $result); } + /** - * @test + * Test backspace functionality still works. */ - public function test01() { - $stream = new ArrayInputStream([ - "\r" - ]); - $this->assertEquals("CR", KeysMap::readAndTranslate($stream)); + public function testBackspaceWithArrowKeys() { + // Type "hello", backspace once, arrow keys (ignored), type "p" + $inputStream = new ArrayInputStream(["hello\177\033[A\033[Bp\n"]); + $result = KeysMap::readLine($inputStream); + + // Should be "hellp" (hello -> hell -> hell -> hell -> hellp) + $this->assertEquals('hellp', $result); } + /** - * @test + * Test ESC key handling. */ - public function test02() { - $stream = new ArrayInputStream([ - "\r", - "\n" - ]); - $this->assertEquals("CR", KeysMap::readAndTranslate($stream)); - $this->assertEquals("LF", KeysMap::readAndTranslate($stream)); + public function testEscapeKey() { + $inputStream = new ArrayInputStream(["\ehello\n"]); + $result = KeysMap::readAndTranslate($inputStream); + $this->assertEquals('ESC', $result); } } diff --git a/tests/WebFiori/Tests/Cli/RunnerTest.php b/tests/WebFiori/Tests/Cli/RunnerTest.php index 0728a58..38783ee 100644 --- a/tests/WebFiori/Tests/Cli/RunnerTest.php +++ b/tests/WebFiori/Tests/Cli/RunnerTest.php @@ -15,8 +15,6 @@ use WebFiori\Tests\Cli\TestCommands\WithExceptionCommand; use WebFiori\Tests\Cli\TestCommands\Command03; use WebFiori\Tests\Cli\TestCommand; -use const DS; -use const ROOT_DIR; /** @@ -160,7 +158,7 @@ public function testRunner05() { "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-name\" with this command.\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()); } @@ -175,7 +173,7 @@ public function testRunner06() { "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-name\" with this command.\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() @@ -201,7 +199,7 @@ public function testRunner07() { "\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-name\" with this command.\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()); } @@ -214,14 +212,12 @@ public function testRunner08() { $runner->setInputs([]); $this->assertEquals(0, $runner->runCommand(new HelpCommand(), [ '--ansi', - '--command-name' => 'super-hero' + '--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", - "\e[1;33m help:\e[0m[Optional] Display command help.\n", - " -h:[Optional] \n" ], $runner->getOutput()); } /** @@ -240,7 +236,7 @@ public function testRunner09() { "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-name\" with this command.\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()); @@ -256,15 +252,13 @@ public function testRunner10() { $runner->setArgsVector([ 'entry.php', 'help', - '--command-name' => 'super-hero' + '--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", - " help:[Optional] Display command help.\n", - " -h:[Optional] \n" ], $runner->getOutput()); } /** @@ -276,7 +270,7 @@ public function testRunner11() { $r->setArgsVector([ 'entry.php', 'help', - '--command-name' => 'super hero', + '--command' => 'super hero', '--ansi' ]); $r->register(new Command00()); @@ -339,7 +333,7 @@ public function testRunner13() { "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-name\" with this command.\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()); @@ -359,7 +353,7 @@ public function testRunner14() { '-i', ]); $runner->setInputs([ - 'help --ansi --command-name=super-hero', + 'help --ansi --command=super-hero', 'super-hero name=Ibrahim', 'exit' ]); @@ -370,8 +364,6 @@ public function testRunner14() { ">> super-hero: A command to display hero's name.\n", " Supported Arguments:\n", " name: The name of the hero\n", - " help:[Optional] Display command help.\n", - " -h:[Optional] \n", ">> Hello hero Ibrahim\n", ">> " ], $runner->getOutput()); @@ -393,31 +385,30 @@ public function testRunner15() { '-i', ]); $runner->setInputs([ - 'help --command-name=super-hero', + 'help --command=super-hero', 'with-exception', 'exit' ]); $runner->start(); $output = $runner->getOutput(); // Null out the stack trace content as it can vary - for ($i = 14; $i < count($output) - 2; $i++) { + 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", - " help:[Optional] Display command help.\n", - " -h:[Optional] \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", + "At: ".ROOT_DIR."tests".DS."WebFiori".DS."Tests".DS."Cli".DS."TestCommands".DS."WithExceptionCommand.php\n", "Line: 13\n", "Stack Trace: \n\n", null, diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 905bd7d..7ba7bbf 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -1,30 +1,23 @@ - + + + + - + + - ../WebFiori/Cli/Command.php - ../WebFiori/Cli/CommandArgument.php - ../WebFiori/Cli/Formatter.php - ../WebFiori/Cli/KeysMap.php - ../WebFiori/Cli/Runner.php - ../WebFiori/Cli/CommandTestCase.php - ../WebFiori/Cli/InputValidator.php - ../WebFiori/Cli/Streams/ArrayInputStream.php - ../WebFiori/Cli/Streams/ArrayOutputStream.php - ../WebFiori/Cli/Streams/FileInputStream.php - ../WebFiori/Cli/Streams/FileOutputStream.php - ../WebFiori/Cli/Discovery/CommandDiscovery.php - ../WebFiori/Cli/Discovery/CommandMetadata.php - ../WebFiori/Cli/Discovery/CommandCache.php - ../WebFiori/Cli/Discovery/AutoDiscoverable.php - ../WebFiori/Cli/Exceptions/CommandDiscoveryException.php - ../WebFiori/Cli/Progress/ProgressBar.php - ../WebFiori/Cli/Progress/ProgressBarStyle.php - ../WebFiori/Cli/Progress/ProgressBarFormat.php + ../WebFiori/Cli - + + + ./WebFiori/Tests/Cli diff --git a/tests/phpunit10.xml b/tests/phpunit10.xml index 9f549e3..77a1e2b 100644 --- a/tests/phpunit10.xml +++ b/tests/phpunit10.xml @@ -1,10 +1,15 @@ - + + + + ../WebFiori/Cli + + @@ -13,27 +18,4 @@ ./WebFiori/Tests/Cli - - - ../WebFiori/Cli/Command.php - ../WebFiori/Cli/CommandArgument.php - ../WebFiori/Cli/Formatter.php - ../WebFiori/Cli/KeysMap.php - ../WebFiori/Cli/Runner.php - ../WebFiori/Cli/CommandTestCase.php - ../WebFiori/Cli/InputValidator.php - ../WebFiori/Cli/Streams/ArrayInputStream.php - ../WebFiori/Cli/Streams/ArrayOutputStream.php - ../WebFiori/Cli/Streams/FileInputStream.php - ../WebFiori/Cli/Streams/FileOutputStream.php - ../WebFiori/Cli/Discovery/CommandDiscovery.php - ../WebFiori/Cli/Discovery/CommandMetadata.php - ../WebFiori/Cli/Discovery/CommandCache.php - ../WebFiori/Cli/Discovery/AutoDiscoverable.php - ../WebFiori/Cli/Exceptions/CommandDiscoveryException.php - ../WebFiori/Cli/Progress/ProgressBar.php - ../WebFiori/Cli/Progress/ProgressBarStyle.php - ../WebFiori/Cli/Progress/ProgressBarFormat.php - -