Skip to content

Commit df8a839

Browse files
committed
fix: preserve enclosing stream filter when using MockInputOutput
1 parent e7ee5b0 commit df8a839

4 files changed

Lines changed: 117 additions & 0 deletions

File tree

system/Test/Filters/CITestStreamFilter.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,22 @@ public static function removeOutputFilter(): void
9292
self::removeFilter(self::$out);
9393
}
9494

95+
/**
96+
* Whether an output filter is currently attached to STDOUT.
97+
*/
98+
public static function hasOutputFilter(): bool
99+
{
100+
return self::$out !== null;
101+
}
102+
103+
/**
104+
* Whether an error filter is currently attached to STDERR.
105+
*/
106+
public static function hasErrorFilter(): bool
107+
{
108+
return self::$err !== null;
109+
}
110+
95111
/**
96112
* @param resource|null $stream
97113
*

system/Test/Mock/MockInputOutput.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ final class MockInputOutput extends InputOutput
3535
*/
3636
private array $outputs = [];
3737

38+
/**
39+
* Snapshot of the shared CITestStreamFilter state captured before this
40+
* object attaches its own filters, restored once it is done. Lets a test
41+
* combine MockInputOutput with an enclosing StreamFilterTrait without the
42+
* latter's filters being torn down.
43+
*
44+
* @var array{output: bool, error: bool, buffer: string}|null
45+
*/
46+
private ?array $priorFilterState = null;
47+
3848
/**
3949
* Sets user inputs.
4050
*
@@ -88,6 +98,12 @@ public function getOutputs(): array
8898

8999
private function addStreamFilters(): void
90100
{
101+
$this->priorFilterState = [
102+
'output' => CITestStreamFilter::hasOutputFilter(),
103+
'error' => CITestStreamFilter::hasErrorFilter(),
104+
'buffer' => CITestStreamFilter::$buffer,
105+
];
106+
91107
CITestStreamFilter::registration();
92108
CITestStreamFilter::addOutputFilter();
93109
CITestStreamFilter::addErrorFilter();
@@ -97,6 +113,22 @@ private function removeStreamFilters(): void
97113
{
98114
CITestStreamFilter::removeOutputFilter();
99115
CITestStreamFilter::removeErrorFilter();
116+
117+
if ($this->priorFilterState === null) {
118+
return;
119+
}
120+
121+
CITestStreamFilter::$buffer = $this->priorFilterState['buffer'];
122+
123+
if ($this->priorFilterState['output']) {
124+
CITestStreamFilter::addOutputFilter();
125+
}
126+
127+
if ($this->priorFilterState['error']) {
128+
CITestStreamFilter::addErrorFilter();
129+
}
130+
131+
$this->priorFilterState = null;
100132
}
101133

102134
public function input(?string $prefix = null): string
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\Test\Mock;
15+
16+
use CodeIgniter\CLI\CLI;
17+
use CodeIgniter\Test\CIUnitTestCase;
18+
use CodeIgniter\Test\StreamFilterTrait;
19+
use PHPUnit\Framework\Attributes\Group;
20+
21+
/**
22+
* @internal
23+
*/
24+
#[Group('Others')]
25+
final class MockInputOutputTest extends CIUnitTestCase
26+
{
27+
use StreamFilterTrait;
28+
29+
protected function tearDown(): void
30+
{
31+
parent::tearDown();
32+
33+
CLI::resetInputOutput();
34+
}
35+
36+
public function testFwriteThroughMockPreservesEnclosingStreamFilter(): void
37+
{
38+
CLI::write('before mock');
39+
$this->assertStringContainsString('before mock', $this->getStreamFilterBuffer());
40+
41+
$io = new MockInputOutput();
42+
CLI::setInputOutput($io);
43+
CLI::write('through mock');
44+
CLI::resetInputOutput();
45+
46+
// The mock captured its own write into its own buffer...
47+
$this->assertStringContainsString('through mock', $io->getOutput());
48+
// ...and left the enclosing StreamFilterTrait buffer untouched.
49+
$this->assertStringNotContainsString('through mock', $this->getStreamFilterBuffer());
50+
51+
// The enclosing filter is still attached, so later writes are captured.
52+
CLI::write('after mock');
53+
$this->assertStringContainsString('before mock', $this->getStreamFilterBuffer());
54+
$this->assertStringContainsString('after mock', $this->getStreamFilterBuffer());
55+
}
56+
57+
public function testInputThroughMockPreservesEnclosingStreamFilter(): void
58+
{
59+
$io = new MockInputOutput();
60+
$io->setInputs(['y']);
61+
CLI::setInputOutput($io);
62+
CLI::prompt('Continue?', ['y', 'n']);
63+
CLI::resetInputOutput();
64+
65+
CLI::write('after prompt');
66+
$this->assertStringContainsString('after prompt', $this->getStreamFilterBuffer());
67+
}
68+
}

user_guide_src/source/changelogs/v4.7.4.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Bugs Fixed
3434
- **Database:** Fixed a bug where ``updateBatch()`` could be called after Query Builder ``where()`` conditions, even though it's not supported. In this situation, now the ``DatabaseException`` is thrown.
3535
- **HTTP:** Fixed a bug where the User Agent library reported Safari's WebKit version instead of the browser version from the ``Version`` token.
3636
- **Model:** Fixed a bug in ``Model::objectToRawArray()`` where the ``$recursive`` parameter was ignored.
37+
- **Testing:** Fixed a bug where using ``MockInputOutput`` within a test that also uses ``StreamFilterTrait`` tore down the trait's stream filters, so CLI output produced after the ``MockInputOutput`` interaction (such as in ``tearDown()``) was no longer captured and leaked to the console.
3738

3839
See the repo's
3940
`CHANGELOG.md <https://github.com/codeigniter4/CodeIgniter4/blob/develop/CHANGELOG.md>`_

0 commit comments

Comments
 (0)