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:
115: $messages[] = $message;
116: }
117:
118: $missingMessages = [];
119: foreach ($previousMessages as $previousMessage) {
120: foreach ($messagesBeforeDiffing as $message) {
121: if (
122: $previousMessage['message'] === $message['message']
123: && $previousMessage['line'] === $message['line']
124: ) {
125: continue 2;
126: }
127: }
128:
129: unset($previousMessage['tip']);
130:
131: $missingMessages[] = $previousMessage;
132: }
133:
134: $previousMessages = array_merge($previousMessages, $messages);
135: $expectedJsonFile = sprintf('%s/%s-%d%s.json', $this->getDataPath(), $topic, $level, $this->getResultSuffix());
136:
137: $exception = $this->compareFiles($expectedJsonFile, $messages);
138: if ($exception !== null) {
139: $exceptions[] = $exception;
140: }
141:
142: $expectedJsonMissingFile = sprintf('%s/%s-%d-missing%s.json', $this->getDataPath(), $topic, $level, $this->getResultSuffix());
143: $exception = $this->compareFiles($expectedJsonMissingFile, $missingMessages);
144: if ($exception === null) {
145: continue;
146: }
147:
148: $exceptions[] = $exception;
149: }
150:
151: if (count($exceptions) > 0) {
152: throw $exceptions[0];
153: }
154: }
155:
156: /**
157: * @return string[]
158: */
159: public function getAdditionalAnalysedFiles(): array
160: {
161: return [];
162: }
163:
164: /**
165: * @param string[] $expectedMessages
166: */
167: private function compareFiles(string $expectedJsonFile, array $expectedMessages): ?AssertionFailedError
168: {
169: if (count($expectedMessages) === 0) {
170: try {
171: self::assertFileDoesNotExist($expectedJsonFile);
172: return null;
173: } catch (AssertionFailedError $e) {
174: unlink($expectedJsonFile);
175: return $e;
176: }
177: }
178:
179: $actualOutput = Json::encode($expectedMessages, Json::PRETTY);
180:
181: try {
182: $this->assertJsonStringEqualsJsonFile(
183: $expectedJsonFile,
184: $actualOutput,
185: );
186: } catch (AssertionFailedError $e) {
187: FileWriter::write($expectedJsonFile, $actualOutput);
188: return $e;
189: }
190:
191: return null;
192: }
193:
194: public static function assertFileDoesNotExist(string $filename, string $message = ''): void
195: {
196: if (!method_exists(parent::class, 'assertFileDoesNotExist')) {
197: parent::assertFileNotExists($filename, $message);
198: return;
199: }
200:
201: parent::assertFileDoesNotExist($filename, $message);
202: }
203:
204: }
205: