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