Skip to content

Commit 559ebe3

Browse files
committed
feature #37733 [PhpUnitBridge] Add ability to set a baseline for deprecation testing (alexpott)
This PR was submitted for the master branch but it was squashed and merged into the 5.x branch instead. Discussion ---------- [PhpUnitBridge] Add ability to set a baseline for deprecation testing | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Tickets | Fix #37715, #34496 | License | MIT | Doc PR | symfony/symfony-docs#... <!-- required for new features --> This PR allows you set new options for `SYMFONY_DEPRECATIONS_HELPER` env var: * `generateBaseline` - if this is set to TRUE any deprecations that occur will be written to the file defined in the `baselineFile` option * `baselineFile` a path to a file that contains a json encoded array of deprecations that will be skipped. ### Questions * If you set `generateBaseline` without also setting `baselineFile` an exception is thrown. We could use a default filename if one is not provided (like PHPStan). * How much error checking should we do around the `baselineFile` variable - should we check if it is readable or should we rely on `file_get_contents`()? ### Still @todo Add proper end-to-end testing using a .phpt test Commits ------- 483236f [PhpUnitBridge] Add ability to set a baseline for deprecation testing
2 parents 414a5ab + 483236f commit 559ebe3

File tree

7 files changed

+574
-7
lines changed

7 files changed

+574
-7
lines changed

src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php

+7
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ public function handleError($type, $msg, $file, $line, $context = [])
129129
if ($deprecation->isMuted()) {
130130
return null;
131131
}
132+
if ($this->getConfiguration()->isBaselineDeprecation($deprecation)) {
133+
return null;
134+
}
132135
$group = 'other';
133136

134137
if ($deprecation->originatesFromAnObject()) {
@@ -207,6 +210,10 @@ public function shutdown()
207210
$isFailingAtShutdown = !$configuration->tolerates($this->deprecationGroups);
208211
$this->displayDeprecations($groups, $configuration, $isFailingAtShutdown);
209212

213+
if ($configuration->isGeneratingBaseline()) {
214+
$configuration->writeBaseline();
215+
}
216+
210217
if ($isFailing || $isFailingAtShutdown) {
211218
exit(1);
212219
}

src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php

+102-7
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,28 @@ class Configuration
3737
private $verboseOutput;
3838

3939
/**
40-
* @param int[] $thresholds A hash associating groups to thresholds
41-
* @param string $regex Will be matched against messages, to decide
42-
* whether to display a stack trace
43-
* @param bool[] $verboseOutput Keyed by groups
40+
* @var bool
41+
*/
42+
private $generateBaseline = false;
43+
44+
/**
45+
* @var string
46+
*/
47+
private $baselineFile = '';
48+
49+
/**
50+
* @var array
51+
*/
52+
private $baselineDeprecations = [];
53+
54+
/**
55+
* @param int[] $thresholds A hash associating groups to thresholds
56+
* @param string $regex Will be matched against messages, to decide whether to display a stack trace
57+
* @param bool[] $verboseOutput Keyed by groups
58+
* @param bool $generateBaseline Whether to generate or update the baseline file
59+
* @param string $baselineFile The path to the baseline file
4460
*/
45-
private function __construct(array $thresholds = [], $regex = '', $verboseOutput = [])
61+
private function __construct(array $thresholds = [], $regex = '', $verboseOutput = [], $generateBaseline = false, $baselineFile = '')
4662
{
4763
$groups = ['total', 'indirect', 'direct', 'self'];
4864

@@ -87,6 +103,22 @@ private function __construct(array $thresholds = [], $regex = '', $verboseOutput
87103
}
88104
$this->verboseOutput[$group] = (bool) $status;
89105
}
106+
107+
if ($generateBaseline && !$baselineFile) {
108+
throw new \InvalidArgumentException('You cannot use the "generateBaseline" configuration option without providing a "baselineFile" configuration option.');
109+
}
110+
$this->generateBaseline = $generateBaseline;
111+
$this->baselineFile = $baselineFile;
112+
if ($this->baselineFile && !$this->generateBaseline) {
113+
if (is_file($this->baselineFile)) {
114+
$map = json_decode(file_get_contents($this->baselineFile));
115+
foreach ($map as $baseline_deprecation) {
116+
$this->baselineDeprecations[$baseline_deprecation->location][$baseline_deprecation->message] = $baseline_deprecation->count;
117+
}
118+
} else {
119+
throw new \InvalidArgumentException(sprintf('The baselineFile "%s" does not exist.', $this->baselineFile));
120+
}
121+
}
90122
}
91123

92124
/**
@@ -125,6 +157,61 @@ public function tolerates(array $deprecationGroups)
125157
return true;
126158
}
127159

160+
/**
161+
* @return bool
162+
*/
163+
public function isBaselineDeprecation(Deprecation $deprecation)
164+
{
165+
if ($deprecation->originatesFromAnObject()) {
166+
$location = $deprecation->originatingClass().'::'.$deprecation->originatingMethod();
167+
} else {
168+
$location = 'procedural code';
169+
}
170+
171+
$message = $deprecation->getMessage();
172+
$result = isset($this->baselineDeprecations[$location][$message]) && $this->baselineDeprecations[$location][$message] > 0;
173+
if ($this->generateBaseline) {
174+
if ($result) {
175+
++$this->baselineDeprecations[$location][$message];
176+
} else {
177+
$this->baselineDeprecations[$location][$message] = 1;
178+
$result = true;
179+
}
180+
} elseif ($result) {
181+
--$this->baselineDeprecations[$location][$message];
182+
}
183+
184+
return $result;
185+
}
186+
187+
/**
188+
* @return bool
189+
*/
190+
public function isGeneratingBaseline()
191+
{
192+
return $this->generateBaseline;
193+
}
194+
195+
public function getBaselineFile()
196+
{
197+
return $this->baselineFile;
198+
}
199+
200+
public function writeBaseline()
201+
{
202+
$map = [];
203+
foreach ($this->baselineDeprecations as $location => $messages) {
204+
foreach ($messages as $message => $count) {
205+
$map[] = [
206+
'location' => $location,
207+
'message' => $message,
208+
'count' => $count,
209+
];
210+
}
211+
}
212+
file_put_contents($this->baselineFile, json_encode($map, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES));
213+
}
214+
128215
/**
129216
* @param string $message
130217
*
@@ -161,7 +248,7 @@ public static function fromUrlEncodedString($serializedConfiguration)
161248
{
162249
parse_str($serializedConfiguration, $normalizedConfiguration);
163250
foreach (array_keys($normalizedConfiguration) as $key) {
164-
if (!\in_array($key, ['max', 'disabled', 'verbose', 'quiet'], true)) {
251+
if (!\in_array($key, ['max', 'disabled', 'verbose', 'quiet', 'generateBaseline', 'baselineFile'], true)) {
165252
throw new \InvalidArgumentException(sprintf('Unknown configuration option "%s".', $key));
166253
}
167254
}
@@ -171,6 +258,8 @@ public static function fromUrlEncodedString($serializedConfiguration)
171258
'disabled' => false,
172259
'verbose' => true,
173260
'quiet' => [],
261+
'generateBaseline' => false,
262+
'baselineFile' => '',
174263
];
175264

176265
if ('' === $normalizedConfiguration['disabled'] || filter_var($normalizedConfiguration['disabled'], \FILTER_VALIDATE_BOOLEAN)) {
@@ -188,7 +277,13 @@ public static function fromUrlEncodedString($serializedConfiguration)
188277
}
189278
}
190279

191-
return new self($normalizedConfiguration['max'], '', $verboseOutput);
280+
return new self(
281+
isset($normalizedConfiguration['max']) ? $normalizedConfiguration['max'] : [],
282+
'',
283+
$verboseOutput,
284+
filter_var($normalizedConfiguration['generateBaseline'], \FILTER_VALIDATE_BOOLEAN),
285+
$normalizedConfiguration['baselineFile']
286+
);
192287
}
193288

194289
/**

src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php

+168
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Configuration;
16+
use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Deprecation;
1617
use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\DeprecationGroup;
1718

1819
class ConfigurationTest extends TestCase
1920
{
21+
private $files;
22+
2023
public function testItThrowsOnStringishValue()
2124
{
2225
$this->expectException(\InvalidArgumentException::class);
@@ -244,4 +247,169 @@ private function buildGroups($counts)
244247

245248
return $groups;
246249
}
250+
251+
public function testBaselineGenerationEmptyFile()
252+
{
253+
$filename = $this->createFile();
254+
$configuration = Configuration::fromUrlEncodedString('generateBaseline=true&baselineFile=' . urlencode($filename));
255+
$this->assertTrue($configuration->isGeneratingBaseline());
256+
$trace = debug_backtrace();
257+
$this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 1', $trace, '')));
258+
$this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 2', $trace, '')));
259+
$this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 1', $trace, '')));
260+
$configuration->writeBaseline();
261+
$this->assertEquals($filename, $configuration->getBaselineFile());
262+
$expected_baseline = [
263+
[
264+
'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest',
265+
'message' => 'Test message 1',
266+
'count' => 2,
267+
],
268+
[
269+
'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest',
270+
'message' => 'Test message 2',
271+
'count' => 1,
272+
],
273+
];
274+
$this->assertEquals(json_encode($expected_baseline, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES), file_get_contents($filename));
275+
}
276+
277+
public function testBaselineGenerationNoFile()
278+
{
279+
$filename = $this->createFile();
280+
$configuration = Configuration::fromUrlEncodedString('generateBaseline=true&baselineFile=' . urlencode($filename));
281+
$this->assertTrue($configuration->isGeneratingBaseline());
282+
$trace = debug_backtrace();
283+
$this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 1', $trace, '')));
284+
$this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 2', $trace, '')));
285+
$this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 2', $trace, '')));
286+
$this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 1', $trace, '')));
287+
$configuration->writeBaseline();
288+
$this->assertEquals($filename, $configuration->getBaselineFile());
289+
$expected_baseline = [
290+
[
291+
'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest',
292+
'message' => 'Test message 1',
293+
'count' => 2,
294+
],
295+
[
296+
'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest',
297+
'message' => 'Test message 2',
298+
'count' => 2,
299+
],
300+
];
301+
$this->assertEquals(json_encode($expected_baseline, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES), file_get_contents($filename));
302+
303+
}
304+
305+
public function testExistingBaseline()
306+
{
307+
$filename = $this->createFile();
308+
$baseline = [
309+
[
310+
'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest',
311+
'message' => 'Test message 1',
312+
'count' => 1,
313+
],
314+
[
315+
'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest',
316+
'message' => 'Test message 2',
317+
'count' => 1,
318+
],
319+
];
320+
file_put_contents($filename, json_encode($baseline));
321+
322+
$configuration = Configuration::fromUrlEncodedString('baselineFile=' . urlencode($filename));
323+
$this->assertFalse($configuration->isGeneratingBaseline());
324+
$trace = debug_backtrace();
325+
$this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 1', $trace, '')));
326+
$this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 2', $trace, '')));
327+
$this->assertFalse($configuration->isBaselineDeprecation(new Deprecation('Test message 3', $trace, '')));
328+
$this->assertEquals($filename, $configuration->getBaselineFile());
329+
}
330+
331+
public function testExistingBaselineAndGeneration()
332+
{
333+
$filename = $this->createFile();
334+
$baseline = [
335+
[
336+
'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest',
337+
'message' => 'Test message 1',
338+
'count' => 1,
339+
],
340+
[
341+
'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest',
342+
'message' => 'Test message 2',
343+
'count' => 1,
344+
],
345+
];
346+
file_put_contents($filename, json_encode($baseline));
347+
$configuration = Configuration::fromUrlEncodedString('generateBaseline=true&baselineFile=' . urlencode($filename));
348+
$this->assertTrue($configuration->isGeneratingBaseline());
349+
$trace = debug_backtrace();
350+
$this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 2', $trace, '')));
351+
$this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 3', $trace, '')));
352+
$configuration->writeBaseline();
353+
$this->assertEquals($filename, $configuration->getBaselineFile());
354+
$expected_baseline = [
355+
[
356+
'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest',
357+
'message' => 'Test message 2',
358+
'count' => 1,
359+
],
360+
[
361+
'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest',
362+
'message' => 'Test message 3',
363+
'count' => 1,
364+
],
365+
];
366+
$this->assertEquals(json_encode($expected_baseline, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES), file_get_contents($filename));
367+
}
368+
369+
public function testBaselineArgumentException()
370+
{
371+
$this->expectException(\InvalidArgumentException::class);
372+
$this->expectExceptionMessage('You cannot use the "generateBaseline" configuration option without providing a "baselineFile" configuration option.');
373+
Configuration::fromUrlEncodedString('generateBaseline=true');
374+
}
375+
376+
public function testBaselineFileException()
377+
{
378+
$filename = $this->createFile();
379+
unlink($filename);
380+
$this->expectException(\InvalidArgumentException::class);
381+
$this->expectExceptionMessage(sprintf('The baselineFile "%s" does not exist.', $filename));
382+
Configuration::fromUrlEncodedString('baselineFile=' . urlencode($filename));
383+
}
384+
385+
public function testBaselineFileWriteError()
386+
{
387+
$filename = $this->createFile();
388+
chmod($filename, 0444);
389+
$this->expectError();
390+
$this->expectErrorMessageMatches('/failed to open stream: Permission denied/');
391+
$configuration = Configuration::fromUrlEncodedString('generateBaseline=true&baselineFile=' . urlencode($filename));
392+
$configuration->writeBaseline();
393+
}
394+
395+
protected function setUp(): void
396+
{
397+
$this->files = [];
398+
}
399+
400+
protected function tearDown(): void
401+
{
402+
foreach ($this->files as $file) {
403+
if (file_exists($file)) {
404+
unlink($file);
405+
}
406+
}
407+
}
408+
409+
private function createFile() {
410+
$filename = tempnam(sys_get_temp_dir(), 'sf-');
411+
$this->files[] = $filename;
412+
return $filename;
413+
}
414+
247415
}

0 commit comments

Comments
 (0)