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