From 6536f5c99a54ff6caed1089dd760a0ca2011348b Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 3 Aug 2022 11:52:51 +0900 Subject: [PATCH 1/6] feat: add StreamWrapper for php protocol --- system/Test/PhpStreamWrapper.php | 70 ++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 system/Test/PhpStreamWrapper.php diff --git a/system/Test/PhpStreamWrapper.php b/system/Test/PhpStreamWrapper.php new file mode 100644 index 000000000000..3d0a9c806892 --- /dev/null +++ b/system/Test/PhpStreamWrapper.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Test; + +/** + * StreamWrapper for php protocol + * + * This class is used for mocking `php://stdin`. + * + * See https://www.php.net/manual/en/class.streamwrapper.php + */ +class PhpStreamWrapper +{ + private static string $content = ''; + private int $position = 0; + + public static function setContent(string $content) + { + self::$content = $content; + } + + public static function register() + { + stream_wrapper_unregister('php'); + stream_wrapper_register('php', self::class); + } + + public static function restore() + { + stream_wrapper_restore('php'); + } + + public function stream_open(string $path): bool + { + return true; + } + + /** + * @return false|string + */ + public function stream_read(int $count) + { + $return = substr(self::$content, $this->position, $count); + $this->position += strlen($return); + + return $return; + } + + /** + * @return array|false + */ + public function stream_stat() + { + return []; + } + + public function stream_eof(): bool + { + return $this->position >= strlen(self::$content); + } +} From e311426a997ffd1fda162aaca6f3cdf130b519c7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 3 Aug 2022 11:55:33 +0900 Subject: [PATCH 2/6] feat: make CLI::input() testable --- system/CLI/CLI.php | 9 ++++----- tests/system/CLI/CLITest.php | 29 ++++++++++++++--------------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 7a5030314948..dabd9c1091b1 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -175,18 +175,17 @@ public static function init() * php index.php user -v --v -name=John --name=John * * @param string $prefix - * - * @codeCoverageIgnore */ public static function input(?string $prefix = null): string { - if (static::$readline_support) { - return readline($prefix); + // readline() can't be tested. + if (static::$readline_support && ENVIRONMENT !== 'testing') { + return readline($prefix); // @codeCoverageIgnore } echo $prefix; - return fgets(STDIN); + return fgets(fopen('php://stdin', 'rb')); } /** diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index da4524ae47a8..66ecf369d55a 100644 --- a/tests/system/CLI/CLITest.php +++ b/tests/system/CLI/CLITest.php @@ -12,6 +12,7 @@ namespace CodeIgniter\CLI; use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\PhpStreamWrapper; use CodeIgniter\Test\StreamFilterTrait; use ReflectionProperty; use RuntimeException; @@ -59,22 +60,20 @@ public function testWait() $time = time(); CLI::wait(1); $this->assertCloseEnough(1, time() - $time); + } + + public function testWaitZero() + { + PhpStreamWrapper::register(); + PhpStreamWrapper::setContent(' '); + + // test the press any key to continue... + $time = time(); + CLI::wait(0); + + $this->assertSame(0, time() - $time); - // Leaving the code fragment below in, to remind myself (or others) - // of what appears to be the most likely path to test this last - // bit of wait() functionality. - // The problem: if the block below is enabled, the phpunit tests - // go catatonic when it is executed, presumably because of - // the CLI::input() waiting for a key press - // - // // test the press any key to continue... - // stream_filter_register('CLITestKeyboardFilter', 'CodeIgniter\CLI\CLITestKeyboardFilter'); - // $spoofer = stream_filter_append(STDIN, 'CLITestKeyboardFilter'); - // $time = time(); - // CLITestKeyboardFilter::$spoofed = ' '; - // CLI::wait(0); - // stream_filter_remove($spoofer); - // $this->assertEquals(0, time() - $time); + PhpStreamWrapper::restore(); } public function testIsWindows() From d787fe666a1882803c8acf3ee90ed9e5fa8285dc Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 3 Aug 2022 12:11:52 +0900 Subject: [PATCH 3/6] test: add test for CLI::prompt() --- tests/system/CLI/CLITest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index 66ecf369d55a..225a7dfa259e 100644 --- a/tests/system/CLI/CLITest.php +++ b/tests/system/CLI/CLITest.php @@ -76,6 +76,20 @@ public function testWaitZero() PhpStreamWrapper::restore(); } + public function testPrompt() + { + PhpStreamWrapper::register(); + + $expected = 'red'; + PhpStreamWrapper::setContent($expected); + + $output = CLI::prompt('What is your favorite color?'); + + $this->assertSame($expected, $output); + + PhpStreamWrapper::restore(); + } + public function testIsWindows() { $this->assertSame(('\\' === DIRECTORY_SEPARATOR), CLI::isWindows()); From 20056d04590d4191076acc88177b4280ce192638 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 4 Aug 2022 08:58:46 +0900 Subject: [PATCH 4/6] docs: add user guide --- user_guide_src/source/changelogs/v4.3.0.rst | 3 ++- user_guide_src/source/cli/cli_library.rst | 6 +++++ user_guide_src/source/testing/overview.rst | 26 ++++++++++++++++--- .../source/testing/overview/019.php | 25 ++++++++++++++++++ 4 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 user_guide_src/source/testing/overview/019.php diff --git a/user_guide_src/source/changelogs/v4.3.0.rst b/user_guide_src/source/changelogs/v4.3.0.rst index 3a429f6bc36b..0b5ae39ac4fb 100644 --- a/user_guide_src/source/changelogs/v4.3.0.rst +++ b/user_guide_src/source/changelogs/v4.3.0.rst @@ -33,7 +33,8 @@ Others Enhancements ************ -- Added the ``StreamFilterTrait`` to make it easier to work with capturing data from STDOUT and STDERR streams. See :ref:`testing-overview-stream-filters`. +- Added the ``StreamFilterTrait`` to make it easier to work with capturing data from STDOUT and STDERR streams. See :ref:`testing-cli-output`. +- Added the ``PhpStreamWrapper`` to make it easier to work with setting data to ``php://stdin``. See :ref:`testing-cli-input`. - Added before and after events to ``BaseModel::insertBatch()`` and ``BaseModel::updateBatch()`` methods. See :ref:`model-events-callbacks`. - Added ``Model::allowEmptyInserts()`` method to insert empty data. See :ref:`Using CodeIgniter's Model ` - Added ``$routes->useSupportedLocalesOnly(true)`` so that the Router returns 404 Not Found if the locale in the URL is not supported in ``Config\App::$supportedLocales``. See :ref:`Localization ` diff --git a/user_guide_src/source/cli/cli_library.rst b/user_guide_src/source/cli/cli_library.rst index 29bc6b252f88..b81584c25f43 100644 --- a/user_guide_src/source/cli/cli_library.rst +++ b/user_guide_src/source/cli/cli_library.rst @@ -31,6 +31,12 @@ Sometimes you need to ask the user for more information. They might not have pro arguments, or the script may have encountered an existing file and needs confirmation before overwriting. This is handled with the ``prompt()`` or ``promptByKey()`` method. +.. note:: Since v4.3.0, you can write tests for these methods with ``PhpStreamWrapper``. + See :ref:`testing-cli-input`. + +prompt() +======== + You can provide a question by passing it in as the first parameter: .. literalinclude:: cli_library/002.php diff --git a/user_guide_src/source/testing/overview.rst b/user_guide_src/source/testing/overview.rst index 54e874b8dbaf..b2cef40c80ef 100644 --- a/user_guide_src/source/testing/overview.rst +++ b/user_guide_src/source/testing/overview.rst @@ -252,10 +252,10 @@ component name: .. note:: All component Factories are reset by default between each test. Modify your test case's ``$setUpMethods`` if you need instances to persist. -.. _testing-overview-stream-filters: +.. _testing-cli-output: -Stream Filters -============== +Testing CLI Output +================== **StreamFilterTrait** provides an alternate to these helper methods. @@ -276,3 +276,23 @@ 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``. + +.. _testing-cli-input: + +Testing CLI Input +================= + +**PhpStreamWrapper** provides a way to write tests for methods that require user input, +such as ``CLI::prompt()``, ``CLI::wait()``, and ``CLI::input()``. + +**Overview of methods** + +- ``PhpStreamWrapper::register()`` Register the ``PhpStreamWrapper`` to php protocol. +- ``PhpStreamWrapper::restore()`` Restore the php protocol wrapper back to the PHP built-in wrapper. +- ``PhpStreamWrapper::setContent()`` Set the input data. + +An example demonstrating this inside one of your test cases: + +.. literalinclude:: overview/019.php + +.. note:: It is strongly recommended that ``PhpStreamWrapper`` be registered/unregistered only when needed. Otherwise, it will interfere with other built-in php streams while registered. diff --git a/user_guide_src/source/testing/overview/019.php b/user_guide_src/source/testing/overview/019.php new file mode 100644 index 000000000000..96c2d2e73231 --- /dev/null +++ b/user_guide_src/source/testing/overview/019.php @@ -0,0 +1,25 @@ +assertSame($expected, $output); + + // Restore php protocol wrapper. + PhpStreamWrapper::restore(); + } +} From c4cefdbd58c14684b40158c02353afcab8731861 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 4 Aug 2022 11:03:30 +0900 Subject: [PATCH 5/6] refactor: make PhpStreamWrapper final --- system/Test/PhpStreamWrapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Test/PhpStreamWrapper.php b/system/Test/PhpStreamWrapper.php index 3d0a9c806892..6bbb4d144b6b 100644 --- a/system/Test/PhpStreamWrapper.php +++ b/system/Test/PhpStreamWrapper.php @@ -18,7 +18,7 @@ * * See https://www.php.net/manual/en/class.streamwrapper.php */ -class PhpStreamWrapper +final class PhpStreamWrapper { private static string $content = ''; private int $position = 0; From 2bf7753ffefaea36a5a1c1093a4b934f2fd0ab64 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 4 Aug 2022 13:16:44 +0900 Subject: [PATCH 6/6] docs: add explanation about stream wrapper --- user_guide_src/source/testing/overview.rst | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/testing/overview.rst b/user_guide_src/source/testing/overview.rst index b2cef40c80ef..749279c0260c 100644 --- a/user_guide_src/source/testing/overview.rst +++ b/user_guide_src/source/testing/overview.rst @@ -285,14 +285,24 @@ Testing CLI Input **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 php protocol. +- ``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 - -.. note:: It is strongly recommended that ``PhpStreamWrapper`` be registered/unregistered only when needed. Otherwise, it will interfere with other built-in php streams while registered.