1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Testing;
4:
5: use Nette\Utils\Json;
6: use Nette\Utils\JsonException;
7: use PHPStan\File\FileHelper;
8: use PHPStan\File\FileWriter;
9: use PHPStan\ShouldNotHappenException;
10: use PHPUnit\Framework\AssertionFailedError;
11: use PHPUnit\Framework\Attributes\DataProvider;
12: use PHPUnit\Framework\TestCase;
13: use function array_merge;
14: use function count;
15: use function escapeshellarg;
16: use function escapeshellcmd;
17: use function exec;
18: use function implode;
19: use function method_exists;
20: use function putenv;
21: use function range;
22: use function sprintf;
23: use function unlink;
24: use const DIRECTORY_SEPARATOR;
25: use const PHP_BINARY;
26:
27: /** @api */
28: abstract class LevelsTestCase extends TestCase
29: {
30:
31: /**
32: * @return array<array<string>>
33: */
34: abstract public static function dataTopics(): array;
35:
36: abstract public function getDataPath(): string;
37:
38: abstract public function getPhpStanExecutablePath(): string;
39:
40: abstract public function getPhpStanConfigPath(): ?string;
41:
42: protected function getResultSuffix(): string
43: {
44: return '';
45: }
46:
47: protected function shouldAutoloadAnalysedFile(): bool
48: {
49: return true;
50: }
51:
52: #[DataProvider('dataTopics')]
53: public function testLevels(
54: string $topic,
55: ): void
56: {
57: $file = sprintf('%s' . DIRECTORY_SEPARATOR . '%s.php', $this->getDataPath(), $topic);
58: $command = escapeshellcmd($this->getPhpStanExecutablePath());
59: $configPath = $this->getPhpStanConfigPath();
60: $fileHelper = new FileHelper(__DIR__ . '/../..');
61:
62: $previousMessages = [];
63:
64: $exceptions = [];
65:
66: exec(sprintf('%s %s clear-result-cache %s 2>&1', escapeshellarg(PHP_BINARY), $command, $configPath !== null ? '--configuration ' . escapeshellarg($configPath) : ''), $clearResultCacheOutputLines, $clearResultCacheExitCode);
67: if ($clearResultCacheExitCode !== 0) {
68: throw new ShouldNotHappenException('Could not clear result cache: ' . implode("\n", $clearResultCacheOutputLines));
69: }
70:
71: putenv('__PHPSTAN_FORCE_VALIDATE_STUB_FILES=1');
72:
73: foreach (range(0, 10) as $level) {
74: unset($outputLines);
75: exec(sprintf('%s %s analyse --no-progress --error-format=prettyJson --level=%d %s %s %s', escapeshellarg(PHP_BINARY), $command, $level, $configPath !== null ? '--configuration ' . escapeshellarg($configPath) : '', $this->shouldAutoloadAnalysedFile() ? sprintf('--autoload-file %s', escapeshellarg($file)) : '', escapeshellarg($file)), $outputLines);
76:
77: $output = implode("\n", $outputLines);
78:
79: try {
80: $actualJson = Json::decode($output, Json::FORCE_ARRAY);
81: } catch (JsonException) {
82: throw new JsonException(sprintf('Cannot decode: %s', $output));
83: }
84: if (count($actualJson['files']) > 0) {
85: $normalizedFilePath = $fileHelper->normalizePath($file);
86: if (!isset($actualJson['files'][$normalizedFilePath])) {
87: $messagesBeforeDiffing = [];
88: } else {
89: $messagesBeforeDiffing = $actualJson['files'][$normalizedFilePath]['messages'];
90: }
91:
92: foreach ($this->getAdditionalAnalysedFiles() as $additionalAnalysedFile) {
93: $normalizedAdditionalFilePath = $fileHelper->normalizePath($additionalAnalysedFile);
94: if (!isset($actualJson['files'][$normalizedAdditionalFilePath])) {
95: continue;
96: }
97:
98: $messagesBeforeDiffing = array_merge($messagesBeforeDiffing, $actualJson['files'][$normalizedAdditionalFilePath]['messages']);
99: }
100: } else {
101: $messagesBeforeDiffing = [];
102: }
103:
104: $messages = [];
105: foreach ($messagesBeforeDiffing as $message) {
106: foreach ($previousMessages as $lastMessage) {
107: if (
108: $message['message'] === $lastMessage['message']
109: && $message['line'] === $lastMessage['line']
110: ) {
111: continue 2;
112: }
113: }
114:
115: unset($message['tip']);
116: unset($message['identifier']);
117:
118: $messages[] = $message;
119: }
120:
121: $missingMessages = [];
122: foreach ($previousMessages as $previousMessage) {
123: foreach ($messagesBeforeDiffing as $message) {
124: if (
125: $previousMessage['message'] === $message['message']
126: && $previousMessage['line'] === $message['line']
127: ) {
128: continue 2;
129: }
130: }
131:
132: unset($previousMessage['tip']);
133:
134: $missingMessages[] = $previousMessage;
135: }
136:
137: $previousMessages = array_merge($previousMessages, $messages);
138: $expectedJsonFile = sprintf('%s/%s-%d%s.json', $this->getDataPath(), $topic, $level, $this->getResultSuffix());
139:
140: $exception = $this->compareFiles($expectedJsonFile, $messages);
141: if ($exception !== null) {
142: $exceptions[] = $exception;
143: }
144:
145: $expectedJsonMissingFile = sprintf('%s/%s-%d-missing%s.json', $this->getDataPath(), $topic, $level, $this->getResultSuffix());
146: $exception = $this->compareFiles($expectedJsonMissingFile, $missingMessages);
147: if ($exception === null) {
148: continue;
149: }
150:
151: $exceptions[] = $exception;
152: }
153:
154: if (count($exceptions) > 0) {
155: throw $exceptions[0];
156: }
157: }
158:
159: /**
160: * @return string[]
161: */
162: public function getAdditionalAnalysedFiles(): array
163: {
164: return [];
165: }
166:
167: /**
168: * @param string[] $expectedMessages
169: */
170: private function compareFiles(string $expectedJsonFile, array $expectedMessages): ?AssertionFailedError
171: {
172: if (count($expectedMessages) === 0) {
173: try {
174: self::ourCustomAssertFileDoesNotExist($expectedJsonFile);
175: return null;
176: } catch (AssertionFailedError $e) {
177: unlink($expectedJsonFile);
178: return $e;
179: }
180: }
181:
182: $actualOutput = Json::encode($expectedMessages, Json::PRETTY);
183:
184: try {
185: $this->assertJsonStringEqualsJsonFile(
186: $expectedJsonFile,
187: $actualOutput,
188: );
189: } catch (AssertionFailedError $e) {
190: FileWriter::write($expectedJsonFile, $actualOutput);
191: return $e;
192: }
193:
194: return null;
195: }
196:
197: public static function ourCustomAssertFileDoesNotExist(string $filename, string $message = ''): void
198: {
199: // this method is no longer called assertFileDoesNotExist because this method is final in PHPUnit 10
200: if (!method_exists(parent::class, 'assertFileDoesNotExist')) {
201: parent::assertFileNotExists($filename, $message);
202: return;
203: }
204:
205: parent::assertFileDoesNotExist($filename, $message);
206: }
207:
208: }
209: