From 6f5bb00dbf598a59dc23bbf93a81676b808ee391 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 26 Sep 2023 07:57:20 +0900 Subject: [PATCH 01/17] feat: add InputOutput and MockInputOutput class --- system/CLI/InputOutput.php | 72 ++++++++++++++ system/Test/Mock/MockInputOutput.php | 139 +++++++++++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 system/CLI/InputOutput.php create mode 100644 system/Test/Mock/MockInputOutput.php diff --git a/system/CLI/InputOutput.php b/system/CLI/InputOutput.php new file mode 100644 index 000000000000..1e28efa4c7cc --- /dev/null +++ b/system/CLI/InputOutput.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\CLI; + +/** + * Input and Output for CLI. + */ +class InputOutput +{ + /** + * Is the readline library on the system? + */ + private bool $readlineSupport; + + public function __construct() + { + // Readline is an extension for PHP that makes interactivity with PHP + // much more bash-like. + // http://www.php.net/manual/en/readline.installation.php + $this->readlineSupport = extension_loaded('readline'); + } + + /** + * Get input from the shell, using readline or the standard STDIN + * + * Named options must be in the following formats: + * php index.php user -v --v -name=John --name=John + * + * @param string|null $prefix You may specify a string with which to prompt the user. + */ + public function input(?string $prefix = null): string + { + // readline() can't be tested. + if ($this->readlineSupport && ENVIRONMENT !== 'testing') { + return readline($prefix); // @codeCoverageIgnore + } + + echo $prefix; + + return fgets(fopen('php://stdin', 'rb')); + } + + /** + * While the library is intended for use on CLI commands, + * commands can be called from controllers and elsewhere + * so we need a way to allow them to still work. + * + * For now, just echo the content, but look into a better + * solution down the road. + * + * @param resource $handle + */ + public function fwrite($handle, string $string): void + { + if (! is_cli()) { + echo $string; + + return; + } + + fwrite($handle, $string); + } +} diff --git a/system/Test/Mock/MockInputOutput.php b/system/Test/Mock/MockInputOutput.php new file mode 100644 index 000000000000..5ad7c47b58fd --- /dev/null +++ b/system/Test/Mock/MockInputOutput.php @@ -0,0 +1,139 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Test\Mock; + +use CodeIgniter\CLI\InputOutput; +use CodeIgniter\Test\Filters\CITestStreamFilter; +use CodeIgniter\Test\PhpStreamWrapper; +use InvalidArgumentException; +use LogicException; + +class MockInputOutput extends InputOutput +{ + /** + * String to be entered by the user. + * + * @var array + * @phpstan-var list + */ + private array $inputs = []; + + /** + * Output lines. + * + * @var array + * @phpstan-var list + */ + private array $outputs = []; + + /** + * Sets user inputs. + * + * @param array $inputs + * @phpstan-param list $inputs + */ + public function setInputs(array $inputs): void + { + $this->inputs = $inputs; + } + + /** + * Gets the item from the output array. + * + * @param int|null $index The output array index. If null, returns all output + * string. If negative int, returns the last $index-th + * item. + */ + public function getOutput(?int $index = null): string + { + if ($index === null) { + return implode('', $this->outputs); + } + + if (array_key_exists($index, $this->outputs)) { + return $this->outputs[$index]; + } + + if ($index < 0) { + $i = count($this->outputs) + $index; + + if (array_key_exists($i, $this->outputs)) { + return $this->outputs[$i]; + } + } + + throw new InvalidArgumentException( + 'No such index in output: ' . $index . ', the last index is: ' + . (count($this->outputs) - 1) + ); + } + + /** + * Returns the outputs array. + */ + public function getOutputs(): array + { + return $this->outputs; + } + + private function addStreamFilters(): void + { + CITestStreamFilter::registration(); + CITestStreamFilter::addOutputFilter(); + CITestStreamFilter::addErrorFilter(); + } + + private function removeStreamFilters(): void + { + CITestStreamFilter::removeOutputFilter(); + CITestStreamFilter::removeErrorFilter(); + } + + public function input(?string $prefix = null): string + { + if ($this->inputs === []) { + throw new LogicException( + 'No input data. Specifiy input data with `MockInputOutput::setInputs()`.' + ); + } + + $input = array_shift($this->inputs); + + $this->addStreamFilters(); + + PhpStreamWrapper::register(); + PhpStreamWrapper::setContent($input); + + $userInput = parent::input($prefix); + $this->outputs[] = CITestStreamFilter::$buffer . $input; + + PhpStreamWrapper::restore(); + + $this->removeStreamFilters(); + + if ($input !== $userInput) { + throw new LogicException($input . '!==' . $userInput); + } + + return $input; + } + + public function fwrite($handle, string $string): void + { + $this->addStreamFilters(); + + parent::fwrite($handle, $string); + $this->outputs[] = CITestStreamFilter::$buffer; + + $this->removeStreamFilters(); + } +} From a6fe7f7b6cac64566fb5a49566e384878c068e90 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 26 Sep 2023 07:58:51 +0900 Subject: [PATCH 02/17] refactor: use InputOutput in CLI class --- system/CLI/CLI.php | 57 +++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index ec30e9a55b56..f178fceeeb2d 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -43,7 +43,7 @@ class CLI * * @var bool * - * @deprecated 4.4.2 Should be protected. + * @deprecated 4.4.2 Should be protected, and no longer used. * @TODO Fix to camelCase in the next major version. */ public static $readline_support = false; @@ -152,6 +152,11 @@ class CLI */ protected static $isColored = false; + /** + * Input and Output for CLI. + */ + protected static ?InputOutput $io = null; + /** * Static "constructor". * @@ -181,6 +186,8 @@ public static function init() // For "! defined('STDOUT')" see: https://github.com/codeigniter4/CodeIgniter4/issues/7047 define('STDOUT', 'php://output'); // @codeCoverageIgnore } + + static::resetInputOutput(); } /** @@ -193,14 +200,7 @@ public static function init() */ public static function input(?string $prefix = null): string { - // readline() can't be tested. - if (static::$readline_support && ENVIRONMENT !== 'testing') { - return readline($prefix); // @codeCoverageIgnore - } - - echo $prefix; - - return fgets(fopen('php://stdin', 'rb')); + return static::$io->input($prefix); } /** @@ -225,8 +225,6 @@ public static function input(?string $prefix = null): string * @param array|string|null $validation Validation rules * * @return string The user input - * - * @codeCoverageIgnore */ public static function prompt(string $field, $options = null, $validation = null): string { @@ -265,7 +263,7 @@ public static function prompt(string $field, $options = null, $validation = null static::fwrite(STDOUT, $field . (trim($field) ? ' ' : '') . $extraOutput . ': '); // Read the input from keyboard. - $input = trim(static::input()) ?: $default; + $input = trim(static::$io->input()) ?: $default; if ($validation !== []) { while (! static::validate('"' . trim($field) . '"', $input, $validation)) { @@ -285,8 +283,6 @@ public static function prompt(string $field, $options = null, $validation = null * @param array|string|null $validation Validation rules * * @return string The selected key of $options - * - * @codeCoverageIgnore */ public static function promptByKey($text, array $options, $validation = null): string { @@ -415,8 +411,6 @@ private static function printKeysAndValues(array $options): void * @param string $field Prompt "field" output * @param string $value Input value * @param array|string $rules Validation rules - * - * @codeCoverageIgnore */ protected static function validate(string $field, string $value, $rules): bool { @@ -533,11 +527,8 @@ public static function wait(int $seconds, bool $countdown = false) } elseif ($seconds > 0) { sleep($seconds); } else { - // this chunk cannot be tested because of keyboard input - // @codeCoverageIgnoreStart static::write(static::$wait_msg); - static::input(); - // @codeCoverageIgnoreEnd + static::$io->input(); } } @@ -567,8 +558,6 @@ public static function newLine(int $num = 1) /** * Clears the screen of output * - * @codeCoverageIgnore - * * @return void */ public static function clearScreen() @@ -762,8 +751,6 @@ public static function getHeight(int $default = 32): int /** * Populates the CLI's dimensions. * - * @codeCoverageIgnore - * * @return void */ public static function generateDimensions() @@ -1137,15 +1124,23 @@ public static function table(array $tbody, array $thead = []) */ protected static function fwrite($handle, string $string) { - if (! is_cli()) { - // @codeCoverageIgnoreStart - echo $string; + static::$io->fwrite($handle, $string); + } - return; - // @codeCoverageIgnoreEnd - } + /** + * Testing purpose only + */ + public static function setInputOutput(InputOutput $io): void + { + static::$io = $io; + } - fwrite($handle, $string); + /** + * Testing purpose only + */ + public static function resetInputOutput(): void + { + static::$io = new InputOutput(); } } From 5173b3860583b21e90999bc222041fade7ca4036 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 26 Sep 2023 08:12:35 +0900 Subject: [PATCH 03/17] test: add test for user input in CLI --- .../Database/ShowTableInfoMockIOTest.php | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/system/Commands/Database/ShowTableInfoMockIOTest.php diff --git a/tests/system/Commands/Database/ShowTableInfoMockIOTest.php b/tests/system/Commands/Database/ShowTableInfoMockIOTest.php new file mode 100644 index 000000000000..b06f877ec487 --- /dev/null +++ b/tests/system/Commands/Database/ShowTableInfoMockIOTest.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands\Database; + +use CodeIgniter\CLI\CLI; +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\DatabaseTestTrait; +use CodeIgniter\Test\Mock\MockInputOutput; + +/** + * @group DatabaseLive + * + * @internal + */ +final class ShowTableInfoMockIOTest extends CIUnitTestCase +{ + use DatabaseTestTrait; + + protected $migrateOnce = true; + + protected function setUp(): void + { + parent::setUp(); + + putenv('NO_COLOR=1'); + CLI::init(); + } + + protected function tearDown(): void + { + parent::tearDown(); + + putenv('NO_COLOR'); + CLI::init(); + } + + public function testDbTableWithInputs(): void + { + // Set MockInputOutput to CLI. + $io = new MockInputOutput(); + CLI::setInputOutput($io); + + // User will input "a\n" and "0\n". + $io->setInputs(["a\n", "0\n"]); + + command('db:table'); + + $result = $io->getOutput(); + + $expected = 'Data of Table "db_migrations":'; + $this->assertStringContainsString($expected, $result); + + $expectedPattern = '/\| id[[:blank:]]+\| version[[:blank:]]+\| class[[:blank:]]+\| group[[:blank:]]+\| namespace[[:blank:]]+\| time[[:blank:]]+\| batch \|/'; + $this->assertMatchesRegularExpression($expectedPattern, $result); + + // Remove MockInputOutput. + CLI::resetInputOutput(); + } +} From 099e81624f549dca235a5e1636b1c212e4fea005 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 26 Sep 2023 08:16:43 +0900 Subject: [PATCH 04/17] docs: remove outdated comment --- system/CLI/CLI.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index f178fceeeb2d..44e2e494c65f 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -27,13 +27,6 @@ * possible to test using travis-ci. It has been phpunit-annotated * to prevent messing up code coverage. * - * Some of the methods require keyboard input, and are not unit-testable - * as a result: input() and prompt(). - * validate() is internal, and not testable if prompt() isn't. - * The wait() method is mostly testable, as long as you don't give it - * an argument of "0". - * These have been flagged to ignore for code coverage purposes. - * * @see \CodeIgniter\CLI\CLITest */ class CLI From d5fc5e75aaee9c7b200ebbf8f09e88454f076910 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 26 Sep 2023 08:50:35 +0900 Subject: [PATCH 05/17] fix: MockInputOutput output readline() does not return the last "\n", but fgets() returns the last "\n". --- system/Test/Mock/MockInputOutput.php | 2 +- .../system/Commands/Database/ShowTableInfoMockIOTest.php | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/system/Test/Mock/MockInputOutput.php b/system/Test/Mock/MockInputOutput.php index 5ad7c47b58fd..23c18e4a7cb2 100644 --- a/system/Test/Mock/MockInputOutput.php +++ b/system/Test/Mock/MockInputOutput.php @@ -114,7 +114,7 @@ public function input(?string $prefix = null): string PhpStreamWrapper::setContent($input); $userInput = parent::input($prefix); - $this->outputs[] = CITestStreamFilter::$buffer . $input; + $this->outputs[] = CITestStreamFilter::$buffer . $input . PHP_EOL; PhpStreamWrapper::restore(); diff --git a/tests/system/Commands/Database/ShowTableInfoMockIOTest.php b/tests/system/Commands/Database/ShowTableInfoMockIOTest.php index b06f877ec487..1337139962eb 100644 --- a/tests/system/Commands/Database/ShowTableInfoMockIOTest.php +++ b/tests/system/Commands/Database/ShowTableInfoMockIOTest.php @@ -49,13 +49,17 @@ public function testDbTableWithInputs(): void $io = new MockInputOutput(); CLI::setInputOutput($io); - // User will input "a\n" and "0\n". - $io->setInputs(["a\n", "0\n"]); + // User will input "a" (invalid value) and "0". + $io->setInputs(['a', '0']); command('db:table'); $result = $io->getOutput(); + $expected = 'Which table do you want to see? [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]: a +The Which table do you want to see? field must be one of: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9.'; + $this->assertStringContainsString($expected, $result); + $expected = 'Data of Table "db_migrations":'; $this->assertStringContainsString($expected, $result); From 85ad0c90dc87b09b3acadbb03102089cac0cb345 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 26 Sep 2023 10:26:05 +0900 Subject: [PATCH 06/17] test: fix assertion The number of tables may vary. --- tests/system/Commands/Database/ShowTableInfoMockIOTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/system/Commands/Database/ShowTableInfoMockIOTest.php b/tests/system/Commands/Database/ShowTableInfoMockIOTest.php index 1337139962eb..911a53d68b36 100644 --- a/tests/system/Commands/Database/ShowTableInfoMockIOTest.php +++ b/tests/system/Commands/Database/ShowTableInfoMockIOTest.php @@ -56,9 +56,9 @@ public function testDbTableWithInputs(): void $result = $io->getOutput(); - $expected = 'Which table do you want to see? [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]: a -The Which table do you want to see? field must be one of: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9.'; - $this->assertStringContainsString($expected, $result); + $expectedPattern = '/Which table do you want to see\? \[0, 1, 2, 3, 4, 5, 6, 7, 8, 9.*?\]: a +The Which table do you want to see\? field must be one of: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9.*?./'; + $this->assertMatchesRegularExpression($expectedPattern, $result); $expected = 'Data of Table "db_migrations":'; $this->assertStringContainsString($expected, $result); From 1e78628b013b7954014c1e1d0513c5cbe6f38ff1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 26 Sep 2023 12:40:02 +0900 Subject: [PATCH 07/17] docs: move Testing CLI to a new page --- user_guide_src/source/testing/cli.rst | 86 ++++++++++++++++++++++ user_guide_src/source/testing/index.rst | 1 + user_guide_src/source/testing/overview.rst | 83 --------------------- 3 files changed, 87 insertions(+), 83 deletions(-) create mode 100644 user_guide_src/source/testing/cli.rst diff --git a/user_guide_src/source/testing/cli.rst b/user_guide_src/source/testing/cli.rst new file mode 100644 index 000000000000..b9b24acaf32e --- /dev/null +++ b/user_guide_src/source/testing/cli.rst @@ -0,0 +1,86 @@ +#################### +Testing CLI Commands +#################### + +.. _testing-cli-output: + +Testing CLI Output +================== + +StreamFilterTrait +----------------- + +.. versionadded:: 4.3.0 + +**StreamFilterTrait** provides an alternate to these helper methods. + +You may need to test things that are difficult to test. Sometimes, capturing a stream, like PHP's own STDOUT, or STDERR, +might be helpful. The ``StreamFilterTrait`` helps you capture the output from the stream of your choice. + +**Overview of methods** + +- ``StreamFilterTrait::getStreamFilterBuffer()`` Get the captured data from the buffer. +- ``StreamFilterTrait::resetStreamFilterBuffer()`` Reset captured data. + +An example demonstrating this inside one of your test cases: + +.. literalinclude:: overview/018.php + +The ``StreamFilterTrait`` has a configurator that is called automatically. +See :ref:`Testing Traits `. + +If you override the ``setUp()`` or ``tearDown()`` methods in your test, then you must call the ``parent::setUp()`` and +``parent::tearDown()`` methods respectively to configure the ``StreamFilterTrait``. + +CITestStreamFilter +------------------ + +**CITestStreamFilter** for manual/single use. + +If you need to capture streams in only one test, then instead of using the StreamFilterTrait trait, you can manually +add a filter to streams. + +**Overview of methods** + +- ``CITestStreamFilter::registration()`` Filter registration. +- ``CITestStreamFilter::addOutputFilter()`` Adding a filter to the output stream. +- ``CITestStreamFilter::addErrorFilter()`` Adding a filter to the error stream. +- ``CITestStreamFilter::removeOutputFilter()`` Removing a filter from the output stream. +- ``CITestStreamFilter::removeErrorFilter()`` Removing a filter from the error stream. + +.. literalinclude:: overview/020.php + +.. _testing-cli-input: + +Testing CLI Input +================= + +PhpStreamWrapper +---------------- + +.. versionadded:: 4.3.0 + +**PhpStreamWrapper** provides a way to write tests for methods that require user input, +such as ``CLI::prompt()``, ``CLI::wait()``, and ``CLI::input()``. + +.. note:: The PhpStreamWrapper is a stream wrapper class. + If you don't know PHP's stream wrapper, + see `The streamWrapper class `_ + in the PHP maual. + +**Overview of methods** + +- ``PhpStreamWrapper::register()`` Register the ``PhpStreamWrapper`` to the ``php`` protocol. +- ``PhpStreamWrapper::restore()`` Restore the php protocol wrapper back to the PHP built-in wrapper. +- ``PhpStreamWrapper::setContent()`` Set the input data. + +.. important:: The PhpStreamWrapper is intended for only testing ``php://stdin``. + But when you register it, it handles all the `php protocol `_ streams, + such as ``php://stdout``, ``php://stderr``, ``php://memory``. + So it is strongly recommended that ``PhpStreamWrapper`` be registered/unregistered + only when needed. Otherwise, it will interfere with other built-in php streams + while registered. + +An example demonstrating this inside one of your test cases: + +.. literalinclude:: overview/019.php diff --git a/user_guide_src/source/testing/index.rst b/user_guide_src/source/testing/index.rst index 949a68b99b51..6d7ded362af4 100644 --- a/user_guide_src/source/testing/index.rst +++ b/user_guide_src/source/testing/index.rst @@ -14,6 +14,7 @@ The following sections should get you quickly testing your applications. Controller Testing HTTP Testing response + cli benchmark debugging Mocking diff --git a/user_guide_src/source/testing/overview.rst b/user_guide_src/source/testing/overview.rst index d969f73cdc41..d7d03faceeeb 100644 --- a/user_guide_src/source/testing/overview.rst +++ b/user_guide_src/source/testing/overview.rst @@ -266,86 +266,3 @@ component name: .. literalinclude:: overview/017.php .. note:: All component Factories are reset by default between each test. Modify your test case's ``$setUpMethods`` if you need instances to persist. - -.. _testing-cli-output: - -Testing CLI Output -================== - -StreamFilterTrait ------------------ - -.. versionadded:: 4.3.0 - -**StreamFilterTrait** provides an alternate to these helper methods. - -You may need to test things that are difficult to test. Sometimes, capturing a stream, like PHP's own STDOUT, or STDERR, -might be helpful. The ``StreamFilterTrait`` helps you capture the output from the stream of your choice. - -**Overview of methods** - -- ``StreamFilterTrait::getStreamFilterBuffer()`` Get the captured data from the buffer. -- ``StreamFilterTrait::resetStreamFilterBuffer()`` Reset captured data. - -An example demonstrating this inside one of your test cases: - -.. literalinclude:: overview/018.php - -The ``StreamFilterTrait`` has a configurator that is called automatically. -See :ref:`Testing Traits `. - -If you override the ``setUp()`` or ``tearDown()`` methods in your test, then you must call the ``parent::setUp()`` and -``parent::tearDown()`` methods respectively to configure the ``StreamFilterTrait``. - -CITestStreamFilter ------------------- - -**CITestStreamFilter** for manual/single use. - -If you need to capture streams in only one test, then instead of using the StreamFilterTrait trait, you can manually -add a filter to streams. - -**Overview of methods** - -- ``CITestStreamFilter::registration()`` Filter registration. -- ``CITestStreamFilter::addOutputFilter()`` Adding a filter to the output stream. -- ``CITestStreamFilter::addErrorFilter()`` Adding a filter to the error stream. -- ``CITestStreamFilter::removeOutputFilter()`` Removing a filter from the output stream. -- ``CITestStreamFilter::removeErrorFilter()`` Removing a filter from the error stream. - -.. literalinclude:: overview/020.php - -.. _testing-cli-input: - -Testing CLI Input -================= - -PhpStreamWrapper ----------------- - -.. versionadded:: 4.3.0 - -**PhpStreamWrapper** provides a way to write tests for methods that require user input, -such as ``CLI::prompt()``, ``CLI::wait()``, and ``CLI::input()``. - -.. note:: The PhpStreamWrapper is a stream wrapper class. - If you don't know PHP's stream wrapper, - see `The streamWrapper class `_ - in the PHP maual. - -**Overview of methods** - -- ``PhpStreamWrapper::register()`` Register the ``PhpStreamWrapper`` to the ``php`` protocol. -- ``PhpStreamWrapper::restore()`` Restore the php protocol wrapper back to the PHP built-in wrapper. -- ``PhpStreamWrapper::setContent()`` Set the input data. - -.. important:: The PhpStreamWrapper is intended for only testing ``php://stdin``. - But when you register it, it handles all the `php protocol `_ streams, - such as ``php://stdout``, ``php://stderr``, ``php://memory``. - So it is strongly recommended that ``PhpStreamWrapper`` be registered/unregistered - only when needed. Otherwise, it will interfere with other built-in php streams - while registered. - -An example demonstrating this inside one of your test cases: - -.. literalinclude:: overview/019.php From 53c76b68399ad861b3ac694b0bdb1d3acfdb3414 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 26 Sep 2023 13:16:33 +0900 Subject: [PATCH 08/17] docs: add docs --- user_guide_src/source/changelogs/v4.5.0.rst | 3 + user_guide_src/source/testing/cli.rst | 79 +++++++++++++++++++-- user_guide_src/source/testing/cli/001.php | 37 ++++++++++ 3 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 user_guide_src/source/testing/cli/001.php diff --git a/user_guide_src/source/changelogs/v4.5.0.rst b/user_guide_src/source/changelogs/v4.5.0.rst index 044e0f6d8cca..d65c751080e6 100644 --- a/user_guide_src/source/changelogs/v4.5.0.rst +++ b/user_guide_src/source/changelogs/v4.5.0.rst @@ -183,6 +183,9 @@ Testing - **DomParser:** The new methods were added ``seeXPath()`` and ``dontSeeXPath()`` which allows users to work directly with DOMXPath object, using complex expressions. +- **CLI:** The new ``InputOutput`` class was added and now you can write test + for commands more easily if you use ``MockInputOutput``. + See :ref:`using-mock-input-output`. Database ======== diff --git a/user_guide_src/source/testing/cli.rst b/user_guide_src/source/testing/cli.rst index b9b24acaf32e..be9d9845325a 100644 --- a/user_guide_src/source/testing/cli.rst +++ b/user_guide_src/source/testing/cli.rst @@ -2,11 +2,73 @@ Testing CLI Commands #################### +.. contents:: + :local: + :depth: 3 + +.. _using-mock-input-output: + +********************* +Using MockInputOutput +********************* + +.. versionadded:: 4.5.0 + +MockInputOutput +=============== + +**MockInputOutput** provides a esay way to write tests for commands that require +user input, such as ``CLI::prompt()``, ``CLI::wait()``, and ``CLI::input()``. + +You can replace the ``InputOutput`` class with ``MockInputOutput`` during test +execution to capture inputs and outputs. + +.. note:: When you use ``MockInputOutput``, you don't need to use + :ref:`stream-filter-trait`, :ref:`ci-test-stream-filter`, and + :ref:`php-stream-wrapper`. + +Helper Methods +--------------- + +getOutput(?int $index = null): string +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Gets the output. + +- If you call it like ``$io->getOutput()``, it returns the whole output string. +- If you specify ``0`` or a positive number, it returns the output array item. + Each item has the output of a ``CLI::fwrite()`` call. +- If you specify a negative number ``-n``, it returns the last ``n``-th item of + the output array. + +getOutputs(): array +^^^^^^^^^^^^^^^^^^^ + +Returns the output array. Each item has the output of a ``CLI::fwrite()`` call. + +How to Use +========== + +- ``CLI::setInputOutput()`` can set the ``MockInputOutput`` instance to the ``CLI`` class. +- ``CLI::resetInputOutput()`` resets the ``InputOutput`` instance in the ``CLI`` class. +- ``MockInputOutput::setInputs()`` sets the user input array. +- ``MockInputOutput::getOutput()`` gets the command output. + +The following test code is to test the command ``spark db:table``: + +.. literalinclude:: cli/001.php + +*********************** +Without MockInputOutput +*********************** + .. _testing-cli-output: Testing CLI Output ================== +.. _stream-filter-trait: + StreamFilterTrait ----------------- @@ -17,10 +79,11 @@ StreamFilterTrait You may need to test things that are difficult to test. Sometimes, capturing a stream, like PHP's own STDOUT, or STDERR, might be helpful. The ``StreamFilterTrait`` helps you capture the output from the stream of your choice. -**Overview of methods** +How to Use +^^^^^^^^^^ -- ``StreamFilterTrait::getStreamFilterBuffer()`` Get the captured data from the buffer. -- ``StreamFilterTrait::resetStreamFilterBuffer()`` Reset captured data. +- ``StreamFilterTrait::getStreamFilterBuffer()`` gets the captured data from the buffer. +- ``StreamFilterTrait::resetStreamFilterBuffer()`` resets captured data. An example demonstrating this inside one of your test cases: @@ -32,6 +95,8 @@ See :ref:`Testing Traits `. If you override the ``setUp()`` or ``tearDown()`` methods in your test, then you must call the ``parent::setUp()`` and ``parent::tearDown()`` methods respectively to configure the ``StreamFilterTrait``. +.. _ci-test-stream-filter: + CITestStreamFilter ------------------ @@ -40,7 +105,8 @@ CITestStreamFilter If you need to capture streams in only one test, then instead of using the StreamFilterTrait trait, you can manually add a filter to streams. -**Overview of methods** +How to Use +^^^^^^^^^^ - ``CITestStreamFilter::registration()`` Filter registration. - ``CITestStreamFilter::addOutputFilter()`` Adding a filter to the output stream. @@ -55,6 +121,8 @@ add a filter to streams. Testing CLI Input ================= +.. _php-stream-wrapper: + PhpStreamWrapper ---------------- @@ -68,7 +136,8 @@ such as ``CLI::prompt()``, ``CLI::wait()``, and ``CLI::input()``. see `The streamWrapper class `_ in the PHP maual. -**Overview of methods** +How to Use +^^^^^^^^^^ - ``PhpStreamWrapper::register()`` Register the ``PhpStreamWrapper`` to the ``php`` protocol. - ``PhpStreamWrapper::restore()`` Restore the php protocol wrapper back to the PHP built-in wrapper. diff --git a/user_guide_src/source/testing/cli/001.php b/user_guide_src/source/testing/cli/001.php new file mode 100644 index 000000000000..c01212049b8c --- /dev/null +++ b/user_guide_src/source/testing/cli/001.php @@ -0,0 +1,37 @@ +setInputs(['a', '0']); + + command('db:table'); + + // Get the whole output string. + $output = $io->getOutput(); + + $expected = 'Which table do you want to see? [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]: a'; + $this->assertStringContainsString($expected, $output); + + $expected = 'Data of Table "db_migrations":'; + $this->assertStringContainsString($expected, $output); + + // Remove MockInputOutput. + CLI::resetInputOutput(); + } +} From 8014b6be6b8f156b657116e9351f2fd18d28ef56 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 13 Oct 2023 16:54:35 +0900 Subject: [PATCH 09/17] test: update assertion --- tests/system/Commands/Database/ShowTableInfoMockIOTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/Commands/Database/ShowTableInfoMockIOTest.php b/tests/system/Commands/Database/ShowTableInfoMockIOTest.php index 911a53d68b36..35b2d442cd10 100644 --- a/tests/system/Commands/Database/ShowTableInfoMockIOTest.php +++ b/tests/system/Commands/Database/ShowTableInfoMockIOTest.php @@ -57,7 +57,7 @@ public function testDbTableWithInputs(): void $result = $io->getOutput(); $expectedPattern = '/Which table do you want to see\? \[0, 1, 2, 3, 4, 5, 6, 7, 8, 9.*?\]: a -The Which table do you want to see\? field must be one of: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9.*?./'; +The "Which table do you want to see\?" field must be one of: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9.*?./'; $this->assertMatchesRegularExpression($expectedPattern, $result); $expected = 'Data of Table "db_migrations":'; From 9ff8db287ad11bc6583389b9991ecd3944cd0e4c Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 2 Nov 2023 08:44:28 +0900 Subject: [PATCH 10/17] docs: fix by proofreading Co-authored-by: Michal Sniatala --- user_guide_src/source/testing/cli.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/testing/cli.rst b/user_guide_src/source/testing/cli.rst index be9d9845325a..ace1f12419e8 100644 --- a/user_guide_src/source/testing/cli.rst +++ b/user_guide_src/source/testing/cli.rst @@ -17,7 +17,7 @@ Using MockInputOutput MockInputOutput =============== -**MockInputOutput** provides a esay way to write tests for commands that require +**MockInputOutput** provides an easy way to write tests for commands that require user input, such as ``CLI::prompt()``, ``CLI::wait()``, and ``CLI::input()``. You can replace the ``InputOutput`` class with ``MockInputOutput`` during test From 086ca7f1edbff214e027730232cc7a6c9b9c2323 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 5 Nov 2023 09:05:15 +0900 Subject: [PATCH 11/17] refactor: add final to MockInputOutput Co-authored-by: MGatner --- system/Test/Mock/MockInputOutput.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Test/Mock/MockInputOutput.php b/system/Test/Mock/MockInputOutput.php index 23c18e4a7cb2..aa751d75564b 100644 --- a/system/Test/Mock/MockInputOutput.php +++ b/system/Test/Mock/MockInputOutput.php @@ -17,7 +17,7 @@ use InvalidArgumentException; use LogicException; -class MockInputOutput extends InputOutput +final class MockInputOutput extends InputOutput { /** * String to be entered by the user. From 835462d7e47f71dfd73a75a6056b4dce19626bfd Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 5 Nov 2023 09:05:42 +0900 Subject: [PATCH 12/17] refactor: add strict_types Co-authored-by: MGatner --- system/Test/Mock/MockInputOutput.php | 1 + 1 file changed, 1 insertion(+) diff --git a/system/Test/Mock/MockInputOutput.php b/system/Test/Mock/MockInputOutput.php index aa751d75564b..01f1a9a4fe05 100644 --- a/system/Test/Mock/MockInputOutput.php +++ b/system/Test/Mock/MockInputOutput.php @@ -1,4 +1,5 @@ Date: Sun, 5 Nov 2023 09:06:35 +0900 Subject: [PATCH 13/17] docs: native @var tag Co-authored-by: MGatner --- system/Test/Mock/MockInputOutput.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/system/Test/Mock/MockInputOutput.php b/system/Test/Mock/MockInputOutput.php index 01f1a9a4fe05..512680544edb 100644 --- a/system/Test/Mock/MockInputOutput.php +++ b/system/Test/Mock/MockInputOutput.php @@ -23,8 +23,7 @@ final class MockInputOutput extends InputOutput /** * String to be entered by the user. * - * @var array - * @phpstan-var list + * @var list */ private array $inputs = []; From 962777ecc6dc11937c6843a61f3bb244fbe3a49f Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 5 Nov 2023 09:07:08 +0900 Subject: [PATCH 14/17] docs: fix by proofreading Co-authored-by: MGatner --- user_guide_src/source/changelogs/v4.5.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.5.0.rst b/user_guide_src/source/changelogs/v4.5.0.rst index d65c751080e6..0169152d0edf 100644 --- a/user_guide_src/source/changelogs/v4.5.0.rst +++ b/user_guide_src/source/changelogs/v4.5.0.rst @@ -183,7 +183,7 @@ Testing - **DomParser:** The new methods were added ``seeXPath()`` and ``dontSeeXPath()`` which allows users to work directly with DOMXPath object, using complex expressions. -- **CLI:** The new ``InputOutput`` class was added and now you can write test +- **CLI:** The new ``InputOutput`` class was added and now you can write tests for commands more easily if you use ``MockInputOutput``. See :ref:`using-mock-input-output`. From 2ef3169ffd25f95c265654e2ceaf29c01c1afaa6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 5 Nov 2023 09:11:03 +0900 Subject: [PATCH 15/17] docs: add @testTag --- system/CLI/CLI.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 44e2e494c65f..27ea440703cc 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -1122,6 +1122,8 @@ protected static function fwrite($handle, string $string) /** * Testing purpose only + * + * @testTag */ public static function setInputOutput(InputOutput $io): void { @@ -1130,6 +1132,8 @@ public static function setInputOutput(InputOutput $io): void /** * Testing purpose only + * + * @testTag */ public static function resetInputOutput(): void { From 6767da77f693dad6ea16ab535f08cc092da6dff4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 5 Nov 2023 09:11:20 +0900 Subject: [PATCH 16/17] refactor: add declare(strict_types=1) to new class --- system/CLI/InputOutput.php | 2 ++ system/Test/Mock/MockInputOutput.php | 1 + 2 files changed, 3 insertions(+) diff --git a/system/CLI/InputOutput.php b/system/CLI/InputOutput.php index 1e28efa4c7cc..e71dddc79f59 100644 --- a/system/CLI/InputOutput.php +++ b/system/CLI/InputOutput.php @@ -1,5 +1,7 @@ Date: Sun, 5 Nov 2023 16:53:01 +0900 Subject: [PATCH 17/17] fix: TypeError in InputOutput::input() --- system/CLI/InputOutput.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/system/CLI/InputOutput.php b/system/CLI/InputOutput.php index e71dddc79f59..5b6c1da02f4d 100644 --- a/system/CLI/InputOutput.php +++ b/system/CLI/InputOutput.php @@ -48,7 +48,13 @@ public function input(?string $prefix = null): string echo $prefix; - return fgets(fopen('php://stdin', 'rb')); + $input = fgets(fopen('php://stdin', 'rb')); + + if ($input === false) { + $input = ''; + } + + return $input; } /**