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