diff --git a/src/Analyser/Parallel/ParallelAnalysisNodeExtractor.php b/src/Analyser/Parallel/ParallelAnalysisNodeExtractor.php index c5360ca6..5819eccf 100644 --- a/src/Analyser/Parallel/ParallelAnalysisNodeExtractor.php +++ b/src/Analyser/Parallel/ParallelAnalysisNodeExtractor.php @@ -553,10 +553,22 @@ private function workerFailure(string $error, string $stderrFile): RuntimeExcept */ private function createWorkerFiles(string $dir): array { + $created = []; + + try { + $created[] = $inputFile = $this->temporaryFile($dir); + $created[] = $outputFile = $this->temporaryFile($dir); + $stderrFile = $this->temporaryFile($dir); + } catch (RuntimeException $runtimeException) { + $this->cleanup($created); + + throw $runtimeException; + } + return [ - 'inputFile' => $this->temporaryFile($dir), - 'outputFile' => $this->temporaryFile($dir), - 'stderrFile' => $this->temporaryFile($dir), + 'inputFile' => $inputFile, + 'outputFile' => $outputFile, + 'stderrFile' => $stderrFile, ]; } diff --git a/tests/Analyser/Parallel/MockFunctions.php b/tests/Analyser/Parallel/MockFunctions.php index fb11ef00..c5ac6195 100644 --- a/tests/Analyser/Parallel/MockFunctions.php +++ b/tests/Analyser/Parallel/MockFunctions.php @@ -76,6 +76,11 @@ function tempnam(string $directory, string $prefix): string|false return false; } + // an int allows that many successful calls before failing + if (is_int($GLOBALS['mock_tempnam']) && $GLOBALS['mock_tempnam']-- <= 0) { + return false; + } + $result = \tempnam($directory, $prefix); if ($result !== false) { diff --git a/tests/Analyser/Parallel/ParallelAnalysisNodeExtractorTest.php b/tests/Analyser/Parallel/ParallelAnalysisNodeExtractorTest.php index cd207221..2217e4ec 100644 --- a/tests/Analyser/Parallel/ParallelAnalysisNodeExtractorTest.php +++ b/tests/Analyser/Parallel/ParallelAnalysisNodeExtractorTest.php @@ -736,6 +736,38 @@ public function testExtractThrowsWhenTempnamFails(): void } } + public function testExtractRemovesCreatedWorkerFilesWhenLaterTempnamFails(): void + { + $GLOBALS['mock_missing_functions'] = ['pcntl_fork']; + $GLOBALS['mock_tempnam'] = 1; + $GLOBALS['mock_tracked_tempnam_files'] = []; + + $dir = $this->makeTemporaryDirectory('structarmed-parallel-test'); + $file = $dir . '/Foo.php'; + file_put_contents($file, 'extract([$file]); + $this->fail('Expected RuntimeException was not thrown.'); + } catch (RuntimeException $runtimeException) { + $this->assertSame( + 'Unable to create temporary file for parallel analysis.', + $runtimeException->getMessage() + ); + + /** @var list $created */ + $created = $GLOBALS['mock_tracked_tempnam_files']; + $this->assertCount(1, $created); + $this->assertFileDoesNotExist($created[0]); + } finally { + $GLOBALS['mock_missing_functions'] = []; + $GLOBALS['mock_tempnam'] = false; + $GLOBALS['mock_tracked_tempnam_files'] = []; + } + } + public function testExtractThrowsWhenPayloadIsInvalid(): void { $GLOBALS['mock_file_get_contents_payload'] = ['invalid' => 'payload'];