diff --git a/.psalm/baseline.xml b/.psalm/baseline.xml
index d438adbe551..1bb414f842b 100644
--- a/.psalm/baseline.xml
+++ b/.psalm/baseline.xml
@@ -531,6 +531,14 @@
=']]>
+
+
+
+
+
+ ]]>
+
+
Driver
@@ -569,6 +577,17 @@
stop
+
+
+ $errorFile
+ $errorLine
+ $errorString
+
+
+ Issue::from($file, $line, null, $description)
+ Issue::from($file, $line, null, $description)
+
+
GroupFilterIterator
@@ -635,6 +654,9 @@
+
+ generateBaseline()]]>
+
nameAndVersion
@@ -726,6 +748,7 @@
hasCoverageCacheDirectory
+ baseline
detect
diff --git a/ChangeLog-10.4.md b/ChangeLog-10.4.md
index cf9e253ca73..766288f8062 100644
--- a/ChangeLog-10.4.md
+++ b/ChangeLog-10.4.md
@@ -6,6 +6,7 @@ All notable changes of the PHPUnit 10.4 release series are documented in this fi
### Added
+* [#5441](https://github.com/sebastianbergmann/phpunit/issues/5441): Baseline for `E_(USER_)DEPRECATED`, `E_(USER_)NOTICE`, `E_STRICT`, and `E_(USER_)WARNING`
* [#5462](https://github.com/sebastianbergmann/phpunit/pull/5462): Support for multiple arguments
* [#5471](https://github.com/sebastianbergmann/phpunit/issues/5471): `assertFileMatchesFormat()` and `assertFileMatchesFormatFile()`
* Attribute `id` attribute for `testCaseMethod` elements in the XML document generated by `--list-tests-xml`
diff --git a/phpunit.xml b/phpunit.xml
index ee249a2d47c..f4f103eb2d3 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -13,6 +13,7 @@
+ tests/end-to-end/baseline
tests/end-to-end/cli
tests/end-to-end/code-coverage
tests/end-to-end/event
diff --git a/phpunit.xsd b/phpunit.xsd
index c4c961d6db5..bd22b2ca2a7 100644
--- a/phpunit.xsd
+++ b/phpunit.xsd
@@ -24,6 +24,7 @@
+
diff --git a/src/Event/Emitter/DispatchingEmitter.php b/src/Event/Emitter/DispatchingEmitter.php
index 0a7999e49bc..66e6ec1eb83 100644
--- a/src/Event/Emitter/DispatchingEmitter.php
+++ b/src/Event/Emitter/DispatchingEmitter.php
@@ -752,7 +752,7 @@ public function testTriggeredPhpunitDeprecation(Code\Test $test, string $message
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
- public function testTriggeredPhpDeprecation(Code\Test $test, string $message, string $file, int $line, bool $suppressed): void
+ public function testTriggeredPhpDeprecation(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void
{
$this->dispatcher->dispatch(
new Test\PhpDeprecationTriggered(
@@ -762,6 +762,7 @@ public function testTriggeredPhpDeprecation(Code\Test $test, string $message, st
$file,
$line,
$suppressed,
+ $ignoredByBaseline,
),
);
}
@@ -770,7 +771,7 @@ public function testTriggeredPhpDeprecation(Code\Test $test, string $message, st
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
- public function testTriggeredDeprecation(Code\Test $test, string $message, string $file, int $line, bool $suppressed): void
+ public function testTriggeredDeprecation(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void
{
$this->dispatcher->dispatch(
new Test\DeprecationTriggered(
@@ -780,6 +781,7 @@ public function testTriggeredDeprecation(Code\Test $test, string $message, strin
$file,
$line,
$suppressed,
+ $ignoredByBaseline,
),
);
}
@@ -806,7 +808,7 @@ public function testTriggeredError(Code\Test $test, string $message, string $fil
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
- public function testTriggeredNotice(Code\Test $test, string $message, string $file, int $line, bool $suppressed): void
+ public function testTriggeredNotice(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void
{
$this->dispatcher->dispatch(
new Test\NoticeTriggered(
@@ -816,6 +818,7 @@ public function testTriggeredNotice(Code\Test $test, string $message, string $fi
$file,
$line,
$suppressed,
+ $ignoredByBaseline,
),
);
}
@@ -824,7 +827,7 @@ public function testTriggeredNotice(Code\Test $test, string $message, string $fi
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
- public function testTriggeredPhpNotice(Code\Test $test, string $message, string $file, int $line, bool $suppressed): void
+ public function testTriggeredPhpNotice(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void
{
$this->dispatcher->dispatch(
new Test\PhpNoticeTriggered(
@@ -834,6 +837,7 @@ public function testTriggeredPhpNotice(Code\Test $test, string $message, string
$file,
$line,
$suppressed,
+ $ignoredByBaseline,
),
);
}
@@ -842,7 +846,7 @@ public function testTriggeredPhpNotice(Code\Test $test, string $message, string
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
- public function testTriggeredWarning(Code\Test $test, string $message, string $file, int $line, bool $suppressed): void
+ public function testTriggeredWarning(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void
{
$this->dispatcher->dispatch(
new Test\WarningTriggered(
@@ -852,6 +856,7 @@ public function testTriggeredWarning(Code\Test $test, string $message, string $f
$file,
$line,
$suppressed,
+ $ignoredByBaseline,
),
);
}
@@ -860,7 +865,7 @@ public function testTriggeredWarning(Code\Test $test, string $message, string $f
* @throws InvalidArgumentException
* @throws UnknownEventTypeException
*/
- public function testTriggeredPhpWarning(Code\Test $test, string $message, string $file, int $line, bool $suppressed): void
+ public function testTriggeredPhpWarning(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void
{
$this->dispatcher->dispatch(
new Test\PhpWarningTriggered(
@@ -870,6 +875,7 @@ public function testTriggeredPhpWarning(Code\Test $test, string $message, string
$file,
$line,
$suppressed,
+ $ignoredByBaseline,
),
);
}
diff --git a/src/Event/Emitter/Emitter.php b/src/Event/Emitter/Emitter.php
index cda52aca064..b60bafbf387 100644
--- a/src/Event/Emitter/Emitter.php
+++ b/src/Event/Emitter/Emitter.php
@@ -167,19 +167,19 @@ public function testSkipped(Code\Test $test, string $message): void;
public function testTriggeredPhpunitDeprecation(Code\Test $test, string $message): void;
- public function testTriggeredPhpDeprecation(Code\Test $test, string $message, string $file, int $line, bool $suppressed): void;
+ public function testTriggeredPhpDeprecation(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void;
- public function testTriggeredDeprecation(Code\Test $test, string $message, string $file, int $line, bool $suppressed): void;
+ public function testTriggeredDeprecation(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void;
public function testTriggeredError(Code\Test $test, string $message, string $file, int $line, bool $suppressed): void;
- public function testTriggeredNotice(Code\Test $test, string $message, string $file, int $line, bool $suppressed): void;
+ public function testTriggeredNotice(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void;
- public function testTriggeredPhpNotice(Code\Test $test, string $message, string $file, int $line, bool $suppressed): void;
+ public function testTriggeredPhpNotice(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void;
- public function testTriggeredWarning(Code\Test $test, string $message, string $file, int $line, bool $suppressed): void;
+ public function testTriggeredWarning(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void;
- public function testTriggeredPhpWarning(Code\Test $test, string $message, string $file, int $line, bool $suppressed): void;
+ public function testTriggeredPhpWarning(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void;
public function testTriggeredPhpunitError(Code\Test $test, string $message): void;
diff --git a/src/Event/Events/Test/Issue/DeprecationTriggered.php b/src/Event/Events/Test/Issue/DeprecationTriggered.php
index f27705cd510..7eeb2340333 100644
--- a/src/Event/Events/Test/Issue/DeprecationTriggered.php
+++ b/src/Event/Events/Test/Issue/DeprecationTriggered.php
@@ -40,20 +40,22 @@ final class DeprecationTriggered implements Event
*/
private readonly int $line;
private readonly bool $suppressed;
+ private readonly bool $ignoredByBaseline;
/**
* @psalm-param non-empty-string $message
* @psalm-param non-empty-string $file
* @psalm-param positive-int $line
*/
- public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed)
+ public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline)
{
- $this->telemetryInfo = $telemetryInfo;
- $this->test = $test;
- $this->message = $message;
- $this->file = $file;
- $this->line = $line;
- $this->suppressed = $suppressed;
+ $this->telemetryInfo = $telemetryInfo;
+ $this->test = $test;
+ $this->message = $message;
+ $this->file = $file;
+ $this->line = $line;
+ $this->suppressed = $suppressed;
+ $this->ignoredByBaseline = $ignoredByBaseline;
}
public function telemetryInfo(): Telemetry\Info
@@ -95,6 +97,11 @@ public function wasSuppressed(): bool
return $this->suppressed;
}
+ public function ignoredByBaseline(): bool
+ {
+ return $this->ignoredByBaseline;
+ }
+
public function asString(): string
{
$message = $this->message;
@@ -103,9 +110,17 @@ public function asString(): string
$message = PHP_EOL . $message;
}
+ $status = '';
+
+ if ($this->ignoredByBaseline) {
+ $status = 'Baseline-Ignored ';
+ } elseif ($this->suppressed) {
+ $status = 'Suppressed ';
+ }
+
return sprintf(
'Test Triggered %sDeprecation (%s)%s',
- $this->wasSuppressed() ? 'Suppressed ' : '',
+ $status,
$this->test->id(),
$message,
);
diff --git a/src/Event/Events/Test/Issue/NoticeTriggered.php b/src/Event/Events/Test/Issue/NoticeTriggered.php
index 57f2a908c50..d8a27bbd852 100644
--- a/src/Event/Events/Test/Issue/NoticeTriggered.php
+++ b/src/Event/Events/Test/Issue/NoticeTriggered.php
@@ -40,20 +40,22 @@ final class NoticeTriggered implements Event
*/
private readonly int $line;
private readonly bool $suppressed;
+ private readonly bool $ignoredByBaseline;
/**
* @psalm-param non-empty-string $message
* @psalm-param non-empty-string $file
* @psalm-param positive-int $line
*/
- public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed)
+ public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline)
{
- $this->telemetryInfo = $telemetryInfo;
- $this->test = $test;
- $this->message = $message;
- $this->file = $file;
- $this->line = $line;
- $this->suppressed = $suppressed;
+ $this->telemetryInfo = $telemetryInfo;
+ $this->test = $test;
+ $this->message = $message;
+ $this->file = $file;
+ $this->line = $line;
+ $this->suppressed = $suppressed;
+ $this->ignoredByBaseline = $ignoredByBaseline;
}
public function telemetryInfo(): Telemetry\Info
@@ -95,6 +97,11 @@ public function wasSuppressed(): bool
return $this->suppressed;
}
+ public function ignoredByBaseline(): bool
+ {
+ return $this->ignoredByBaseline;
+ }
+
public function asString(): string
{
$message = $this->message;
@@ -103,9 +110,17 @@ public function asString(): string
$message = PHP_EOL . $message;
}
+ $status = '';
+
+ if ($this->ignoredByBaseline) {
+ $status = 'Baseline-Ignored ';
+ } elseif ($this->suppressed) {
+ $status = 'Suppressed ';
+ }
+
return sprintf(
'Test Triggered %sNotice (%s)%s',
- $this->wasSuppressed() ? 'Suppressed ' : '',
+ $status,
$this->test->id(),
$message,
);
diff --git a/src/Event/Events/Test/Issue/PhpDeprecationTriggered.php b/src/Event/Events/Test/Issue/PhpDeprecationTriggered.php
index 5846af0bf6e..a59e5c41cbb 100644
--- a/src/Event/Events/Test/Issue/PhpDeprecationTriggered.php
+++ b/src/Event/Events/Test/Issue/PhpDeprecationTriggered.php
@@ -40,20 +40,22 @@ final class PhpDeprecationTriggered implements Event
*/
private readonly int $line;
private readonly bool $suppressed;
+ private readonly bool $ignoredByBaseline;
/**
* @psalm-param non-empty-string $message
* @psalm-param non-empty-string $file
* @psalm-param positive-int $line
*/
- public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed)
+ public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline)
{
- $this->telemetryInfo = $telemetryInfo;
- $this->test = $test;
- $this->message = $message;
- $this->file = $file;
- $this->line = $line;
- $this->suppressed = $suppressed;
+ $this->telemetryInfo = $telemetryInfo;
+ $this->test = $test;
+ $this->message = $message;
+ $this->file = $file;
+ $this->line = $line;
+ $this->suppressed = $suppressed;
+ $this->ignoredByBaseline = $ignoredByBaseline;
}
public function telemetryInfo(): Telemetry\Info
@@ -95,6 +97,11 @@ public function wasSuppressed(): bool
return $this->suppressed;
}
+ public function ignoredByBaseline(): bool
+ {
+ return $this->ignoredByBaseline;
+ }
+
public function asString(): string
{
$message = $this->message;
@@ -103,9 +110,17 @@ public function asString(): string
$message = PHP_EOL . $message;
}
+ $status = '';
+
+ if ($this->ignoredByBaseline) {
+ $status = 'Baseline-Ignored ';
+ } elseif ($this->suppressed) {
+ $status = 'Suppressed ';
+ }
+
return sprintf(
'Test Triggered %sPHP Deprecation (%s)%s',
- $this->wasSuppressed() ? 'Suppressed ' : '',
+ $status,
$this->test->id(),
$message,
);
diff --git a/src/Event/Events/Test/Issue/PhpNoticeTriggered.php b/src/Event/Events/Test/Issue/PhpNoticeTriggered.php
index d30efb07aa2..f03d0ba9efb 100644
--- a/src/Event/Events/Test/Issue/PhpNoticeTriggered.php
+++ b/src/Event/Events/Test/Issue/PhpNoticeTriggered.php
@@ -40,20 +40,22 @@ final class PhpNoticeTriggered implements Event
*/
private readonly int $line;
private readonly bool $suppressed;
+ private readonly bool $ignoredByBaseline;
/**
* @psalm-param non-empty-string $message
* @psalm-param non-empty-string $file
* @psalm-param positive-int $line
*/
- public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed)
+ public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline)
{
- $this->telemetryInfo = $telemetryInfo;
- $this->test = $test;
- $this->message = $message;
- $this->file = $file;
- $this->line = $line;
- $this->suppressed = $suppressed;
+ $this->telemetryInfo = $telemetryInfo;
+ $this->test = $test;
+ $this->message = $message;
+ $this->file = $file;
+ $this->line = $line;
+ $this->suppressed = $suppressed;
+ $this->ignoredByBaseline = $ignoredByBaseline;
}
public function telemetryInfo(): Telemetry\Info
@@ -95,6 +97,11 @@ public function wasSuppressed(): bool
return $this->suppressed;
}
+ public function ignoredByBaseline(): bool
+ {
+ return $this->ignoredByBaseline;
+ }
+
public function asString(): string
{
$message = $this->message;
@@ -103,9 +110,17 @@ public function asString(): string
$message = PHP_EOL . $message;
}
+ $status = '';
+
+ if ($this->ignoredByBaseline) {
+ $status = 'Baseline-Ignored ';
+ } elseif ($this->suppressed) {
+ $status = 'Suppressed ';
+ }
+
return sprintf(
'Test Triggered %sPHP Notice (%s)%s',
- $this->wasSuppressed() ? 'Suppressed ' : '',
+ $status,
$this->test->id(),
$message,
);
diff --git a/src/Event/Events/Test/Issue/PhpWarningTriggered.php b/src/Event/Events/Test/Issue/PhpWarningTriggered.php
index d0ec61cca55..a93dc73e949 100644
--- a/src/Event/Events/Test/Issue/PhpWarningTriggered.php
+++ b/src/Event/Events/Test/Issue/PhpWarningTriggered.php
@@ -40,20 +40,22 @@ final class PhpWarningTriggered implements Event
*/
private readonly int $line;
private readonly bool $suppressed;
+ private readonly bool $ignoredByBaseline;
/**
* @psalm-param non-empty-string $message
* @psalm-param non-empty-string $file
* @psalm-param positive-int $line
*/
- public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed)
+ public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline)
{
- $this->telemetryInfo = $telemetryInfo;
- $this->test = $test;
- $this->message = $message;
- $this->file = $file;
- $this->line = $line;
- $this->suppressed = $suppressed;
+ $this->telemetryInfo = $telemetryInfo;
+ $this->test = $test;
+ $this->message = $message;
+ $this->file = $file;
+ $this->line = $line;
+ $this->suppressed = $suppressed;
+ $this->ignoredByBaseline = $ignoredByBaseline;
}
public function telemetryInfo(): Telemetry\Info
@@ -95,6 +97,11 @@ public function wasSuppressed(): bool
return $this->suppressed;
}
+ public function ignoredByBaseline(): bool
+ {
+ return $this->ignoredByBaseline;
+ }
+
public function asString(): string
{
$message = $this->message;
@@ -103,9 +110,17 @@ public function asString(): string
$message = PHP_EOL . $message;
}
+ $status = '';
+
+ if ($this->ignoredByBaseline) {
+ $status = 'Baseline-Ignored ';
+ } elseif ($this->suppressed) {
+ $status = 'Suppressed ';
+ }
+
return sprintf(
'Test Triggered %sPHP Warning (%s)%s',
- $this->wasSuppressed() ? 'Suppressed ' : '',
+ $status,
$this->test->id(),
$message,
);
diff --git a/src/Event/Events/Test/Issue/WarningTriggered.php b/src/Event/Events/Test/Issue/WarningTriggered.php
index ddd005c7f4a..9bccafa1e64 100644
--- a/src/Event/Events/Test/Issue/WarningTriggered.php
+++ b/src/Event/Events/Test/Issue/WarningTriggered.php
@@ -40,20 +40,22 @@ final class WarningTriggered implements Event
*/
private readonly int $line;
private readonly bool $suppressed;
+ private readonly bool $ignoredByBaseline;
/**
* @psalm-param non-empty-string $message
* @psalm-param non-empty-string $file
* @psalm-param positive-int $line
*/
- public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed)
+ public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline)
{
- $this->telemetryInfo = $telemetryInfo;
- $this->test = $test;
- $this->message = $message;
- $this->file = $file;
- $this->line = $line;
- $this->suppressed = $suppressed;
+ $this->telemetryInfo = $telemetryInfo;
+ $this->test = $test;
+ $this->message = $message;
+ $this->file = $file;
+ $this->line = $line;
+ $this->suppressed = $suppressed;
+ $this->ignoredByBaseline = $ignoredByBaseline;
}
public function telemetryInfo(): Telemetry\Info
@@ -95,6 +97,11 @@ public function wasSuppressed(): bool
return $this->suppressed;
}
+ public function ignoredByBaseline(): bool
+ {
+ return $this->ignoredByBaseline;
+ }
+
public function asString(): string
{
$message = $this->message;
@@ -103,9 +110,17 @@ public function asString(): string
$message = PHP_EOL . $message;
}
+ $status = '';
+
+ if ($this->ignoredByBaseline) {
+ $status = 'Baseline-Ignored ';
+ } elseif ($this->suppressed) {
+ $status = 'Suppressed ';
+ }
+
return sprintf(
'Test Triggered %sWarning (%s)%s',
- $this->wasSuppressed() ? 'Suppressed ' : '',
+ $status,
$this->test->id(),
$message,
);
diff --git a/src/Runner/Baseline/Baseline.php b/src/Runner/Baseline/Baseline.php
new file mode 100644
index 00000000000..4921f3188fe
--- /dev/null
+++ b/src/Runner/Baseline/Baseline.php
@@ -0,0 +1,59 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\Runner\Baseline;
+
+/**
+ * @internal This class is not covered by the backward compatibility promise for PHPUnit
+ */
+final class Baseline
+{
+ public const VERSION = 1;
+
+ /**
+ * @psalm-var array>>
+ */
+ private array $issues = [];
+
+ public function add(Issue $issue): void
+ {
+ if (!isset($this->issues[$issue->file()])) {
+ $this->issues[$issue->file()] = [];
+ }
+
+ if (!isset($this->issues[$issue->file()][$issue->line()])) {
+ $this->issues[$issue->file()][$issue->line()] = [];
+ }
+
+ $this->issues[$issue->file()][$issue->line()][] = $issue;
+ }
+
+ public function has(Issue $issue): bool
+ {
+ if (!isset($this->issues[$issue->file()][$issue->line()])) {
+ return false;
+ }
+
+ foreach ($this->issues[$issue->file()][$issue->line()] as $_issue) {
+ if ($_issue->equals($issue)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @psalm-return array>>
+ */
+ public function groupedByFileAndLine(): array
+ {
+ return $this->issues;
+ }
+}
diff --git a/src/Runner/Baseline/Exception/CannotLoadBaselineException.php b/src/Runner/Baseline/Exception/CannotLoadBaselineException.php
new file mode 100644
index 00000000000..c05e803e545
--- /dev/null
+++ b/src/Runner/Baseline/Exception/CannotLoadBaselineException.php
@@ -0,0 +1,20 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\Runner\Baseline;
+
+use PHPUnit\Runner\Exception;
+use RuntimeException;
+
+/**
+ * @internal This class is not covered by the backward compatibility promise for PHPUnit
+ */
+final class CannotLoadBaselineException extends RuntimeException implements Exception
+{
+}
diff --git a/src/Runner/Baseline/Exception/FileDoesNotHaveLineException.php b/src/Runner/Baseline/Exception/FileDoesNotHaveLineException.php
new file mode 100644
index 00000000000..1121fa3949e
--- /dev/null
+++ b/src/Runner/Baseline/Exception/FileDoesNotHaveLineException.php
@@ -0,0 +1,31 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\Runner\Baseline;
+
+use function sprintf;
+use PHPUnit\Runner\Exception;
+use RuntimeException;
+
+/**
+ * @internal This class is not covered by the backward compatibility promise for PHPUnit
+ */
+final class FileDoesNotHaveLineException extends RuntimeException implements Exception
+{
+ public function __construct(string $file, int $line)
+ {
+ parent::__construct(
+ sprintf(
+ 'File "%s" does not have line %d',
+ $file,
+ $line,
+ ),
+ );
+ }
+}
diff --git a/src/Runner/Baseline/Generator.php b/src/Runner/Baseline/Generator.php
new file mode 100644
index 00000000000..6a94baebc66
--- /dev/null
+++ b/src/Runner/Baseline/Generator.php
@@ -0,0 +1,80 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\Runner\Baseline;
+
+use PHPUnit\Event\EventFacadeIsSealedException;
+use PHPUnit\Event\Facade;
+use PHPUnit\Event\Test\DeprecationTriggered;
+use PHPUnit\Event\Test\NoticeTriggered;
+use PHPUnit\Event\Test\PhpDeprecationTriggered;
+use PHPUnit\Event\Test\PhpNoticeTriggered;
+use PHPUnit\Event\Test\PhpWarningTriggered;
+use PHPUnit\Event\Test\WarningTriggered;
+use PHPUnit\Event\UnknownSubscriberTypeException;
+use PHPUnit\Runner\FileDoesNotExistException;
+use PHPUnit\TextUI\Configuration\Source;
+use PHPUnit\TextUI\Configuration\SourceFilter;
+
+/**
+ * @internal This class is not covered by the backward compatibility promise for PHPUnit
+ */
+final class Generator
+{
+ private Baseline $baseline;
+ private readonly Source $source;
+
+ /**
+ * @throws EventFacadeIsSealedException
+ * @throws UnknownSubscriberTypeException
+ */
+ public function __construct(Facade $facade, Source $source)
+ {
+ $facade->registerSubscribers(
+ new TestTriggeredDeprecationSubscriber($this),
+ new TestTriggeredNoticeSubscriber($this),
+ new TestTriggeredPhpDeprecationSubscriber($this),
+ new TestTriggeredPhpNoticeSubscriber($this),
+ new TestTriggeredPhpWarningSubscriber($this),
+ new TestTriggeredWarningSubscriber($this),
+ );
+
+ $this->baseline = new Baseline;
+ $this->source = $source;
+ }
+
+ public function baseline(): Baseline
+ {
+ return $this->baseline;
+ }
+
+ /**
+ * @throws FileDoesNotExistException
+ * @throws FileDoesNotHaveLineException
+ */
+ public function testTriggeredIssue(DeprecationTriggered|NoticeTriggered|PhpDeprecationTriggered|PhpNoticeTriggered|PhpWarningTriggered|WarningTriggered $event): void
+ {
+ if (!$this->source->ignoreSuppressionOfPhpWarnings() && $event->wasSuppressed()) {
+ return;
+ }
+
+ if ($this->source->restrictWarnings() && !(new SourceFilter)->includes($this->source, $event->file())) {
+ return;
+ }
+
+ $this->baseline->add(
+ Issue::from(
+ $event->file(),
+ $event->line(),
+ null,
+ $event->message(),
+ ),
+ );
+ }
+}
diff --git a/src/Runner/Baseline/Issue.php b/src/Runner/Baseline/Issue.php
new file mode 100644
index 00000000000..d90b067b982
--- /dev/null
+++ b/src/Runner/Baseline/Issue.php
@@ -0,0 +1,143 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\Runner\Baseline;
+
+use function assert;
+use function file;
+use function is_file;
+use function sha1;
+use PHPUnit\Runner\FileDoesNotExistException;
+
+/**
+ * @internal This class is not covered by the backward compatibility promise for PHPUnit
+ */
+final class Issue
+{
+ /**
+ * @psalm-var non-empty-string
+ */
+ private readonly string $file;
+
+ /**
+ * @psalm-var positive-int
+ */
+ private readonly int $line;
+
+ /**
+ * @psalm-var non-empty-string
+ */
+ private readonly string $hash;
+
+ /**
+ * @psalm-var non-empty-string
+ */
+ private readonly string $description;
+
+ /**
+ * @psalm-param non-empty-string $file
+ * @psalm-param positive-int $line
+ * @psalm-param ?non-empty-string $hash
+ * @psalm-param non-empty-string $description
+ *
+ * @throws FileDoesNotExistException
+ * @throws FileDoesNotHaveLineException
+ */
+ public static function from(string $file, int $line, ?string $hash, string $description): self
+ {
+ if ($hash === null) {
+ $hash = self::calculateHash($file, $line);
+ }
+
+ return new self($file, $line, $hash, $description);
+ }
+
+ /**
+ * @psalm-param non-empty-string $file
+ * @psalm-param positive-int $line
+ * @psalm-param non-empty-string $hash
+ * @psalm-param non-empty-string $description
+ */
+ private function __construct(string $file, int $line, string $hash, string $description)
+ {
+ $this->file = $file;
+ $this->line = $line;
+ $this->hash = $hash;
+ $this->description = $description;
+ }
+
+ /**
+ * @psalm-return non-empty-string
+ */
+ public function file(): string
+ {
+ return $this->file;
+ }
+
+ /**
+ * @psalm-return positive-int
+ */
+ public function line(): int
+ {
+ return $this->line;
+ }
+
+ /**
+ * @psalm-return non-empty-string
+ */
+ public function hash(): string
+ {
+ return $this->hash;
+ }
+
+ /**
+ * @psalm-return non-empty-string
+ */
+ public function description(): string
+ {
+ return $this->description;
+ }
+
+ public function equals(self $other): bool
+ {
+ return $this->file() === $other->file() &&
+ $this->line() === $other->line() &&
+ $this->hash() === $other->hash() &&
+ $this->description() === $other->description();
+ }
+
+ /**
+ * @psalm-param non-empty-string $file
+ * @psalm-param positive-int $line
+ *
+ * @psalm-return non-empty-string
+ *
+ * @throws FileDoesNotExistException
+ * @throws FileDoesNotHaveLineException
+ */
+ private static function calculateHash(string $file, int $line): string
+ {
+ if (!is_file($file)) {
+ throw new FileDoesNotExistException($file);
+ }
+
+ $lines = file($file, FILE_IGNORE_NEW_LINES);
+ $key = $line - 1;
+
+ if (!isset($lines[$key])) {
+ throw new FileDoesNotHaveLineException($file, $line);
+ }
+
+ $hash = sha1($lines[$key]);
+
+ assert(!empty($hash));
+
+ return $hash;
+ }
+}
diff --git a/src/Runner/Baseline/Reader.php b/src/Runner/Baseline/Reader.php
new file mode 100644
index 00000000000..3aa5cfbc89a
--- /dev/null
+++ b/src/Runner/Baseline/Reader.php
@@ -0,0 +1,100 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\Runner\Baseline;
+
+use function assert;
+use function dirname;
+use function file_exists;
+use function realpath;
+use function sprintf;
+use function str_replace;
+use function trim;
+use DOMElement;
+use DOMXPath;
+use PHPUnit\Util\Xml\Loader as XmlLoader;
+use PHPUnit\Util\Xml\XmlException;
+
+/**
+ * @internal This class is not covered by the backward compatibility promise for PHPUnit
+ */
+final class Reader
+{
+ /**
+ * @psalm-param non-empty-string $baselineFile
+ *
+ * @throws CannotLoadBaselineException
+ */
+ public function read(string $baselineFile): Baseline
+ {
+ if (!file_exists($baselineFile)) {
+ throw new CannotLoadBaselineException(
+ sprintf(
+ 'Cannot read baseline %s, file does not exist',
+ $baselineFile,
+ ),
+ );
+ }
+
+ try {
+ $document = (new XmlLoader)->loadFile($baselineFile);
+ } catch (XmlException $e) {
+ throw new CannotLoadBaselineException(
+ sprintf(
+ 'Cannot read baseline: %s',
+ trim($e->getMessage()),
+ ),
+ );
+ }
+
+ $version = (int) $document->documentElement->getAttribute('version');
+
+ if ($version !== Baseline::VERSION) {
+ throw new CannotLoadBaselineException(
+ sprintf(
+ 'Cannot read baseline %s, version %d is not supported',
+ $baselineFile,
+ $version,
+ ),
+ );
+ }
+
+ $baseline = new Baseline;
+ $baselineDirectory = dirname(realpath($baselineFile));
+ $xpath = new DOMXPath($document);
+
+ foreach ($xpath->query('file') as $fileElement) {
+ assert($fileElement instanceof DOMElement);
+
+ $file = $baselineDirectory . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $fileElement->getAttribute('path'));
+
+ foreach ($xpath->query('line', $fileElement) as $lineElement) {
+ assert($lineElement instanceof DOMElement);
+
+ $line = (int) $lineElement->getAttribute('number');
+ $hash = $lineElement->getAttribute('hash');
+
+ foreach ($xpath->query('issue', $lineElement) as $issueElement) {
+ assert($issueElement instanceof DOMElement);
+
+ $description = $issueElement->textContent;
+
+ assert(!empty($file));
+ assert($line > 0);
+ assert(!empty($hash));
+ assert(!empty($description));
+
+ $baseline->add(Issue::from($file, $line, $hash, $description));
+ }
+ }
+ }
+
+ return $baseline;
+ }
+}
diff --git a/src/Runner/Baseline/RelativePathCalculator.php b/src/Runner/Baseline/RelativePathCalculator.php
new file mode 100644
index 00000000000..06ed7057de9
--- /dev/null
+++ b/src/Runner/Baseline/RelativePathCalculator.php
@@ -0,0 +1,103 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\Runner\Baseline;
+
+use function array_fill;
+use function array_merge;
+use function array_slice;
+use function assert;
+use function count;
+use function explode;
+use function implode;
+use function str_replace;
+use function strpos;
+use function substr;
+use function trim;
+
+/**
+ * @internal This class is not covered by the backward compatibility promise for PHPUnit
+ *
+ * @see Copied from https://github.com/phpstan/phpstan-src/blob/1.10.33/src/File/ParentDirectoryRelativePathHelper.php
+ */
+final class RelativePathCalculator
+{
+ /**
+ * @psalm-var non-empty-string $baselineDirectory
+ */
+ private readonly string $baselineDirectory;
+
+ /**
+ * @psalm-param non-empty-string $baselineDirectory
+ */
+ public function __construct(string $baselineDirectory)
+ {
+ $this->baselineDirectory = $baselineDirectory;
+ }
+
+ /**
+ * @psalm-param non-empty-string $filename
+ *
+ * @psalm-return non-empty-string
+ */
+ public function calculate(string $filename): string
+ {
+ $result = implode('/', $this->parts($filename));
+
+ assert($result !== '');
+
+ return $result;
+ }
+
+ /**
+ * @psalm-param non-empty-string $filename
+ *
+ * @psalm-return list
+ */
+ public function parts(string $filename): array
+ {
+ $schemePosition = strpos($filename, '://');
+
+ if ($schemePosition !== false) {
+ $filename = substr($filename, $schemePosition + 3);
+
+ assert($filename !== '');
+ }
+
+ $parentParts = explode('/', trim(str_replace('\\', '/', $this->baselineDirectory), '/'));
+ $parentPartsCount = count($parentParts);
+ $filenameParts = explode('/', trim(str_replace('\\', '/', $filename), '/'));
+ $filenamePartsCount = count($filenameParts);
+
+ $i = 0;
+
+ for (; $i < $filenamePartsCount; $i++) {
+ if ($parentPartsCount < $i + 1) {
+ break;
+ }
+
+ $parentPath = implode('/', array_slice($parentParts, 0, $i + 1));
+ $filenamePath = implode('/', array_slice($filenameParts, 0, $i + 1));
+
+ if ($parentPath !== $filenamePath) {
+ break;
+ }
+ }
+
+ if ($i === 0) {
+ return [$filename];
+ }
+
+ $dotsCount = $parentPartsCount - $i;
+
+ assert($dotsCount >= 0);
+
+ return array_merge(array_fill(0, $dotsCount, '..'), array_slice($filenameParts, $i));
+ }
+}
diff --git a/src/Runner/Baseline/Subscriber/Subscriber.php b/src/Runner/Baseline/Subscriber/Subscriber.php
new file mode 100644
index 00000000000..b3ba386c893
--- /dev/null
+++ b/src/Runner/Baseline/Subscriber/Subscriber.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\Runner\Baseline;
+
+/**
+ * @internal This class is not covered by the backward compatibility promise for PHPUnit
+ */
+abstract class Subscriber
+{
+ private readonly Generator $generator;
+
+ public function __construct(Generator $generator)
+ {
+ $this->generator = $generator;
+ }
+
+ protected function generator(): Generator
+ {
+ return $this->generator;
+ }
+}
diff --git a/src/Runner/Baseline/Subscriber/TestTriggeredDeprecationSubscriber.php b/src/Runner/Baseline/Subscriber/TestTriggeredDeprecationSubscriber.php
new file mode 100644
index 00000000000..f26ed2ecf31
--- /dev/null
+++ b/src/Runner/Baseline/Subscriber/TestTriggeredDeprecationSubscriber.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\Runner\Baseline;
+
+use PHPUnit\Event\Test\DeprecationTriggered;
+use PHPUnit\Event\Test\DeprecationTriggeredSubscriber;
+use PHPUnit\Runner\FileDoesNotExistException;
+
+/**
+ * @internal This class is not covered by the backward compatibility promise for PHPUnit
+ */
+final class TestTriggeredDeprecationSubscriber extends Subscriber implements DeprecationTriggeredSubscriber
+{
+ /**
+ * @throws FileDoesNotExistException
+ * @throws FileDoesNotHaveLineException
+ */
+ public function notify(DeprecationTriggered $event): void
+ {
+ $this->generator()->testTriggeredIssue($event);
+ }
+}
diff --git a/src/Runner/Baseline/Subscriber/TestTriggeredNoticeSubscriber.php b/src/Runner/Baseline/Subscriber/TestTriggeredNoticeSubscriber.php
new file mode 100644
index 00000000000..a531fbcad9c
--- /dev/null
+++ b/src/Runner/Baseline/Subscriber/TestTriggeredNoticeSubscriber.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\Runner\Baseline;
+
+use PHPUnit\Event\Test\NoticeTriggered;
+use PHPUnit\Event\Test\NoticeTriggeredSubscriber;
+use PHPUnit\Runner\FileDoesNotExistException;
+
+/**
+ * @internal This class is not covered by the backward compatibility promise for PHPUnit
+ */
+final class TestTriggeredNoticeSubscriber extends Subscriber implements NoticeTriggeredSubscriber
+{
+ /**
+ * @throws FileDoesNotExistException
+ * @throws FileDoesNotHaveLineException
+ */
+ public function notify(NoticeTriggered $event): void
+ {
+ $this->generator()->testTriggeredIssue($event);
+ }
+}
diff --git a/src/Runner/Baseline/Subscriber/TestTriggeredPhpDeprecationSubscriber.php b/src/Runner/Baseline/Subscriber/TestTriggeredPhpDeprecationSubscriber.php
new file mode 100644
index 00000000000..a7a5d9f117d
--- /dev/null
+++ b/src/Runner/Baseline/Subscriber/TestTriggeredPhpDeprecationSubscriber.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\Runner\Baseline;
+
+use PHPUnit\Event\Test\PhpDeprecationTriggered;
+use PHPUnit\Event\Test\PhpDeprecationTriggeredSubscriber;
+use PHPUnit\Runner\FileDoesNotExistException;
+
+/**
+ * @internal This class is not covered by the backward compatibility promise for PHPUnit
+ */
+final class TestTriggeredPhpDeprecationSubscriber extends Subscriber implements PhpDeprecationTriggeredSubscriber
+{
+ /**
+ * @throws FileDoesNotExistException
+ * @throws FileDoesNotHaveLineException
+ */
+ public function notify(PhpDeprecationTriggered $event): void
+ {
+ $this->generator()->testTriggeredIssue($event);
+ }
+}
diff --git a/src/Runner/Baseline/Subscriber/TestTriggeredPhpNoticeSubscriber.php b/src/Runner/Baseline/Subscriber/TestTriggeredPhpNoticeSubscriber.php
new file mode 100644
index 00000000000..26085fb63d0
--- /dev/null
+++ b/src/Runner/Baseline/Subscriber/TestTriggeredPhpNoticeSubscriber.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\Runner\Baseline;
+
+use PHPUnit\Event\Test\PhpNoticeTriggered;
+use PHPUnit\Event\Test\PhpNoticeTriggeredSubscriber;
+use PHPUnit\Runner\FileDoesNotExistException;
+
+/**
+ * @internal This class is not covered by the backward compatibility promise for PHPUnit
+ */
+final class TestTriggeredPhpNoticeSubscriber extends Subscriber implements PhpNoticeTriggeredSubscriber
+{
+ /**
+ * @throws FileDoesNotExistException
+ * @throws FileDoesNotHaveLineException
+ */
+ public function notify(PhpNoticeTriggered $event): void
+ {
+ $this->generator()->testTriggeredIssue($event);
+ }
+}
diff --git a/src/Runner/Baseline/Subscriber/TestTriggeredPhpWarningSubscriber.php b/src/Runner/Baseline/Subscriber/TestTriggeredPhpWarningSubscriber.php
new file mode 100644
index 00000000000..a0e617b4f5f
--- /dev/null
+++ b/src/Runner/Baseline/Subscriber/TestTriggeredPhpWarningSubscriber.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\Runner\Baseline;
+
+use PHPUnit\Event\Test\PhpWarningTriggered;
+use PHPUnit\Event\Test\PhpWarningTriggeredSubscriber;
+use PHPUnit\Runner\FileDoesNotExistException;
+
+/**
+ * @internal This class is not covered by the backward compatibility promise for PHPUnit
+ */
+final class TestTriggeredPhpWarningSubscriber extends Subscriber implements PhpWarningTriggeredSubscriber
+{
+ /**
+ * @throws FileDoesNotExistException
+ * @throws FileDoesNotHaveLineException
+ */
+ public function notify(PhpWarningTriggered $event): void
+ {
+ $this->generator()->testTriggeredIssue($event);
+ }
+}
diff --git a/src/Runner/Baseline/Subscriber/TestTriggeredWarningSubscriber.php b/src/Runner/Baseline/Subscriber/TestTriggeredWarningSubscriber.php
new file mode 100644
index 00000000000..793b7149138
--- /dev/null
+++ b/src/Runner/Baseline/Subscriber/TestTriggeredWarningSubscriber.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\Runner\Baseline;
+
+use PHPUnit\Event\Test\WarningTriggered;
+use PHPUnit\Event\Test\WarningTriggeredSubscriber;
+use PHPUnit\Runner\FileDoesNotExistException;
+
+/**
+ * @internal This class is not covered by the backward compatibility promise for PHPUnit
+ */
+final class TestTriggeredWarningSubscriber extends Subscriber implements WarningTriggeredSubscriber
+{
+ /**
+ * @throws FileDoesNotExistException
+ * @throws FileDoesNotHaveLineException
+ */
+ public function notify(WarningTriggered $event): void
+ {
+ $this->generator()->testTriggeredIssue($event);
+ }
+}
diff --git a/src/Runner/Baseline/Writer.php b/src/Runner/Baseline/Writer.php
new file mode 100644
index 00000000000..28540930068
--- /dev/null
+++ b/src/Runner/Baseline/Writer.php
@@ -0,0 +1,65 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\Runner\Baseline;
+
+use function assert;
+use function dirname;
+use function file_put_contents;
+use XMLWriter;
+
+/**
+ * @internal This class is not covered by the backward compatibility promise for PHPUnit
+ */
+final class Writer
+{
+ /**
+ * @psalm-param non-empty-string $baselineFile
+ */
+ public function write(string $baselineFile, Baseline $baseline): void
+ {
+ $pathCalculator = new RelativePathCalculator(dirname($baselineFile));
+
+ $writer = new XMLWriter;
+
+ $writer->openMemory();
+ $writer->setIndent(true);
+ $writer->startDocument();
+
+ $writer->startElement('files');
+ $writer->writeAttribute('version', (string) Baseline::VERSION);
+
+ foreach ($baseline->groupedByFileAndLine() as $file => $lines) {
+ assert(!empty($file));
+
+ $writer->startElement('file');
+ $writer->writeAttribute('path', $pathCalculator->calculate($file));
+
+ foreach ($lines as $line => $issues) {
+ $writer->startElement('line');
+ $writer->writeAttribute('number', (string) $line);
+ $writer->writeAttribute('hash', $issues[0]->hash());
+
+ foreach ($issues as $issue) {
+ $writer->startElement('issue');
+ $writer->writeCData($issue->description());
+ $writer->endElement();
+ }
+
+ $writer->endElement();
+ }
+
+ $writer->endElement();
+ }
+
+ $writer->endElement();
+
+ file_put_contents($baselineFile, $writer->outputMemory());
+ }
+}
diff --git a/src/Runner/ErrorHandler.php b/src/Runner/ErrorHandler.php
index 517f8472e89..f8ac78fc2f8 100644
--- a/src/Runner/ErrorHandler.php
+++ b/src/Runner/ErrorHandler.php
@@ -21,6 +21,8 @@
use function set_error_handler;
use PHPUnit\Event;
use PHPUnit\Event\Code\NoTestCaseObjectOnCallStackException;
+use PHPUnit\Runner\Baseline\Baseline;
+use PHPUnit\Runner\Baseline\Issue;
use PHPUnit\Util\ExcludeList;
/**
@@ -29,6 +31,7 @@
final class ErrorHandler
{
private static ?self $instance = null;
+ private ?Baseline $baseline = null;
private bool $enabled = false;
public static function instance(): self
@@ -47,6 +50,8 @@ public function __invoke(int $errorNumber, string $errorString, string $errorFil
return false;
}
+ $ignoredByBaseline = $this->ignoredByBaseline($errorFile, $errorLine, $errorString);
+
switch ($errorNumber) {
case E_NOTICE:
case E_STRICT:
@@ -56,6 +61,7 @@ public function __invoke(int $errorNumber, string $errorString, string $errorFil
$errorFile,
$errorLine,
$suppressed,
+ $ignoredByBaseline,
);
break;
@@ -67,6 +73,7 @@ public function __invoke(int $errorNumber, string $errorString, string $errorFil
$errorFile,
$errorLine,
$suppressed,
+ $ignoredByBaseline,
);
break;
@@ -78,6 +85,7 @@ public function __invoke(int $errorNumber, string $errorString, string $errorFil
$errorFile,
$errorLine,
$suppressed,
+ $ignoredByBaseline,
);
break;
@@ -89,6 +97,7 @@ public function __invoke(int $errorNumber, string $errorString, string $errorFil
$errorFile,
$errorLine,
$suppressed,
+ $ignoredByBaseline,
);
break;
@@ -100,6 +109,7 @@ public function __invoke(int $errorNumber, string $errorString, string $errorFil
$errorFile,
$errorLine,
$suppressed,
+ $ignoredByBaseline,
);
break;
@@ -111,6 +121,7 @@ public function __invoke(int $errorNumber, string $errorString, string $errorFil
$errorFile,
$errorLine,
$suppressed,
+ $ignoredByBaseline,
);
break;
@@ -160,4 +171,23 @@ public function disable(): void
$this->enabled = false;
}
+
+ public function use(Baseline $baseline): void
+ {
+ $this->baseline = $baseline;
+ }
+
+ /**
+ * @psalm-param non-empty-string $file
+ * @psalm-param positive-int $line
+ * @psalm-param non-empty-string $description
+ */
+ private function ignoredByBaseline(string $file, int $line, string $description): bool
+ {
+ if ($this->baseline === null) {
+ return false;
+ }
+
+ return $this->baseline->has(Issue::from($file, $line, null, $description));
+ }
}
diff --git a/src/Runner/TestResult/Collector.php b/src/Runner/TestResult/Collector.php
index 6ca23f33df9..d06aadd2fe6 100644
--- a/src/Runner/TestResult/Collector.php
+++ b/src/Runner/TestResult/Collector.php
@@ -342,6 +342,10 @@ public function testConsideredRisky(ConsideredRisky $event): void
public function testTriggeredDeprecation(DeprecationTriggered $event): void
{
+ if ($event->ignoredByBaseline()) {
+ return;
+ }
+
if (!$this->source->ignoreSuppressionOfDeprecations() && $event->wasSuppressed()) {
return;
}
@@ -368,6 +372,10 @@ public function testTriggeredDeprecation(DeprecationTriggered $event): void
public function testTriggeredPhpDeprecation(PhpDeprecationTriggered $event): void
{
+ if ($event->ignoredByBaseline()) {
+ return;
+ }
+
if (!$this->source->ignoreSuppressionOfPhpDeprecations() && $event->wasSuppressed()) {
return;
}
@@ -425,6 +433,10 @@ public function testTriggeredError(ErrorTriggered $event): void
public function testTriggeredNotice(NoticeTriggered $event): void
{
+ if ($event->ignoredByBaseline()) {
+ return;
+ }
+
if (!$this->source->ignoreSuppressionOfNotices() && $event->wasSuppressed()) {
return;
}
@@ -451,6 +463,10 @@ public function testTriggeredNotice(NoticeTriggered $event): void
public function testTriggeredPhpNotice(PhpNoticeTriggered $event): void
{
+ if ($event->ignoredByBaseline()) {
+ return;
+ }
+
if (!$this->source->ignoreSuppressionOfPhpNotices() && $event->wasSuppressed()) {
return;
}
@@ -477,6 +493,10 @@ public function testTriggeredPhpNotice(PhpNoticeTriggered $event): void
public function testTriggeredWarning(WarningTriggered $event): void
{
+ if ($event->ignoredByBaseline()) {
+ return;
+ }
+
if (!$this->source->ignoreSuppressionOfWarnings() && $event->wasSuppressed()) {
return;
}
@@ -503,6 +523,10 @@ public function testTriggeredWarning(WarningTriggered $event): void
public function testTriggeredPhpWarning(PhpWarningTriggered $event): void
{
+ if ($event->ignoredByBaseline()) {
+ return;
+ }
+
if (!$this->source->ignoreSuppressionOfPhpWarnings() && $event->wasSuppressed()) {
return;
}
diff --git a/src/TextUI/Application.php b/src/TextUI/Application.php
index a430a372af1..f4d0a8b7144 100644
--- a/src/TextUI/Application.php
+++ b/src/TextUI/Application.php
@@ -28,7 +28,12 @@
use PHPUnit\Logging\TestDox\PlainTextRenderer as TestDoxTextRenderer;
use PHPUnit\Logging\TestDox\TestResultCollector as TestDoxResultCollector;
use PHPUnit\Metadata\Api\CodeCoverage as CodeCoverageMetadataApi;
+use PHPUnit\Runner\Baseline\CannotLoadBaselineException;
+use PHPUnit\Runner\Baseline\Generator as BaselineGenerator;
+use PHPUnit\Runner\Baseline\Reader;
+use PHPUnit\Runner\Baseline\Writer;
use PHPUnit\Runner\CodeCoverage;
+use PHPUnit\Runner\ErrorHandler;
use PHPUnit\Runner\Extension\ExtensionBootstrapper;
use PHPUnit\Runner\Extension\Facade as ExtensionFacade;
use PHPUnit\Runner\Extension\PharLoader;
@@ -166,6 +171,8 @@ public function run(array $argv): int
);
}
+ $baselineGenerator = $this->configureBaseline($configuration);
+
EventFacade::instance()->seal();
$timer = new Timer;
@@ -209,6 +216,20 @@ public function run(array $argv): int
CodeCoverage::instance()->generateReports($printer, $configuration);
+ if (isset($baselineGenerator)) {
+ (new Writer)->write(
+ $configuration->generateBaseline(),
+ $baselineGenerator->baseline(),
+ );
+
+ $printer->print(
+ sprintf(
+ PHP_EOL . 'Baseline written to %s.' . PHP_EOL,
+ realpath($configuration->generateBaseline()),
+ ),
+ );
+ }
+
$shellExitCode = (new ShellExitCodeCalculator)->calculate(
$configuration->failOnDeprecation(),
$configuration->failOnEmptyTestSuite(),
@@ -596,4 +617,36 @@ private function initializeTestResultCache(Configuration $configuration): Result
return new NullResultCache;
}
+
+ /**
+ * @throws EventFacadeIsSealedException
+ * @throws UnknownSubscriberTypeException
+ */
+ private function configureBaseline(Configuration $configuration): ?BaselineGenerator
+ {
+ if ($configuration->hasGenerateBaseline()) {
+ return new BaselineGenerator(
+ EventFacade::instance(),
+ $configuration->source(),
+ );
+ }
+
+ if ($configuration->source()->useBaseline()) {
+ /** @psalm-suppress MissingThrowsDocblock */
+ $baselineFile = $configuration->source()->baseline();
+ $baseline = null;
+
+ try {
+ $baseline = (new Reader)->read($baselineFile);
+ } catch (CannotLoadBaselineException $e) {
+ EventFacade::emitter()->testRunnerTriggeredWarning($e->getMessage());
+ }
+
+ if ($baseline !== null) {
+ ErrorHandler::instance()->use($baseline);
+ }
+ }
+
+ return null;
+ }
}
diff --git a/src/TextUI/Configuration/Cli/Builder.php b/src/TextUI/Configuration/Cli/Builder.php
index 2add6a8a9c1..08f14a26df6 100644
--- a/src/TextUI/Configuration/Cli/Builder.php
+++ b/src/TextUI/Configuration/Cli/Builder.php
@@ -55,6 +55,9 @@ final class Builder
'enforce-time-limit',
'exclude-group=',
'filter=',
+ 'generate-baseline=',
+ 'use-baseline=',
+ 'ignore-baseline',
'generate-configuration',
'globals-backup',
'group=',
@@ -193,6 +196,9 @@ public function fromParameters(array $parameters): Configuration
$stopOnSkipped = null;
$stopOnWarning = null;
$filter = null;
+ $generateBaseline = null;
+ $useBaseline = null;
+ $ignoreBaseline = false;
$generateConfiguration = false;
$migrateConfiguration = false;
$groups = null;
@@ -369,6 +375,21 @@ public function fromParameters(array $parameters): Configuration
break;
+ case '--generate-baseline':
+ $generateBaseline = $option[1];
+
+ break;
+
+ case '--use-baseline':
+ $useBaseline = $option[1];
+
+ break;
+
+ case '--ignore-baseline':
+ $ignoreBaseline = true;
+
+ break;
+
case '--generate-configuration':
$generateConfiguration = true;
@@ -840,6 +861,9 @@ public function fromParameters(array $parameters): Configuration
$stopOnSkipped,
$stopOnWarning,
$filter,
+ $generateBaseline,
+ $useBaseline,
+ $ignoreBaseline,
$generateConfiguration,
$migrateConfiguration,
$groups,
diff --git a/src/TextUI/Configuration/Cli/Configuration.php b/src/TextUI/Configuration/Cli/Configuration.php
index ec84bd5de53..cca980eadaa 100644
--- a/src/TextUI/Configuration/Cli/Configuration.php
+++ b/src/TextUI/Configuration/Cli/Configuration.php
@@ -69,6 +69,9 @@ final class Configuration
private readonly ?bool $stopOnSkipped;
private readonly ?bool $stopOnWarning;
private readonly ?string $filter;
+ private readonly ?string $generateBaseline;
+ private readonly ?string $useBaseline;
+ private readonly bool $ignoreBaseline;
private readonly bool $generateConfiguration;
private readonly bool $migrateConfiguration;
private readonly ?array $groups;
@@ -122,7 +125,7 @@ final class Configuration
* @psalm-param list $arguments
* @psalm-param ?non-empty-list $testSuffixes
*/
- public function __construct(array $arguments, ?string $atLeastVersion, ?bool $backupGlobals, ?bool $backupStaticProperties, ?bool $beStrictAboutChangesToGlobalState, ?string $bootstrap, ?string $cacheDirectory, ?bool $cacheResult, ?string $cacheResultFile, bool $checkVersion, ?string $colors, null|int|string $columns, ?string $configurationFile, ?string $coverageClover, ?string $coverageCobertura, ?string $coverageCrap4J, ?string $coverageHtml, ?string $coveragePhp, ?string $coverageText, ?bool $coverageTextShowUncoveredFiles, ?bool $coverageTextShowOnlySummary, ?string $coverageXml, ?bool $pathCoverage, ?string $coverageCacheDirectory, bool $warmCoverageCache, ?int $defaultTimeLimit, ?bool $disableCodeCoverageIgnore, ?bool $disallowTestOutput, ?bool $enforceTimeLimit, ?array $excludeGroups, ?int $executionOrder, ?int $executionOrderDefects, ?bool $failOnDeprecation, ?bool $failOnEmptyTestSuite, ?bool $failOnIncomplete, ?bool $failOnNotice, ?bool $failOnRisky, ?bool $failOnSkipped, ?bool $failOnWarning, ?bool $stopOnDefect, ?bool $stopOnDeprecation, ?bool $stopOnError, ?bool $stopOnFailure, ?bool $stopOnIncomplete, ?bool $stopOnNotice, ?bool $stopOnRisky, ?bool $stopOnSkipped, ?bool $stopOnWarning, ?string $filter, bool $generateConfiguration, bool $migrateConfiguration, ?array $groups, ?array $testsCovering, ?array $testsUsing, bool $help, ?string $includePath, ?array $iniSettings, ?string $junitLogfile, bool $listGroups, bool $listSuites, bool $listTests, ?string $listTestsXml, ?bool $noCoverage, ?bool $noExtensions, ?bool $noOutput, ?bool $noProgress, ?bool $noResults, ?bool $noLogging, ?bool $processIsolation, ?int $randomOrderSeed, ?bool $reportUselessTests, ?bool $resolveDependencies, ?bool $reverseList, ?bool $stderr, ?bool $strictCoverage, ?string $teamcityLogfile, ?string $testdoxHtmlFile, ?string $testdoxTextFile, ?array $testSuffixes, ?string $testSuite, ?string $excludeTestSuite, bool $useDefaultConfiguration, ?bool $displayDetailsOnIncompleteTests, ?bool $displayDetailsOnSkippedTests, ?bool $displayDetailsOnTestsThatTriggerDeprecations, ?bool $displayDetailsOnTestsThatTriggerErrors, ?bool $displayDetailsOnTestsThatTriggerNotices, ?bool $displayDetailsOnTestsThatTriggerWarnings, bool $version, ?array $coverageFilter, ?string $logEventsText, ?string $logEventsVerboseText, ?bool $printerTeamCity, ?bool $printerTestDox)
+ public function __construct(array $arguments, ?string $atLeastVersion, ?bool $backupGlobals, ?bool $backupStaticProperties, ?bool $beStrictAboutChangesToGlobalState, ?string $bootstrap, ?string $cacheDirectory, ?bool $cacheResult, ?string $cacheResultFile, bool $checkVersion, ?string $colors, null|int|string $columns, ?string $configurationFile, ?string $coverageClover, ?string $coverageCobertura, ?string $coverageCrap4J, ?string $coverageHtml, ?string $coveragePhp, ?string $coverageText, ?bool $coverageTextShowUncoveredFiles, ?bool $coverageTextShowOnlySummary, ?string $coverageXml, ?bool $pathCoverage, ?string $coverageCacheDirectory, bool $warmCoverageCache, ?int $defaultTimeLimit, ?bool $disableCodeCoverageIgnore, ?bool $disallowTestOutput, ?bool $enforceTimeLimit, ?array $excludeGroups, ?int $executionOrder, ?int $executionOrderDefects, ?bool $failOnDeprecation, ?bool $failOnEmptyTestSuite, ?bool $failOnIncomplete, ?bool $failOnNotice, ?bool $failOnRisky, ?bool $failOnSkipped, ?bool $failOnWarning, ?bool $stopOnDefect, ?bool $stopOnDeprecation, ?bool $stopOnError, ?bool $stopOnFailure, ?bool $stopOnIncomplete, ?bool $stopOnNotice, ?bool $stopOnRisky, ?bool $stopOnSkipped, ?bool $stopOnWarning, ?string $filter, ?string $generateBaseline, ?string $useBaseline, bool $ignoreBaseline, bool $generateConfiguration, bool $migrateConfiguration, ?array $groups, ?array $testsCovering, ?array $testsUsing, bool $help, ?string $includePath, ?array $iniSettings, ?string $junitLogfile, bool $listGroups, bool $listSuites, bool $listTests, ?string $listTestsXml, ?bool $noCoverage, ?bool $noExtensions, ?bool $noOutput, ?bool $noProgress, ?bool $noResults, ?bool $noLogging, ?bool $processIsolation, ?int $randomOrderSeed, ?bool $reportUselessTests, ?bool $resolveDependencies, ?bool $reverseList, ?bool $stderr, ?bool $strictCoverage, ?string $teamcityLogfile, ?string $testdoxHtmlFile, ?string $testdoxTextFile, ?array $testSuffixes, ?string $testSuite, ?string $excludeTestSuite, bool $useDefaultConfiguration, ?bool $displayDetailsOnIncompleteTests, ?bool $displayDetailsOnSkippedTests, ?bool $displayDetailsOnTestsThatTriggerDeprecations, ?bool $displayDetailsOnTestsThatTriggerErrors, ?bool $displayDetailsOnTestsThatTriggerNotices, ?bool $displayDetailsOnTestsThatTriggerWarnings, bool $version, ?array $coverageFilter, ?string $logEventsText, ?string $logEventsVerboseText, ?bool $printerTeamCity, ?bool $printerTestDox)
{
$this->arguments = $arguments;
$this->atLeastVersion = $atLeastVersion;
@@ -174,6 +177,9 @@ public function __construct(array $arguments, ?string $atLeastVersion, ?bool $ba
$this->stopOnSkipped = $stopOnSkipped;
$this->stopOnWarning = $stopOnWarning;
$this->filter = $filter;
+ $this->generateBaseline = $generateBaseline;
+ $this->useBaseline = $useBaseline;
+ $this->ignoreBaseline = $ignoreBaseline;
$this->generateConfiguration = $generateConfiguration;
$this->migrateConfiguration = $migrateConfiguration;
$this->groups = $groups;
@@ -1186,6 +1192,51 @@ public function filter(): string
return $this->filter;
}
+ /**
+ * @psalm-assert-if-true !null $this->generateBaseline
+ */
+ public function hasGenerateBaseline(): bool
+ {
+ return $this->generateBaseline !== null;
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function generateBaseline(): string
+ {
+ if (!$this->hasGenerateBaseline()) {
+ throw new Exception;
+ }
+
+ return $this->generateBaseline;
+ }
+
+ /**
+ * @psalm-assert-if-true !null $this->useBaseline
+ */
+ public function hasUseBaseline(): bool
+ {
+ return $this->useBaseline !== null;
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function useBaseline(): string
+ {
+ if (!$this->hasUseBaseline()) {
+ throw new Exception;
+ }
+
+ return $this->useBaseline;
+ }
+
+ public function ignoreBaseline(): bool
+ {
+ return $this->ignoreBaseline;
+ }
+
public function generateConfiguration(): bool
{
return $this->generateConfiguration;
diff --git a/src/TextUI/Configuration/Configuration.php b/src/TextUI/Configuration/Configuration.php
index 30043d837d7..72cea86e9e2 100644
--- a/src/TextUI/Configuration/Configuration.php
+++ b/src/TextUI/Configuration/Configuration.php
@@ -137,6 +137,7 @@ final class Configuration
private readonly Php $php;
private readonly bool $controlGarbageCollector;
private readonly int $numberOfTestsBeforeGarbageCollection;
+ private readonly ?string $generateBaseline;
/**
* @psalm-param list $cliArguments
@@ -144,7 +145,7 @@ final class Configuration
* @psalm-param non-empty-list $testSuffixes
* @psalm-param list}> $extensionBootstrappers
*/
- public function __construct(array $cliArguments, ?string $configurationFile, ?string $bootstrap, bool $cacheResult, ?string $cacheDirectory, ?string $coverageCacheDirectory, Source $source, string $testResultCacheFile, ?string $coverageClover, ?string $coverageCobertura, ?string $coverageCrap4j, int $coverageCrap4jThreshold, ?string $coverageHtml, int $coverageHtmlLowUpperBound, int $coverageHtmlHighLowerBound, string $coverageHtmlColorSuccessLow, string $coverageHtmlColorSuccessMedium, string $coverageHtmlColorSuccessHigh, string $coverageHtmlColorWarning, string $coverageHtmlColorDanger, ?string $coverageHtmlCustomCssFile, ?string $coveragePhp, ?string $coverageText, bool $coverageTextShowUncoveredFiles, bool $coverageTextShowOnlySummary, ?string $coverageXml, bool $pathCoverage, bool $ignoreDeprecatedCodeUnitsFromCodeCoverage, bool $disableCodeCoverageIgnore, bool $failOnDeprecation, bool $failOnEmptyTestSuite, bool $failOnIncomplete, bool $failOnNotice, bool $failOnRisky, bool $failOnSkipped, bool $failOnWarning, bool $stopOnDefect, bool $stopOnDeprecation, bool $stopOnError, bool $stopOnFailure, bool $stopOnIncomplete, bool $stopOnNotice, bool $stopOnRisky, bool $stopOnSkipped, bool $stopOnWarning, bool $outputToStandardErrorStream, int|string $columns, bool $noExtensions, ?string $pharExtensionDirectory, array $extensionBootstrappers, bool $backupGlobals, bool $backupStaticProperties, bool $beStrictAboutChangesToGlobalState, bool $colors, bool $processIsolation, bool $enforceTimeLimit, int $defaultTimeLimit, int $timeoutForSmallTests, int $timeoutForMediumTests, int $timeoutForLargeTests, bool $reportUselessTests, bool $strictCoverage, bool $disallowTestOutput, bool $displayDetailsOnIncompleteTests, bool $displayDetailsOnSkippedTests, bool $displayDetailsOnTestsThatTriggerDeprecations, bool $displayDetailsOnTestsThatTriggerErrors, bool $displayDetailsOnTestsThatTriggerNotices, bool $displayDetailsOnTestsThatTriggerWarnings, bool $reverseDefectList, bool $requireCoverageMetadata, bool $registerMockObjectsFromTestArgumentsRecursively, bool $noProgress, bool $noResults, bool $noOutput, int $executionOrder, int $executionOrderDefects, bool $resolveDependencies, ?string $logfileTeamcity, ?string $logfileJunit, ?string $logfileTestdoxHtml, ?string $logfileTestdoxText, ?string $logEventsText, ?string $logEventsVerboseText, bool $teamCityOutput, bool $testDoxOutput, ?array $testsCovering, ?array $testsUsing, ?string $filter, ?array $groups, ?array $excludeGroups, int $randomOrderSeed, bool $includeUncoveredFiles, TestSuiteCollection $testSuite, string $includeTestSuite, string $excludeTestSuite, ?string $defaultTestSuite, array $testSuffixes, Php $php, bool $controlGarbageCollector, int $numberOfTestsBeforeGarbageCollection)
+ public function __construct(array $cliArguments, ?string $configurationFile, ?string $bootstrap, bool $cacheResult, ?string $cacheDirectory, ?string $coverageCacheDirectory, Source $source, string $testResultCacheFile, ?string $coverageClover, ?string $coverageCobertura, ?string $coverageCrap4j, int $coverageCrap4jThreshold, ?string $coverageHtml, int $coverageHtmlLowUpperBound, int $coverageHtmlHighLowerBound, string $coverageHtmlColorSuccessLow, string $coverageHtmlColorSuccessMedium, string $coverageHtmlColorSuccessHigh, string $coverageHtmlColorWarning, string $coverageHtmlColorDanger, ?string $coverageHtmlCustomCssFile, ?string $coveragePhp, ?string $coverageText, bool $coverageTextShowUncoveredFiles, bool $coverageTextShowOnlySummary, ?string $coverageXml, bool $pathCoverage, bool $ignoreDeprecatedCodeUnitsFromCodeCoverage, bool $disableCodeCoverageIgnore, bool $failOnDeprecation, bool $failOnEmptyTestSuite, bool $failOnIncomplete, bool $failOnNotice, bool $failOnRisky, bool $failOnSkipped, bool $failOnWarning, bool $stopOnDefect, bool $stopOnDeprecation, bool $stopOnError, bool $stopOnFailure, bool $stopOnIncomplete, bool $stopOnNotice, bool $stopOnRisky, bool $stopOnSkipped, bool $stopOnWarning, bool $outputToStandardErrorStream, int|string $columns, bool $noExtensions, ?string $pharExtensionDirectory, array $extensionBootstrappers, bool $backupGlobals, bool $backupStaticProperties, bool $beStrictAboutChangesToGlobalState, bool $colors, bool $processIsolation, bool $enforceTimeLimit, int $defaultTimeLimit, int $timeoutForSmallTests, int $timeoutForMediumTests, int $timeoutForLargeTests, bool $reportUselessTests, bool $strictCoverage, bool $disallowTestOutput, bool $displayDetailsOnIncompleteTests, bool $displayDetailsOnSkippedTests, bool $displayDetailsOnTestsThatTriggerDeprecations, bool $displayDetailsOnTestsThatTriggerErrors, bool $displayDetailsOnTestsThatTriggerNotices, bool $displayDetailsOnTestsThatTriggerWarnings, bool $reverseDefectList, bool $requireCoverageMetadata, bool $registerMockObjectsFromTestArgumentsRecursively, bool $noProgress, bool $noResults, bool $noOutput, int $executionOrder, int $executionOrderDefects, bool $resolveDependencies, ?string $logfileTeamcity, ?string $logfileJunit, ?string $logfileTestdoxHtml, ?string $logfileTestdoxText, ?string $logEventsText, ?string $logEventsVerboseText, bool $teamCityOutput, bool $testDoxOutput, ?array $testsCovering, ?array $testsUsing, ?string $filter, ?array $groups, ?array $excludeGroups, int $randomOrderSeed, bool $includeUncoveredFiles, TestSuiteCollection $testSuite, string $includeTestSuite, string $excludeTestSuite, ?string $defaultTestSuite, array $testSuffixes, Php $php, bool $controlGarbageCollector, int $numberOfTestsBeforeGarbageCollection, ?string $generateBaseline)
{
$this->cliArguments = $cliArguments;
$this->configurationFile = $configurationFile;
@@ -247,6 +248,7 @@ public function __construct(array $cliArguments, ?string $configurationFile, ?st
$this->php = $php;
$this->controlGarbageCollector = $controlGarbageCollector;
$this->numberOfTestsBeforeGarbageCollection = $numberOfTestsBeforeGarbageCollection;
+ $this->generateBaseline = $generateBaseline;
}
/**
@@ -1260,4 +1262,24 @@ public function numberOfTestsBeforeGarbageCollection(): int
{
return $this->numberOfTestsBeforeGarbageCollection;
}
+
+ /**
+ * @psalm-assert-if-true !null $this->generateBaseline
+ */
+ public function hasGenerateBaseline(): bool
+ {
+ return $this->generateBaseline !== null;
+ }
+
+ /**
+ * @throws NoBaselineException
+ */
+ public function generateBaseline(): string
+ {
+ if (!$this->hasGenerateBaseline()) {
+ throw new NoBaselineException;
+ }
+
+ return $this->generateBaseline;
+ }
}
diff --git a/src/TextUI/Configuration/Exception/NoBaselineException.php b/src/TextUI/Configuration/Exception/NoBaselineException.php
new file mode 100644
index 00000000000..eb8cf3ba174
--- /dev/null
+++ b/src/TextUI/Configuration/Exception/NoBaselineException.php
@@ -0,0 +1,19 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\TextUI\Configuration;
+
+use RuntimeException;
+
+/**
+ * @internal This class is not covered by the backward compatibility promise for PHPUnit
+ */
+final class NoBaselineException extends RuntimeException implements Exception
+{
+}
diff --git a/src/TextUI/Configuration/Merger.php b/src/TextUI/Configuration/Merger.php
index 1e778dfa3c7..287b1916e48 100644
--- a/src/TextUI/Configuration/Merger.php
+++ b/src/TextUI/Configuration/Merger.php
@@ -705,6 +705,22 @@ public function merge(CliConfiguration $cliConfiguration, XmlConfiguration $xmlC
$sourceExcludeFiles = $xmlConfiguration->source()->excludeFiles();
}
+ $useBaseline = null;
+ $generateBaseline = null;
+
+ if (!$cliConfiguration->hasGenerateBaseline()) {
+ if ($cliConfiguration->hasUseBaseline()) {
+ $useBaseline = $cliConfiguration->useBaseline();
+ } elseif ($xmlConfiguration->source()->hasBaseline()) {
+ $useBaseline = $xmlConfiguration->source()->baseline();
+ }
+ } else {
+ $generateBaseline = $cliConfiguration->generateBaseline();
+ }
+
+ assert($useBaseline !== '');
+ assert($generateBaseline !== '');
+
return new Configuration(
$cliConfiguration->arguments(),
$configurationFile,
@@ -713,6 +729,8 @@ public function merge(CliConfiguration $cliConfiguration, XmlConfiguration $xmlC
$cacheDirectory,
$coverageCacheDirectory,
new Source(
+ $useBaseline,
+ $cliConfiguration->ignoreBaseline(),
FilterDirectoryCollection::fromArray($sourceIncludeDirectories),
$sourceIncludeFiles,
$sourceExcludeDirectories,
@@ -834,6 +852,7 @@ public function merge(CliConfiguration $cliConfiguration, XmlConfiguration $xmlC
),
$xmlConfiguration->phpunit()->controlGarbageCollector(),
$xmlConfiguration->phpunit()->numberOfTestsBeforeGarbageCollection(),
+ $generateBaseline,
);
}
}
diff --git a/src/TextUI/Configuration/Value/Source.php b/src/TextUI/Configuration/Value/Source.php
index 8b5ddfce7a4..b22edba8508 100644
--- a/src/TextUI/Configuration/Value/Source.php
+++ b/src/TextUI/Configuration/Value/Source.php
@@ -16,6 +16,11 @@
*/
final class Source
{
+ /**
+ * @psalm-var non-empty-string
+ */
+ private readonly ?string $baseline;
+ private readonly bool $ignoreBaseline;
private readonly FilterDirectoryCollection $includeDirectories;
private readonly FileCollection $includeFiles;
private readonly FilterDirectoryCollection $excludeDirectories;
@@ -31,8 +36,13 @@ final class Source
private readonly bool $ignoreSuppressionOfWarnings;
private readonly bool $ignoreSuppressionOfPhpWarnings;
- public function __construct(FilterDirectoryCollection $includeDirectories, FileCollection $includeFiles, FilterDirectoryCollection $excludeDirectories, FileCollection $excludeFiles, bool $restrictDeprecations, bool $restrictNotices, bool $restrictWarnings, bool $ignoreSuppressionOfDeprecations, bool $ignoreSuppressionOfPhpDeprecations, bool $ignoreSuppressionOfErrors, bool $ignoreSuppressionOfNotices, bool $ignoreSuppressionOfPhpNotices, bool $ignoreSuppressionOfWarnings, bool $ignoreSuppressionOfPhpWarnings)
+ /**
+ * @psalm-param non-empty-string $baseline
+ */
+ public function __construct(?string $baseline, bool $ignoreBaseline, FilterDirectoryCollection $includeDirectories, FileCollection $includeFiles, FilterDirectoryCollection $excludeDirectories, FileCollection $excludeFiles, bool $restrictDeprecations, bool $restrictNotices, bool $restrictWarnings, bool $ignoreSuppressionOfDeprecations, bool $ignoreSuppressionOfPhpDeprecations, bool $ignoreSuppressionOfErrors, bool $ignoreSuppressionOfNotices, bool $ignoreSuppressionOfPhpNotices, bool $ignoreSuppressionOfWarnings, bool $ignoreSuppressionOfPhpWarnings)
{
+ $this->baseline = $baseline;
+ $this->ignoreBaseline = $ignoreBaseline;
$this->includeDirectories = $includeDirectories;
$this->includeFiles = $includeFiles;
$this->excludeDirectories = $excludeDirectories;
@@ -49,6 +59,36 @@ public function __construct(FilterDirectoryCollection $includeDirectories, FileC
$this->ignoreSuppressionOfPhpWarnings = $ignoreSuppressionOfPhpWarnings;
}
+ /**
+ * @psalm-assert-if-true !null $this->baseline
+ */
+ public function useBaseline(): bool
+ {
+ return $this->hasBaseline() && !$this->ignoreBaseline;
+ }
+
+ /**
+ * @psalm-assert-if-true !null $this->baseline
+ */
+ public function hasBaseline(): bool
+ {
+ return $this->baseline !== null;
+ }
+
+ /**
+ * @psalm-return non-empty-string
+ *
+ * @throws NoBaselineException
+ */
+ public function baseline(): string
+ {
+ if (!$this->hasBaseline()) {
+ throw new NoBaselineException;
+ }
+
+ return $this->baseline;
+ }
+
public function includeDirectories(): FilterDirectoryCollection
{
return $this->includeDirectories;
diff --git a/src/TextUI/Configuration/Xml/DefaultConfiguration.php b/src/TextUI/Configuration/Xml/DefaultConfiguration.php
index 00f73810d20..5c652e95b32 100644
--- a/src/TextUI/Configuration/Xml/DefaultConfiguration.php
+++ b/src/TextUI/Configuration/Xml/DefaultConfiguration.php
@@ -36,6 +36,8 @@ public static function create(): self
return new self(
ExtensionBootstrapCollection::fromArray([]),
new Source(
+ null,
+ false,
CodeCoverageFilterDirectoryCollection::fromArray([]),
FileCollection::fromArray([]),
CodeCoverageFilterDirectoryCollection::fromArray([]),
diff --git a/src/TextUI/Configuration/Xml/Loader.php b/src/TextUI/Configuration/Xml/Loader.php
index 7294777d7b9..2c219292fb5 100644
--- a/src/TextUI/Configuration/Xml/Loader.php
+++ b/src/TextUI/Configuration/Xml/Loader.php
@@ -244,6 +244,7 @@ private function toAbsolutePath(string $filename, string $path): string
private function source(string $filename, DOMXPath $xpath): Source
{
+ $baseline = null;
$restrictDeprecations = false;
$restrictNotices = false;
$restrictWarnings = false;
@@ -258,6 +259,12 @@ private function source(string $filename, DOMXPath $xpath): Source
$element = $this->element($xpath, 'source');
if ($element) {
+ $baseline = $this->getStringAttribute($element, 'baseline');
+
+ if ($baseline !== null) {
+ $baseline = $this->toAbsolutePath($filename, $baseline);
+ }
+
$restrictDeprecations = $this->getBooleanAttribute($element, 'restrictDeprecations', false);
$restrictNotices = $this->getBooleanAttribute($element, 'restrictNotices', false);
$restrictWarnings = $this->getBooleanAttribute($element, 'restrictWarnings', false);
@@ -271,6 +278,8 @@ private function source(string $filename, DOMXPath $xpath): Source
}
return new Source(
+ $baseline,
+ false,
$this->readFilterDirectories($filename, $xpath, 'source/include/directory'),
$this->readFilterFiles($filename, $xpath, 'source/include/file'),
$this->readFilterDirectories($filename, $xpath, 'source/exclude/directory'),
diff --git a/src/TextUI/Help.php b/src/TextUI/Help.php
index 075986d263a..23b4ab121ca 100644
--- a/src/TextUI/Help.php
+++ b/src/TextUI/Help.php
@@ -42,6 +42,9 @@ final class Help
['arg' => '--cache-directory ', 'desc' => 'Specify cache directory'],
['arg' => '--generate-configuration', 'desc' => 'Generate configuration file with suggested settings'],
['arg' => '--migrate-configuration', 'desc' => 'Migrate configuration file to current format'],
+ ['arg' => '--generate-baseline ', 'desc' => 'Generate baseline for issues'],
+ ['arg' => '--use-baseline ', 'desc' => 'Use baseline to ignore issues'],
+ ['arg' => '--ignore-baseline', 'desc' => 'Do not use baseline to ignore issues'],
],
'Selection' => [
diff --git a/src/TextUI/Output/Default/ProgressPrinter/ProgressPrinter.php b/src/TextUI/Output/Default/ProgressPrinter/ProgressPrinter.php
index b7ddf8f99bb..f94f078edbd 100644
--- a/src/TextUI/Output/Default/ProgressPrinter/ProgressPrinter.php
+++ b/src/TextUI/Output/Default/ProgressPrinter/ProgressPrinter.php
@@ -99,6 +99,10 @@ public function testMarkedIncomplete(): void
public function testTriggeredNotice(NoticeTriggered $event): void
{
+ if ($event->ignoredByBaseline()) {
+ return;
+ }
+
if ($this->source->restrictNotices() &&
!(new SourceFilter)->includes($this->source, $event->file())) {
return;
@@ -113,6 +117,10 @@ public function testTriggeredNotice(NoticeTriggered $event): void
public function testTriggeredPhpNotice(PhpNoticeTriggered $event): void
{
+ if ($event->ignoredByBaseline()) {
+ return;
+ }
+
if ($this->source->restrictNotices() &&
!(new SourceFilter)->includes($this->source, $event->file())) {
return;
@@ -127,6 +135,10 @@ public function testTriggeredPhpNotice(PhpNoticeTriggered $event): void
public function testTriggeredDeprecation(DeprecationTriggered $event): void
{
+ if ($event->ignoredByBaseline()) {
+ return;
+ }
+
if ($this->source->restrictDeprecations() &&
!(new SourceFilter)->includes($this->source, $event->file())) {
return;
@@ -141,6 +153,10 @@ public function testTriggeredDeprecation(DeprecationTriggered $event): void
public function testTriggeredPhpDeprecation(PhpDeprecationTriggered $event): void
{
+ if ($event->ignoredByBaseline()) {
+ return;
+ }
+
if ($this->source->restrictDeprecations() &&
!(new SourceFilter)->includes($this->source, $event->file())) {
return;
@@ -165,6 +181,10 @@ public function testConsideredRisky(): void
public function testTriggeredWarning(WarningTriggered $event): void
{
+ if ($event->ignoredByBaseline()) {
+ return;
+ }
+
if ($this->source->restrictWarnings() &&
!(new SourceFilter)->includes($this->source, $event->file())) {
return;
@@ -179,6 +199,10 @@ public function testTriggeredWarning(WarningTriggered $event): void
public function testTriggeredPhpWarning(PhpWarningTriggered $event): void
{
+ if ($event->ignoredByBaseline()) {
+ return;
+ }
+
if ($this->source->restrictWarnings() &&
!(new SourceFilter)->includes($this->source, $event->file())) {
return;
diff --git a/tests/_files/FileWithIssue.php b/tests/_files/FileWithIssue.php
new file mode 100644
index 00000000000..caaa2ce65e7
--- /dev/null
+++ b/tests/_files/FileWithIssue.php
@@ -0,0 +1,14 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace PHPUnit\TestFixture;
+
+final class FileWithIssue
+{
+}
diff --git a/tests/_files/baseline/FileWithIssues.php b/tests/_files/baseline/FileWithIssues.php
new file mode 100644
index 00000000000..13bdbb6af19
--- /dev/null
+++ b/tests/_files/baseline/FileWithIssues.php
@@ -0,0 +1,11 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+$a = $b;
+$b = $c;
diff --git a/tests/_files/baseline/expected.xml b/tests/_files/baseline/expected.xml
new file mode 100644
index 00000000000..f41e43f5995
--- /dev/null
+++ b/tests/_files/baseline/expected.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/_files/configuration_codecoverage.xml b/tests/_files/configuration_codecoverage.xml
index 016c2b88214..5b9cb80971b 100644
--- a/tests/_files/configuration_codecoverage.xml
+++ b/tests/_files/configuration_codecoverage.xml
@@ -1,7 +1,7 @@
-