Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make CLI::input() testable #6335

Merged
merged 6 commits into from
Aug 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions system/CLI/CLI.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
kenjis marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
70 changes: 70 additions & 0 deletions system/Test/PhpStreamWrapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <[email protected]>
*
* 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);
}
}
43 changes: 28 additions & 15 deletions tests/system/CLI/CLITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace CodeIgniter\CLI;

use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\PhpStreamWrapper;
use CodeIgniter\Test\StreamFilterTrait;
use ReflectionProperty;
use RuntimeException;
Expand Down Expand Up @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion user_guide_src/source/changelogs/v4.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <model-allow-empty-inserts>`
- 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 <localization-in-routes>`
Expand Down
6 changes: 6 additions & 0 deletions user_guide_src/source/cli/cli_library.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 33 additions & 3 deletions user_guide_src/source/testing/overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -276,3 +276,33 @@ See :ref:`Testing Traits <testing-overview-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()``.
kenjis marked this conversation as resolved.
Show resolved Hide resolved

.. note:: The PhpStreamWrapper is a stream wrapper class.
If you don't know PHP's stream wrapper,
see `The streamWrapper class <https://www.php.net/manual/en/class.streamwrapper.php>`_
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 <https://www.php.net/manual/en/wrappers.php.php>`_ 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
25 changes: 25 additions & 0 deletions user_guide_src/source/testing/overview/019.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

use CodeIgniter\CLI\CLI;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\PhpStreamWrapper;

final class SomeTest extends CIUnitTestCase
{
public function testPrompt(): void
{
// Register the PhpStreamWrapper.
PhpStreamWrapper::register();

// Set the user input to 'red'. It will be provided as `php://stdin` output.
$expected = 'red';
PhpStreamWrapper::setContent($expected);

$output = CLI::prompt('What is your favorite color?');

$this->assertSame($expected, $output);

// Restore php protocol wrapper.
PhpStreamWrapper::restore();
}
}