Skip to content

Commit acd940a

Browse files
committed
TestUtils/ConfigDouble: bug fix - always reset Config statics after use
The PHPCS native `Config` class uses a number of static properties, which may be updated during tests. These were previously reset to their default values in the `UtilityMethodTestCase::resetTestFile()` method, but this reset was inadvertently removed in commit 4f0f9a4 with the reasoning that, if all tests use the `ConfigDouble`, the reset would no longer be needed as the `ConfigDouble` resets on being initialized. The flaw in this logic is that not all tests are guaranteed to use the `ConfigDouble`, which means that without the reset, the `Config` class may be left "dirty" after tests using the `ConfigDouble`, which could break tests. This commit fixes this issue by: * Adding a `__destruct()` method to the `ConfigDouble` class which will reset the static properties on the PHPCS native `Config` class whenever an object created from this class is destroyed. * Explicitly calling the `__destruct()` method from the `UtilityMethodTestCase::resetTestFile()` method to ensure it is always run after a test has finished ("after class"), even if there would still be a lingering reference to the object. Includes tests for the new `__destruct()` method. Includes an additional test for the `UtilityMethodTestCase` class to ensure this snafu cannot come back without tests failing on it.
1 parent 4a30521 commit acd940a

File tree

5 files changed

+285
-1
lines changed

5 files changed

+285
-1
lines changed

PHPCSUtils/TestUtils/ConfigDouble.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,22 @@ public function __construct(array $cliArgs = [], $skipSettingStandard = false, $
9090
}
9191
}
9292

93+
/**
94+
* Ensures the static properties in the Config class are reset to their default values
95+
* when the ConfigDouble is no longer used.
96+
*
97+
* @since 1.1.0
98+
*
99+
* @return void
100+
*/
101+
public function __destruct()
102+
{
103+
$this->setStaticConfigProperty('overriddenDefaults', []);
104+
$this->setStaticConfigProperty('executablePaths', []);
105+
$this->setStaticConfigProperty('configData', null);
106+
$this->setStaticConfigProperty('configDataFile', null);
107+
}
108+
93109
/**
94110
* Sets the command line values and optionally prevents a file system search for a custom ruleset.
95111
*

PHPCSUtils/TestUtils/UtilityMethodTestCase.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,16 @@ public function skipJSCSSTestsOnPHPCS4()
310310
*/
311311
public static function resetTestFile()
312312
{
313+
/*
314+
* Explicitly trigger __destruct() on the ConfigDouble to reset the Config statics.
315+
* The explicit method call prevents potential stray test-local references to the $config object
316+
* preventing the destructor from running the clean up (which without stray references would be
317+
* automagically triggered when `self::$phpcsFile` is reset, but we can't definitively rely on that).
318+
*/
319+
if (isset(self::$phpcsFile)) {
320+
self::$phpcsFile->config->__destruct();
321+
}
322+
313323
self::$phpcsVersion = '0';
314324
self::$fileExtension = 'inc';
315325
self::$caseFile = '';

Tests/TestUtils/ConfigDouble/ConfigDoubleTest.php

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010

1111
namespace PHPCSUtils\Tests\TestUtils\ConfigDouble;
1212

13+
use PHPCSUtils\BackCompat\Helper;
1314
use PHPCSUtils\TestUtils\ConfigDouble;
15+
use ReflectionProperty;
1416
use Yoast\PHPUnitPolyfills\TestCases\TestCase;
1517

1618
/**
@@ -25,6 +27,23 @@
2527
final class ConfigDoubleTest extends TestCase
2628
{
2729

30+
/**
31+
* Reset the static properties in the Config class to their true defaults to prevent this class
32+
* from influencing other tests.
33+
*
34+
* @afterClass
35+
*
36+
* @return void
37+
*/
38+
public static function resetConfigToDefaults()
39+
{
40+
self::setStaticConfigProperty('overriddenDefaults', []);
41+
self::setStaticConfigProperty('executablePaths', []);
42+
self::setStaticConfigProperty('configData', null);
43+
self::setStaticConfigProperty('configDataFile', null);
44+
$_SERVER['argv'] = [];
45+
}
46+
2847
/**
2948
* Verify that the static properties in the Config class get cleared between instances.
3049
*
@@ -200,4 +219,119 @@ public function testReportWidthOverrideIsSkippedOnRequest()
200219
$this->assertNotSame(80, $config->reportWidth, 'Report width has still been set to 80');
201220
}
202221
}
222+
223+
/**
224+
* Verify that the static properties in the Config class are reset when the object is destroyed.
225+
*
226+
* @covers ::__destruct
227+
*
228+
* @return void
229+
*/
230+
public function testDestruct()
231+
{
232+
$standard = 'Squiz';
233+
$reportWidth = 1250;
234+
$fakeConfFile = 'path/to/file.conf';
235+
$toolName = 'a_tool';
236+
237+
// Create the ConfigDouble object and change the value of a few static properties to allow for testing the reset.
238+
$cliArgs = [
239+
'--standard=' . $standard,
240+
'--report-width=' . $reportWidth,
241+
'--runtime-set',
242+
'arbitraryKey',
243+
'arbitraryValue',
244+
];
245+
$config = new ConfigDouble($cliArgs);
246+
247+
$config->setStaticConfigProperty('configDataFile', $fakeConfFile);
248+
$config::getExecutablePath($toolName);
249+
250+
// Verify the static properties in the Config are set to something other than their default value.
251+
$this->assertSame([$standard], $config->standards, 'Precondition check: Standards was not set to Squiz');
252+
$this->assertSame($reportWidth, $config->reportWidth, 'Precondition check: Report width was not set to 1250');
253+
$this->assertSame(
254+
'arbitraryValue',
255+
Helper::getConfigData('arbitraryKey'),
256+
'Precondition check: ArbitraryKey property was not set on the Config class'
257+
);
258+
259+
$overriddenDefaults = $this->getStaticConfigProperty('overriddenDefaults', $config);
260+
$this->assertIsArray($overriddenDefaults, 'Precondition check: overriddenDefaults property is not an array');
261+
$this->assertNotEmpty($overriddenDefaults, 'Precondition check: overriddenDefaults property is an empty array');
262+
263+
$this->assertSame(
264+
[$toolName => null],
265+
$this->getStaticConfigProperty('executablePaths', $config),
266+
'Precondition check: executablePaths is still an empty array'
267+
);
268+
269+
$configData = $this->getStaticConfigProperty('configData', $config);
270+
$this->assertIsArray($configData, 'Precondition check: configData property is not an array');
271+
$this->assertNotEmpty($configData, 'Precondition check: configData property is an empty array');
272+
273+
$this->assertSame(
274+
$fakeConfFile,
275+
$this->getStaticConfigProperty('configDataFile', $config),
276+
'Precondition check: configDataFile property has not been set'
277+
);
278+
279+
// Destroy the object.
280+
unset($config);
281+
282+
// Verify the static properties in the Config are reset to their default values.
283+
$this->assertSame([], $this->getStaticConfigProperty('overriddenDefaults'), 'overriddenDefaults reset failed');
284+
$this->assertSame([], $this->getStaticConfigProperty('executablePaths'), 'executablePaths reset failed');
285+
$this->assertNull($this->getStaticConfigProperty('configData'), 'configData reset failed');
286+
$this->assertNull($this->getStaticConfigProperty('configDataFile'), 'configDataFile reset failed');
287+
288+
// This assertion must be last as it re-initializes the $configData and $configDataFile properties.
289+
$this->assertNull(Helper::getConfigData('arbitraryKey'), 'arbitraryKey property is still set');
290+
}
291+
292+
/**
293+
* Helper function to retrieve the value of a private static property on the Config class.
294+
*
295+
* @param string $name The name of the property to retrieve.
296+
* @param \PHP_CodeSniffer\Config $config Optional. The config object.
297+
*
298+
* @return mixed
299+
*/
300+
private function getStaticConfigProperty($name, $config = null)
301+
{
302+
$property = new ReflectionProperty('PHP_CodeSniffer\Config', $name);
303+
$property->setAccessible(true);
304+
305+
if ($name === 'overriddenDefaults' && \version_compare(Helper::getVersion(), '3.99.99', '>')) {
306+
// The `overriddenDefaults` property is no longer static on PHPCS 4.0+.
307+
if (isset($config)) {
308+
return $property->getValue($config);
309+
} else {
310+
return [];
311+
}
312+
}
313+
314+
return $property->getValue();
315+
}
316+
317+
/**
318+
* Helper function to set the value of a private static property on the Config class.
319+
*
320+
* @param string $name The name of the property to set.
321+
* @param mixed $value The value to set the property to.
322+
*
323+
* @return void
324+
*/
325+
public static function setStaticConfigProperty($name, $value)
326+
{
327+
$property = new ReflectionProperty('PHP_CodeSniffer\Config', $name);
328+
$property->setAccessible(true);
329+
330+
// The `overriddenDefaults` property is no longer static on PHPCS 4.0+, so ignore it.
331+
if ($name !== 'overriddenDefaults' && \version_compare(Helper::getVersion(), '3.99.99', '<=')) {
332+
$property->setValue(null, $value);
333+
}
334+
335+
$property->setAccessible(false);
336+
}
203337
}

Tests/TestUtils/UtilityMethodTestCase/ResetTestFileTest.php

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010

1111
namespace PHPCSUtils\Tests\TestUtils\UtilityMethodTestCase;
1212

13+
use PHP_CodeSniffer\Config;
14+
use PHPCSUtils\BackCompat\Helper;
1315
use PHPCSUtils\Tests\PolyfilledTestCase;
16+
use ReflectionProperty;
1417

1518
/**
1619
* Tests for the \PHPCSUtils\TestUtils\UtilityMethodTestCase class.
@@ -40,7 +43,7 @@ public static function setUpTestFile()
4043
*
4144
* @return void
4245
*/
43-
public function testTearDown()
46+
public function testTearDownCleansUpStaticTestCaseClassProperties()
4447
{
4548
// Initialize a test, which should change the values of most static properties.
4649
self::$tabWidth = 2;
@@ -66,4 +69,114 @@ public function testTearDown()
6669
$this->assertNull(self::$phpcsFile, 'phpcsFile was not reset');
6770
$this->assertSame(['Dummy.Dummy.Dummy'], self::$selectedSniff, 'selectedSniff was not reset');
6871
}
72+
73+
/**
74+
* Test that the static properties in the Config class are correctly reset.
75+
*
76+
* Ensure that Config set for one test does not influence the next test.
77+
*
78+
* @return void
79+
*/
80+
public function testTearDownCleansUpStaticConfigProperties()
81+
{
82+
$fakeConfFile = 'path/to/file.conf';
83+
$toolName = 'a_tool';
84+
85+
// Set up preconditions.
86+
self::setUpTestFile();
87+
parent::setUpTestFile();
88+
89+
$config = self::$phpcsFile->config;
90+
Helper::setConfigData('arbitraryKey', 'arbitraryValue', true, $config);
91+
$config->setStaticConfigProperty('configDataFile', $fakeConfFile);
92+
$config::getExecutablePath($toolName);
93+
94+
// Verify the static properties in the Config are set to something other than their default value.
95+
$this->assertSame(['PSR1'], $config->standards, 'Precondition check: Standards was not set to PSR1');
96+
97+
$overriddenDefaults = $this->getStaticConfigProperty('overriddenDefaults', $config);
98+
$this->assertIsArray($overriddenDefaults, 'Precondition check: overriddenDefaults property is not an array');
99+
$this->assertNotEmpty($overriddenDefaults, 'Precondition check: overriddenDefaults property is an empty array');
100+
101+
$this->assertSame(
102+
[$toolName => null],
103+
$this->getStaticConfigProperty('executablePaths', $config),
104+
'Precondition check: executablePaths is still an empty array'
105+
);
106+
107+
$configData = $this->getStaticConfigProperty('configData', $config);
108+
$this->assertIsArray($configData, 'Precondition check: configData property is not an array');
109+
$this->assertNotEmpty($configData, 'Precondition check: configData property is an empty array');
110+
111+
$this->assertSame(
112+
$fakeConfFile,
113+
$this->getStaticConfigProperty('configDataFile', $config),
114+
'Precondition check: configDataFile property has not been set'
115+
);
116+
117+
// Reset the file as per the "afterClass"/tear down method.
118+
parent::resetTestFile();
119+
120+
// Verify that the reset also reset the static properties on the Config class.
121+
$this->assertSame(
122+
[],
123+
$this->getStaticConfigProperty('overriddenDefaults'),
124+
'overriddenDefaults reset failed'
125+
);
126+
$this->assertSame([], $this->getStaticConfigProperty('executablePaths'), 'executablePaths reset failed');
127+
$this->assertNull($this->getStaticConfigProperty('configData'), 'configData reset failed');
128+
$this->assertNull($this->getStaticConfigProperty('configDataFile'), 'configDataFile reset failed');
129+
130+
$this->assertNull(Helper::getConfigData('arbitraryKey'), 'arbitraryKey property is still set');
131+
132+
// Now check that if a new Config is created for another test, that previously overridden defaults can be set again.
133+
$newConfig = new Config(['--standard=Squiz']);
134+
135+
// Verify the new Config does not have leaked property values from the Config from the "previous test".
136+
$this->assertSame(['Squiz'], $newConfig->standards, 'New standards choice was not set to Squiz');
137+
$this->assertSame(
138+
['standards' => true],
139+
$this->getStaticConfigProperty('overriddenDefaults', $newConfig),
140+
'overriddenDefaults has not been reinitialized'
141+
);
142+
143+
// Verify that previously overridden config property can be written to.
144+
$newValue = 'new value';
145+
$this->assertTrue(
146+
Helper::setConfigData('arbitraryKey', $newValue, true, $newConfig),
147+
'New value for arbitraryKey is not accepted'
148+
);
149+
$this->assertSame(
150+
$newValue,
151+
Helper::getConfigData('arbitraryKey'),
152+
'Previously overridden default was not allowed to be set'
153+
);
154+
}
155+
156+
/**
157+
* Helper function to retrieve the value of a private static property on the Config class.
158+
*
159+
* @param string $name The name of the property to retrieve.
160+
* @param \PHP_CodeSniffer\Config $config Optional. The config object.
161+
*
162+
* @return mixed
163+
*/
164+
private function getStaticConfigProperty($name, $config = null)
165+
{
166+
$property = new ReflectionProperty('PHP_CodeSniffer\Config', $name);
167+
$property->setAccessible(true);
168+
169+
if ($name === 'overriddenDefaults'
170+
&& (self::$phpcsVersion === '0' || \version_compare(self::$phpcsVersion, '3.99.99', '>'))
171+
) {
172+
// The `overriddenDefaults` property is no longer static on PHPCS 4.0+.
173+
if (isset($config)) {
174+
return $property->getValue($config);
175+
} else {
176+
return [];
177+
}
178+
}
179+
180+
return $property->getValue();
181+
}
69182
}

phpstan.neon.dist

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ parameters:
3535
count: 1
3636

3737
# Level 2
38+
# The __destruct() method exists on the ConfigDouble, not the PHPCS native Config. This is 100% okay.
39+
-
40+
message: '`^Call to an undefined method PHP_CodeSniffer\\Config::__destruct\(\)\.$`'
41+
path: PHPCSUtils\TestUtils\UtilityMethodTestCase.php
42+
count: 1
43+
# The setStaticConfigProperty() method exists on the ConfigDouble, not the PHPCS native Config. This is 100% okay.
44+
-
45+
message: '`^Call to an undefined method PHP_CodeSniffer\\Config::setStaticConfigProperty\(\)\.$`'
46+
path: Tests\TestUtils\UtilityMethodTestCase\ResetTestFileTest.php
47+
count: 1
48+
3849
# Ignoring as this refers to a non-mocked method on the original class. This is 100% okay.
3950
-
4051
message: '`^Call to an undefined method PHPUnit\\Framework\\MockObject\\MockObject::process\(\)\.$`'

0 commit comments

Comments
 (0)