From 11d3f9800fa5da173811aafccb03d3e62e14ca76 Mon Sep 17 00:00:00 2001 From: Wtyd Date: Sat, 11 Jan 2025 23:29:24 +0000 Subject: [PATCH] gh-18 fix(MultiProcessExecution) Improved error output when crashes some tool --- .../Execution/MultiProcessesExecution.php | 32 +++-- .../Process/Execution/ProcessExecution.php | 3 + .../Execution/ProcessExecutionAbstract.php | 9 +- src/Tools/Process/ExecutionFakeTrait.php | 51 +++++++ src/Tools/Process/ProcessFake.php | 82 +++++++++-- .../Execution/MultiProcessesExecutionTest.php | 136 +++++++++++++----- 6 files changed, 250 insertions(+), 63 deletions(-) diff --git a/src/Tools/Process/Execution/MultiProcessesExecution.php b/src/Tools/Process/Execution/MultiProcessesExecution.php index 6d3ffa4..72ea026 100644 --- a/src/Tools/Process/Execution/MultiProcessesExecution.php +++ b/src/Tools/Process/Execution/MultiProcessesExecution.php @@ -4,7 +4,9 @@ namespace Wtyd\GitHooks\Tools\Process\Execution; +use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessTimedOutException; +use Throwable; use Wtyd\GitHooks\Tools\Errors; use Wtyd\GitHooks\Tools\Process\Process; @@ -29,19 +31,26 @@ public function runProcesses(): Errors do { try { $this->addProcessToQueue(); - foreach ($this->runningProcesses as $toolName => $process) { - if (!$process->isTerminated()) { - continue; + // $process->checkTimeout(); // timeout is null for now + // throw new \Exception('asdfasdf'); + if ($process->isTerminated()) { + $this->numberOfRunnedProcesses = $this->finishExecution($process, $toolName); } - $this->numberOfRunnedProcesses = $this->finishExecution($process, $toolName); } } catch (ProcessTimedOutException $th) { $toolName = (string)array_search($th->getProcess(), $this->processes); $this->numberOfRunnedProcesses = $this->finishExecution($th->getProcess(), $toolName, $th->getMessage()); + } catch (ProcessFailedException $th) { + // dd($this->numberOfRunnedProcesses); + $toolName = (string)array_search($th->getProcess(), $this->processes); + $this->numberOfRunnedProcesses = $this->finishExecution($th->getProcess(), $toolName); + } catch (Throwable $th) { + $this->errors->setError('Tool crash', $th->getMessage()); } } while ($totalProcesses > $this->numberOfRunnedProcesses); } catch (\Throwable $th) { + // dd($th->getMessage(), get_class($th), $th->getFile(), $th->getLine()); $this->errors->setError('General', $th->getMessage()); } $endCommandExecution = microtime(true); @@ -50,6 +59,9 @@ public function runProcesses(): Errors return $this->errors; } + /** + * Add process to queue of running processes + */ protected function addProcessToQueue(): void { foreach ($this->processes as $toolName => $process) { @@ -65,7 +77,8 @@ protected function addProcessToQueue(): void /** * Finish process execution - * + * ¡¡¡Warning with egde case!!! Sometimes the process finishes with an error message in the normal output + * (sintaxys errors in Phpmd 2.9 or minus, for example) but the error output is empty. * @param Process $process * @param string $toolName Name of the process. * @param string $exceptionMessage @@ -75,16 +88,19 @@ protected function finishExecution(Process $process, string $toolName, string $e { $this->runnedProcesses[$toolName] = $process; $executionTime = $this->executionTime($process->getLastOutputTime(), $process->getStartTime()); - if ($process->isSuccessful()) { $this->printer->resultSuccess($this->getSuccessString($toolName, $executionTime)); } else { - $errorMessage = $exceptionMessage ?? $process->getOutput(); + $errorMessage = ''; + // if ($process->isTerminated()) { // TODO: comprobar a fondo estas lineas + $errorMessage = $exceptionMessage ?? $process->getErrorOutput(); + $errorMessage = empty($errorMessage) ? $process->getOutput() : $errorMessage; // Edge case + // } if (!$this->tools[$toolName]->isIgnoreErrorsOnExit()) { $this->errors->setError($toolName, $errorMessage); } - $this->printer->resultError($this->getErrorString($toolName, $executionTime)); + $this->printer->resultError($this->getErrorString($toolName, $executionTime)); $this->printer->line($errorMessage); } unset($this->runningProcesses[$toolName]); diff --git a/src/Tools/Process/Execution/ProcessExecution.php b/src/Tools/Process/Execution/ProcessExecution.php index f061e45..84edf09 100644 --- a/src/Tools/Process/Execution/ProcessExecution.php +++ b/src/Tools/Process/Execution/ProcessExecution.php @@ -10,6 +10,9 @@ class ProcessExecution extends ProcessExecutionAbstract { public function runProcesses(): Errors { + if (empty($this->tools)) { + return $this->errors; + } $toolName = array_keys($this->tools)[0]; $process = $this->processes[$toolName]; diff --git a/src/Tools/Process/Execution/ProcessExecutionAbstract.php b/src/Tools/Process/Execution/ProcessExecutionAbstract.php index 14ca14f..8255321 100644 --- a/src/Tools/Process/Execution/ProcessExecutionAbstract.php +++ b/src/Tools/Process/Execution/ProcessExecutionAbstract.php @@ -43,15 +43,8 @@ public function execute(array $tools, int $threads): Errors { $this->tools = $tools; $this->threads = $threads; - $this->createProcesses(); - - try { - return $this->runProcesses(); - } catch (\Throwable $th) { - $this->errors->setError('General', $th->getMessage()); - return $this->errors; - } + return $this->runProcesses(); } abstract protected function runProcesses(): Errors; diff --git a/src/Tools/Process/ExecutionFakeTrait.php b/src/Tools/Process/ExecutionFakeTrait.php index 8a8b15c..b563ac6 100644 --- a/src/Tools/Process/ExecutionFakeTrait.php +++ b/src/Tools/Process/ExecutionFakeTrait.php @@ -15,6 +15,15 @@ trait ExecutionFakeTrait /** @var array<\Wtyd\GitHooks\Tools\Tool\ToolAbstact> */ protected $toolsThatMustFail = []; + /** @var array<\Wtyd\GitHooks\Tools\Tool\ToolAbstact> */ + protected $failedToolsByException = []; + + /** @var array<\Wtyd\GitHooks\Tools\Tool\ToolAbstact> */ + protected $failedToolsByFoundedErrors = []; + + /** @var array<\Wtyd\GitHooks\Tools\Tool\ToolAbstact> */ + protected $setFailByFoundedErrorsInNormalOutput = []; + /** @var array<\Wtyd\GitHooks\Tools\Tool\ToolAbstact> */ protected $toolsWithTimeout = []; @@ -26,18 +35,60 @@ protected function createProcesses(): void foreach ($this->toolsThatMustFail as $tool) { $this->processes[$tool]->setFail(); } + + foreach ($this->failedToolsByException as $tool) { + $this->processes[$tool]->setFailByException(); + } + + foreach ($this->failedToolsByFoundedErrors as $tool) { + $this->processes[$tool]->setFailByFoundedErrors(); + } + + foreach ($this->setFailByFoundedErrorsInNormalOutput as $tool) { + $this->processes[$tool]->setFailByFoundedErrorsInNormalOutput(); + } foreach ($this->toolsWithTimeout as $tool) { $this->processes[$tool]->triggerTimeout(); } } + // // TODO deprecated? public function setToolsThatMustFail(array $toolsThatMustFail): void { $this->toolsThatMustFail = $toolsThatMustFail; } + public function failedToolsByException(array $failedToolsByException): void + { + $this->failedToolsByException = $failedToolsByException; + } + + public function failedToolsByFoundedErrors(array $failedToolsByFoundedErrors): void + { + $this->failedToolsByFoundedErrors = $failedToolsByFoundedErrors; + } + + public function setFailByFoundedErrorsInNormalOutput(array $setFailByFoundedErrorsInNormalOutput): void + { + $this->setFailByFoundedErrorsInNormalOutput = $setFailByFoundedErrorsInNormalOutput; + } + public function setToolsWithTimeout(array $toolsWithTimeout): void { $this->toolsWithTimeout = $toolsWithTimeout; } + + protected function addProcessToQueue(): void + { + foreach ($this->processes as $toolName => $process) { + if (count($this->runningProcesses) === $this->threads) { + break; + } + if (!in_array($process, $this->runningProcesses) && !in_array($process, $this->runnedProcesses)) { + $this->startProcess($process); + $this->runningProcesses[$toolName] = $process; + } + } + parent::addProcessToQueue(); + } } diff --git a/src/Tools/Process/ProcessFake.php b/src/Tools/Process/ProcessFake.php index ac0741c..1e30ccb 100644 --- a/src/Tools/Process/ProcessFake.php +++ b/src/Tools/Process/ProcessFake.php @@ -4,6 +4,7 @@ namespace Wtyd\GitHooks\Tools\Process; +use Symfony\Component\Process\Exception\ProcessFailedException; use Wtyd\GitHooks\Tools\Tool\ToolAbstract; class ProcessFake extends Process @@ -18,9 +19,15 @@ class ProcessFake extends Process private $isSuccessful = true; /** @inheritDoc */ - private $output; + protected $status; + + /** @inheritDoc */ + private $exitcode; private $fakeTimeout = false; + private $outputFake; + private $errorOutputFake; + private $mustRaiseException = false; /** * Do nothing or invokes original method when we want to cause an error by timeout @@ -34,6 +41,10 @@ public function start(callable $callback = null, array $env = []) // Do nothing $this->starttime = microtime(true); } + $this->status = self::STATUS_STARTED; + if ($this->mustRaiseException) { + throw new ProcessFailedException($this); + } } /** @@ -80,12 +91,26 @@ public function isSuccessful(): bool } /** - * Only in MultiProcessExecution when the execution fails * @inheritDoc */ public function getOutput(): string { - return $this->output; + // dd(! $this->isSuccessful, $this->outputFake); + if (! $this->isSuccessful) { + return $this->outputFake; + } + return ''; + } + + /** + * @inheritDoc + */ + public function getErrorOutput(): string + { + if (! $this->isSuccessful) { + return $this->errorOutputFake; + } + return ''; } /** @@ -97,17 +122,10 @@ public function wait(callable $callback = null): int return $this->isSuccessful ? 0 : 1; } - /** - * Mocks that the process fails. - * - * @return ProcessFake - */ - public function setFail(): ProcessFake + private function extractToolName(): string { - $this->isSuccessful = false; $ex = explode(' ', $this->getCommandLine()); - $tools = array_keys(ToolAbstract::SUPPORTED_TOOLS); $nameTool = ''; foreach ($tools as $tool) { @@ -116,9 +134,49 @@ public function setFail(): ProcessFake break; } } + return $nameTool; + } + + /** + * Mocks that the process fails. + * + * @return ProcessFake + */ + public function setFail(): ProcessFake + { + $this->isSuccessful = false; + $nameTool = $this->extractToolName(); + + // getErrorOutput() o getOutput() o Exeception + $this->errorOutputFake = "\nThe tool $nameTool mocks an error\n"; + + return $this; + } - $this->output = "\nThe tool $nameTool mocks an error\n"; + public function setFailByException(): ProcessFake + { + $this->isSuccessful = false; + $this->mustRaiseException = true; + $nameTool = $this->extractToolName(); + $this->outputFake = $this->errorOutputFake = "$nameTool fakes an exception"; + $this->exitcode = 1; + return $this; + } + public function setFailByFoundedErrors(): ProcessFake + { + $this->isSuccessful = false; + $nameTool = $this->extractToolName(); + $this->errorOutputFake = "\n$nameTool fakes an error\n"; + return $this; + } + + public function setFailByFoundedErrorsInNormalOutput(): ProcessFake + { + $this->isSuccessful = false; + $nameTool = $this->extractToolName(); + $this->outputFake = "\n$nameTool fakes an error in normal output\n"; + $this->errorOutputFake = ''; return $this; } diff --git a/tests/Unit/Tools/Execution/MultiProcessesExecutionTest.php b/tests/Unit/Tools/Execution/MultiProcessesExecutionTest.php index d6aaaec..317caea 100644 --- a/tests/Unit/Tools/Execution/MultiProcessesExecutionTest.php +++ b/tests/Unit/Tools/Execution/MultiProcessesExecutionTest.php @@ -12,6 +12,9 @@ use Wtyd\GitHooks\Tools\ToolsFactoy; use Wtyd\GitHooks\Utils\Printer; +/** + * Applied pairwise testing strategy. See tests cases in the link https://pairwise.teremokgames.com/5fujg/ + */ class MultiProcessesExecutionTest extends UnitTestCase { use \Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; @@ -42,17 +45,20 @@ public function oneToolFailsEachTimeDataProvider() ]; } - /** @test */ + /** + * Test added to pairwise testing strategy + * @test + */ function it_returns_empty_errors_when_all_tools_find_NO_errors() { - $this->configurationFile = new ConfigurationFile($this->configurationFileBuilder->buildArray(), self::ALL_TOOLS); - $tools = $this->toolsFactory->__invoke($this->configurationFile->getToolsConfiguration()); + $configurationFile = new ConfigurationFile($this->configurationFileBuilder->buildArray(), self::ALL_TOOLS); + $tools = $this->toolsFactory->__invoke($configurationFile->getToolsConfiguration()); $printerMock = Mock::spy(Printer::class); $multiProcessExecution = new MultiProcessesExecutionFake($printerMock); - $errors = $multiProcessExecution->execute($tools, $this->configurationFile->getProcesses()); + $errors = $multiProcessExecution->execute($tools, $configurationFile->getProcesses()); $this->assertTrue($errors->isEmpty()); @@ -71,51 +77,103 @@ function it_returns_empty_errors_when_all_tools_find_NO_errors() */ function it_returns_errors_when_a_tool_finds_errors($failedTool, $successTools) { - $this->configurationFile = new ConfigurationFile($this->configurationFileBuilder->buildArray(), self::ALL_TOOLS); - $tools = $this->toolsFactory->__invoke($this->configurationFile->getToolsConfiguration()); + $configurationFile = new ConfigurationFile($this->configurationFileBuilder->buildArray(), self::ALL_TOOLS); + $tools = $this->toolsFactory->__invoke($configurationFile->getToolsConfiguration()); $printerMock = Mock::spy(Printer::class); $multiProcessExecution = new MultiProcessesExecutionFake($printerMock); - $multiProcessExecution->setToolsThatMustFail([$failedTool]); - + $multiProcessExecution->failedToolsByFoundedErrors([$failedTool]); - $errors = $multiProcessExecution->execute($tools, $this->configurationFile->getProcesses()); + $errors = $multiProcessExecution->execute($tools, $configurationFile->getProcesses()); $this->assertFalse($errors->isEmpty()); $printerMock->shouldHaveReceived()->resultError(\Mockery::pattern($this->messageRegExp($failedTool, false)))->once(); - $printerMock->shouldHaveReceived()->line(\Mockery::pattern("%The tool $failedTool mocks an error%"))->once(); + $printerMock->shouldHaveReceived()->line(\Mockery::pattern("%$failedTool fakes an error%"))->once(); $printerMock->shouldHaveReceived()->resultSuccess(\Mockery::pattern($this->messageRegExp($successTools[0])))->once(); - $printerMock->shouldNotHaveReceived()->line(\Mockery::pattern("%The tool $successTools[0] mocks an error%")); - $printerMock->shouldHaveReceived()->resultSuccess(\Mockery::pattern($this->messageRegExp($successTools[1])))->once(); - $printerMock->shouldNotHaveReceived()->line(\Mockery::pattern("%The tool $successTools[1] mocks an error%")); - $printerMock->shouldHaveReceived()->resultSuccess(\Mockery::pattern($this->messageRegExp($successTools[2])))->once(); - $printerMock->shouldNotHaveReceived()->line(\Mockery::pattern("%The tool $successTools[2] mocks an error%")); - $printerMock->shouldHaveReceived()->resultSuccess(\Mockery::pattern($this->messageRegExp($successTools[3])))->once(); - $printerMock->shouldNotHaveReceived()->line(\Mockery::pattern("%The tool $successTools[3] mocks an error%")); + $printerMock->shouldHaveReceived()->resultSuccess(\Mockery::pattern($this->messageRegExp($successTools[4])))->once(); + $printerMock->shouldHaveReceived()->resultSuccess(\Mockery::pattern($this->messageRegExp($successTools[5])))->once(); + } + + /** + * @test + * @dataProvider oneToolFailsEachTimeDataProvider + */ + function it_returns_errors_when_a_tool_raise_an_exception($failedTool, $successTools) + { + $configurationFile = new ConfigurationFile($this->configurationFileBuilder->buildArray(), self::ALL_TOOLS); + $tools = $this->toolsFactory->__invoke($configurationFile->getToolsConfiguration()); + + $printerMock = Mock::spy(Printer::class); + + $multiProcessExecution = new MultiProcessesExecutionFake($printerMock); + $multiProcessExecution->failedToolsByException([$failedTool]); + + $errors = $multiProcessExecution->execute($tools, $configurationFile->getProcesses()); + + $this->assertFalse($errors->isEmpty()); + + $printerMock->shouldHaveReceived()->resultError(\Mockery::pattern($this->messageRegExp($failedTool, false)))->once(); + $printerMock->shouldHaveReceived()->line(\Mockery::pattern("%$failedTool fakes an exception%"))->once(); + $printerMock->shouldHaveReceived()->resultSuccess(\Mockery::pattern($this->messageRegExp($successTools[0])))->once(); + $printerMock->shouldHaveReceived()->resultSuccess(\Mockery::pattern($this->messageRegExp($successTools[1])))->once(); + $printerMock->shouldHaveReceived()->resultSuccess(\Mockery::pattern($this->messageRegExp($successTools[2])))->once(); + $printerMock->shouldHaveReceived()->resultSuccess(\Mockery::pattern($this->messageRegExp($successTools[3])))->once(); $printerMock->shouldHaveReceived()->resultSuccess(\Mockery::pattern($this->messageRegExp($successTools[4])))->once(); - $printerMock->shouldNotHaveReceived()->line(\Mockery::pattern("%The tool $successTools[4] mocks an error%")); + $printerMock->shouldHaveReceived()->resultSuccess(\Mockery::pattern($this->messageRegExp($successTools[5])))->once(); + } + + /** + * @test + * @dataProvider oneToolFailsEachTimeDataProvider + * Edge case explained in the finishExecution method in MultiProcessesExecution.php + */ + function it_returns_errors_when_a_tool_is_not_succesfully_and_has_errors_in_normal_output_instead_of_errorOutput($failedTool, $successTools) + { + $configurationFile = new ConfigurationFile($this->configurationFileBuilder->buildArray(), self::ALL_TOOLS); + $tools = $this->toolsFactory->__invoke($configurationFile->getToolsConfiguration()); + + $printerMock = Mock::spy(Printer::class); + $multiProcessExecution = new MultiProcessesExecutionFake($printerMock); + $multiProcessExecution->setFailByFoundedErrorsInNormalOutput([$failedTool]); + + $errors = $multiProcessExecution->execute($tools, $configurationFile->getProcesses()); + + $this->assertFalse($errors->isEmpty()); + + $printerMock->shouldHaveReceived()->resultError(\Mockery::pattern($this->messageRegExp($failedTool, false)))->once(); + $printerMock->shouldHaveReceived()->line(\Mockery::pattern("%$failedTool fakes an error in normal output%"))->once(); + + $printerMock->shouldHaveReceived()->resultSuccess(\Mockery::pattern($this->messageRegExp($successTools[0])))->once(); + $printerMock->shouldHaveReceived()->resultSuccess(\Mockery::pattern($this->messageRegExp($successTools[1])))->once(); + $printerMock->shouldHaveReceived()->resultSuccess(\Mockery::pattern($this->messageRegExp($successTools[2])))->once(); + $printerMock->shouldHaveReceived()->resultSuccess(\Mockery::pattern($this->messageRegExp($successTools[3])))->once(); + $printerMock->shouldHaveReceived()->resultSuccess(\Mockery::pattern($this->messageRegExp($successTools[4])))->once(); $printerMock->shouldHaveReceived()->resultSuccess(\Mockery::pattern($this->messageRegExp($successTools[5])))->once(); - $printerMock->shouldNotHaveReceived()->line(\Mockery::pattern("%The tool $successTools[5] mocks an error%")); } public function twoToolsFailsDataProvider() { return [ - 'Fails phpcs' => ['phpcs', 'phpcbf'], - 'Fails phpcbf' => ['phpcbf', 'phpmd'], - 'Fails phpmd' => ['phpmd', 'phpcpd'], - 'Fails phpcpd' => ['phpcpd', 'parallel-lint'], - 'Fails parallel-lint' => ['parallel-lint', 'phpstan'], - 'Fails phpstan' => ['phpstan', 'security-checker'], - 'Fails security-checker' => ['security-checker', 'phpcs'], + 'Fails phpcs' => [ + 'Failed tool' => 'phpcs', + 'Failed tool with ignore erros on exit' => 'phpcbf', + 'Way to fail' => 'failedToolsByFoundedErrors', + 'Expected error message (depends on method to fake fail)' => 'fakes an error\n', + ], + 'Fails phpcbf' => ['phpcbf', 'phpmd', 'failedToolsByFoundedErrors', 'fakes an error\n'], + 'Fails phpmd' => ['phpmd', 'phpcpd', 'failedToolsByFoundedErrors', 'fakes an error\n'], + 'Fails phpcpd' => ['phpcpd', 'parallel-lint', 'failedToolsByFoundedErrors', 'fakes an error\n'], + 'Fails parallel-lint' => ['parallel-lint', 'phpstan', 'failedToolsByFoundedErrors', 'fakes an error\n'], + 'Fails phpstan' => ['phpstan', 'security-checker', 'failedToolsByFoundedErrors', 'fakes an error\n'], + 'Fails security-checker' => ['security-checker', 'phpcs', 'failedToolsByFoundedErrors', 'fakes an error\n'], ]; } @@ -125,34 +183,42 @@ public function twoToolsFailsDataProvider() */ function it_doesnt_set_errors_when_the_tool_finds_errors_but_ignoreErrorsOnExit_flag_is_setted_to_true( $failedTool, - $failedToolWithIgnoreErrosOnExit + $failedToolWithIgnoreErrosOnExit, + $methodToFakeFail, + $expectedErrorMessage ) { - $this->configurationFile = new ConfigurationFile($this->configurationFileBuilder - ->changeToolOption($failedToolWithIgnoreErrosOnExit, ['ignoreErrorsOnExit' => true]) - ->buildArray(), self::ALL_TOOLS); - $tools = $this->toolsFactory->__invoke($this->configurationFile->getToolsConfiguration()); + $configurationFile = new ConfigurationFile( + $this->configurationFileBuilder + ->changeToolOption($failedToolWithIgnoreErrosOnExit, ['ignoreErrorsOnExit' => true]) + ->buildArray(), + self::ALL_TOOLS + ); + $tools = $this->toolsFactory->__invoke($configurationFile->getToolsConfiguration()); $printerMock = Mock::spy(Printer::class); $multiProcessExecution = new MultiProcessesExecutionFake($printerMock); + $multiProcessExecution->$methodToFakeFail([$failedTool, $failedToolWithIgnoreErrosOnExit]); $multiProcessExecution->setToolsThatMustFail([$failedTool, $failedToolWithIgnoreErrosOnExit]); - $errors = $multiProcessExecution->execute($tools, $this->configurationFile->getProcesses()); + $errors = $multiProcessExecution->execute($tools, $configurationFile->getProcesses()); + $regExpOfExpectedErrorMessage = "%$failedTool $expectedErrorMessage%"; $this->assertCount(1, $errors->getErrors()); $this->assertArrayHasKey($failedTool, $errors->getErrors()); - $this->assertMatchesRegularExpression("%The tool $failedTool mocks an error%", $errors->getErrors()[$failedTool]); + $this->assertMatchesRegularExpression($regExpOfExpectedErrorMessage, $errors->getErrors()[$failedTool]); $printerMock->shouldHaveReceived()->resultError(\Mockery::pattern($this->messageRegExp($failedTool, false)))->once(); - $printerMock->shouldHaveReceived()->line(\Mockery::pattern("%The tool $failedTool mocks an error%"))->once(); + $printerMock->shouldHaveReceived()->line(\Mockery::pattern($regExpOfExpectedErrorMessage))->once(); $printerMock->shouldHaveReceived()->resultError(\Mockery::pattern( $this->messageRegExp($failedToolWithIgnoreErrosOnExit, false) ))->once(); + $regExpOfExpectedErrorMessage = "%$failedToolWithIgnoreErrosOnExit $expectedErrorMessage%"; $printerMock->shouldHaveReceived()->line( - \Mockery::pattern("%The tool $failedToolWithIgnoreErrosOnExit mocks an error%") + \Mockery::pattern($regExpOfExpectedErrorMessage) )->once(); } }