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/system/Test/PhpStreamWrapper.php b/system/Test/PhpStreamWrapper.php new file mode 100644 index 000000000000..6bbb4d144b6b --- /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 + */ +final 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); + } +} diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index da4524ae47a8..225a7dfa259e 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,34 @@ 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); + + PhpStreamWrapper::restore(); + } + + public function testPrompt() + { + PhpStreamWrapper::register(); + + $expected = 'red'; + PhpStreamWrapper::setContent($expected); + + $output = CLI::prompt('What is your favorite color?'); + + $this->assertSame($expected, $output); - // 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() 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..749279c0260c 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,33 @@ 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()``. + +.. 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/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(); + } +}