diff --git a/Makefile b/Makefile index d371ad6b..1cc44080 100644 --- a/Makefile +++ b/Makefile @@ -17,4 +17,4 @@ static-analysis: vendor .PHONY: test test: vendor - php -d zend.assertions=1 vendor/bin/phpunit + php -d zend.assertions=1 vendor/bin/phpunit ${arg} diff --git a/bin/phpunit-wrapper.php b/bin/phpunit-wrapper.php index 799aa718..1bac7f63 100644 --- a/bin/phpunit-wrapper.php +++ b/bin/phpunit-wrapper.php @@ -46,20 +46,19 @@ exit($lastExitCode); } - $command = rtrim($command); - if ($command === 'EXIT') { + if ($command === \ParaTest\Runners\PHPUnit\Worker\WrapperWorker::COMMAND_EXIT) { echo "EXITED\n"; exit($lastExitCode); } $arguments = unserialize($command); $command = implode(' ', $arguments); - echo "Executing: $command\n"; + echo "Executing: {$command}\n"; $info = []; - $info[] = 'Time: ' . (new DateTime())->format(DateTime::RFC3339); - $info[] = "Iteration: $i"; - $info[] = "Command: $command"; + $info[] = 'Time: ' . (new DateTimeImmutable())->format(DateTime::RFC3339); + $info[] = "Iteration: {$i}"; + $info[] = "Command: {$command}"; $info[] = PHP_EOL; $infoText = implode(PHP_EOL, $info) . PHP_EOL; $logInfo($infoText); @@ -73,5 +72,5 @@ $logInfo($infoText); - echo "FINISHED\n"; + echo \ParaTest\Runners\PHPUnit\Worker\WrapperWorker::COMMAND_FINISHED; } diff --git a/composer.json b/composer.json index 8c8a222a..055880e4 100644 --- a/composer.json +++ b/composer.json @@ -25,11 +25,11 @@ "ext-pdo": "*", "ext-reflection": "*", "ext-simplexml": "*", - "brianium/habitat": "^1.0", "phpunit/php-code-coverage": "^9.1.2", "phpunit/php-file-iterator": "^3.0", "phpunit/php-timer": "^5.0", "phpunit/phpunit": "^9.3.5", + "sebastian/environment": "^5.1", "symfony/console": "^4.4 || ^5.1", "symfony/process": "^4.4 || ^5.1" }, @@ -42,6 +42,7 @@ "phpstan/phpstan-phpunit": "^0.12.16", "phpstan/phpstan-strict-rules": "^0.12.4", "squizlabs/php_codesniffer": "^3.5.6", + "symfony/filesystem": "^5.1.3", "thecodingmachine/phpstan-strict-rules": "^0.12.0", "vimeo/psalm": "^3.12.2" }, diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 55e5640b..96b428c2 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -63,15 +63,14 @@ parameters: message: "#^Should not use function \"proc_open\", please change the code\\.$#" count: 1 path: src/Runners/PHPUnit/Worker/BaseWorker.php - - - message: "#^Should not use function \"proc_open\", please change the code\\.$#" - count: 1 - path: test/Functional/Runners/PHPUnit/WorkerTest.php # Known errors - "#^Variable property access on .+$#" - - "#^Variable method call on .+$#" - "#^.+ has no value type specified in iterable type Symfony\\\\Component\\\\Process\\\\Process\\.$#" + - + message: "#^Method ParaTest\\\\Tests\\\\TestBase\\:\\:setUpTest\\(\\) is not final, but since the containing class is abstract, it should be\\.$#" + count: 1 + path: test/TestBase.php - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" count: 2 @@ -120,6 +119,18 @@ parameters: message: "#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertTrue\\(\\) with false will always evaluate to false\\.$#" count: 1 path: test/fixtures/wrapper-runner-exit-code-tests/FailureTest.php + - + message: "#^Offset 'TEST_TOKEN' does not exist on array\\('PARATEST' \\=\\> 1, \\?'TEST_TOKEN' \\=\\> int, \\?'UNIQUE_TEST_TOKEN' \\=\\> string\\)\\.$#" + count: 1 + path: test/Unit/Runners/PHPUnit/OptionsTest.php + - + message: "#^Call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertIsString\\(\\) with string will always evaluate to true\\.$#" + count: 1 + path: test/Unit/Runners/PHPUnit/OptionsTest.php + - + message: "#^Offset 'UNIQUE_TEST_TOKEN' does not exist on array\\('PARATEST' \\=\\> 1, \\?'UNIQUE_TEST_TOKEN' \\=\\> string, 'TEST_TOKEN' \\=\\> int\\)\\.$#" + count: 1 + path: test/Unit/Runners/PHPUnit/OptionsTest.php # Level 7 - diff --git a/src/Coverage/CoverageMerger.php b/src/Coverage/CoverageMerger.php index b21ff411..e570362d 100644 --- a/src/Coverage/CoverageMerger.php +++ b/src/Coverage/CoverageMerger.php @@ -4,33 +4,26 @@ namespace ParaTest\Coverage; -use RuntimeException; use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\CodeCoverage\ProcessedCodeCoverageData; +use SebastianBergmann\Environment\Runtime; use function array_map; use function array_slice; -use function assert; -use function extension_loaded; use function filesize; -use function function_exists; -use function ini_get; -use function is_array; use function is_file; use function unlink; -use const PHP_SAPI; - final class CoverageMerger { /** @var CodeCoverage|null */ private $coverage; /** @var int */ - private $test_limit; + private $testLimit; - public function __construct(int $test_limit = 0) + public function __construct(int $testLimit) { - $this->test_limit = $test_limit; + $this->testLimit = $testLimit; } private function addCoverage(CodeCoverage $coverage): void @@ -48,29 +41,18 @@ private function addCoverage(CodeCoverage $coverage): void * Adds the coverage contained in $coverageFile and deletes the file afterwards. * * @param string $coverageFile Code coverage file - * - * @throws RuntimeException When coverage file is empty. */ - public function addCoverageFromFile(?string $coverageFile): void + public function addCoverageFromFile(string $coverageFile): void { - if ($coverageFile === null || ! is_file($coverageFile)) { - return; - } - - if (filesize($coverageFile) === 0) { + if (! is_file($coverageFile) || filesize($coverageFile) === 0) { $extra = 'This means a PHPUnit process has crashed.'; - - $xdebug = function_exists('xdebug_get_code_coverage'); - $phpdbg = PHP_SAPI === 'phpdbg'; - $pcov = extension_loaded('pcov') && (bool) ini_get('pcov.enabled'); - - if (! $xdebug && ! $phpdbg && ! $pcov) { + if (! (new Runtime())->canCollectCodeCoverage()) { + // @codeCoverageIgnoreStart $extra = 'No coverage driver found! Enable one of Xdebug, PHPDBG or PCOV for coverage.'; + // @codeCoverageIgnoreEnd } - throw new RuntimeException( - "Coverage file $coverageFile is empty. " . $extra - ); + throw new EmptyCoverageFileException("Coverage file {$coverageFile} is empty. " . $extra); } /** @psalm-suppress UnresolvableInclude **/ @@ -79,19 +61,6 @@ public function addCoverageFromFile(?string $coverageFile): void unlink($coverageFile); } - /** - * Get coverage report generator. - */ - public function getReporter(): CoverageReporterInterface - { - assert($this->coverage !== null); - - return new CoverageReporter($this->coverage); - } - - /** - * Get CodeCoverage object. - */ public function getCodeCoverageObject(): ?CodeCoverage { return $this->coverage; @@ -99,20 +68,15 @@ public function getCodeCoverageObject(): ?CodeCoverage private function limitCoverageTests(CodeCoverage $coverage): void { - if ($this->test_limit === 0) { + if ($this->testLimit === 0) { return; } - $testLimit = $this->test_limit; + $testLimit = $this->testLimit; $data = $coverage->getData(true); $newData = array_map( static function (array $lines) use ($testLimit): array { - /** @psalm-suppress MissingClosureReturnType **/ - return array_map(static function ($value) use ($testLimit) { - if (! is_array($value)) { - return $value; - } - + return array_map(static function (array $value) use ($testLimit): array { return array_slice($value, 0, $testLimit); }, $lines); }, diff --git a/src/Coverage/CoverageReporter.php b/src/Coverage/CoverageReporter.php index 13ed5f2f..faac0a15 100644 --- a/src/Coverage/CoverageReporter.php +++ b/src/Coverage/CoverageReporter.php @@ -13,7 +13,7 @@ use SebastianBergmann\CodeCoverage\Report\Xml\Facade as XmlReport; use SebastianBergmann\CodeCoverage\Version; -final class CoverageReporter implements CoverageReporterInterface +final class CoverageReporter { /** @var CodeCoverage */ private $coverage; diff --git a/src/Coverage/CoverageReporterInterface.php b/src/Coverage/CoverageReporterInterface.php deleted file mode 100644 index 63dc8ef9..00000000 --- a/src/Coverage/CoverageReporterInterface.php +++ /dev/null @@ -1,48 +0,0 @@ - '', - 'file' => '', - 'tests' => 0, - 'assertions' => 0, - 'failures' => 0, - 'errors' => 0, - 'skipped' => 0, - 'time' => 0.0, - ]; - public function __construct(string $logFile) { if (! file_exists($logFile)) { - throw new InvalidArgumentException("Log file $logFile does not exist"); + throw new InvalidArgumentException("Log file {$logFile} does not exist"); } $this->logFile = $logFile; if (filesize($logFile) === 0) { throw new InvalidArgumentException( - "Log file $logFile is empty. This means a PHPUnit process has crashed." + "Log file {$logFile} is empty. This means a PHPUnit process has crashed." ); } @@ -144,10 +131,10 @@ private function init(): void */ private function initSuiteFromCases(array $nodeArray): void { - $testCases = []; - $properties = $this->caseNodesToSuiteProperties($nodeArray, $testCases); + $testCases = []; + $testSuite = $this->caseNodesToSuite($nodeArray, $testCases); if (! $this->isSingle) { - $this->addSuite($properties, $testCases); + $this->addSuite($testSuite, $testCases); } else { $suite = $this->suites[0]; $suite->cases = array_merge($suite->cases, $testCases); @@ -158,12 +145,10 @@ private function initSuiteFromCases(array $nodeArray): void * Creates and adds a TestSuite based on the given * suite properties and collection of test cases. * - * @param array{name: string, file: string, assertions: int, tests: int, failures: int, errors: int, skipped: int, time: float} $properties - * @param TestCase[] $testCases + * @param TestCase[] $testCases */ - private function addSuite(array $properties, array $testCases): void + private function addSuite(TestSuite $suite, array $testCases): void { - $suite = TestSuite::suiteFromArray($properties); $suite->cases = $testCases; $this->suites[0]->suites[] = $suite; } @@ -173,31 +158,26 @@ private function addSuite(array $properties, array $testCases): void * * @param SimpleXMLElement[] $nodeArray an array of testcase nodes * @param TestCase[] $testCases an array reference. Individual testcases will be placed here. - * - * @return array{name: string, file: string, assertions: int, tests: int, failures: int, errors: int, skipped: int, time: float} */ - private function caseNodesToSuiteProperties(array $nodeArray, array &$testCases = []): array + private function caseNodesToSuite(array $nodeArray, array &$testCases = []): TestSuite { - /** @var array{name: string, file: string, assertions: int, tests: int, failures: int, errors: int, skipped: int, time: float} $result */ - $result = array_reduce( - $nodeArray, - static function (array $result, SimpleXMLElement $xmlElement) use (&$testCases): array { - $testCases[] = TestCase::caseFromNode($xmlElement); - $result['name'] = (string) $xmlElement['class']; - $result['file'] = (string) $xmlElement['file']; - ++$result['tests']; - $result['assertions'] += (int) $xmlElement['assertions']; - $result['failures'] += ($failues = $xmlElement->xpath('failure')) !== false ? count($failues) : 0; - $result['errors'] += ($error = $xmlElement->xpath('error')) !== false ? count($error) : 0; - $result['skipped'] += ($skipped = $xmlElement->xpath('skipped')) !== false ? count($skipped) : 0; - $result['time'] += (float) $xmlElement['time']; - - return $result; - }, - static::$defaultSuite - ); - - return $result; + $testSuite = TestSuite::empty(); + foreach ($nodeArray as $simpleXMLElement) { + $testCase = TestCase::caseFromNode($simpleXMLElement); + $testCases[] = $testCase; + + $testSuite->name = $testCase->class; + $testSuite->file = $testCase->file; + ++$testSuite->tests; + $testSuite->assertions += $testCase->assertions; + $testSuite->failures += count($testCase->failures); + $testSuite->errors += count($testCase->errors); + $testSuite->warnings += count($testCase->warnings); + $testSuite->skipped += count($testCase->skipped); + $testSuite->time += $testCase->time; + } + + return $testSuite; } /** @@ -239,44 +219,101 @@ private function initSuite(): void $node = current($node); if ($node !== false) { - $this->suites[] = TestSuite::suiteFromNode($node); + $this->suites[] = new TestSuite( + (string) $node['name'], + (int) $node['tests'], + (int) $node['assertions'], + (int) $node['failures'], + (int) $node['errors'], + (int) $node['warnings'], + (int) $node['skipped'], + (float) $node['time'], + (string) $node['file'] + ); } else { - $this->suites[] = TestSuite::suiteFromArray(self::$defaultSuite); + $this->suites[] = TestSuite::empty(); } } + public function getTotalTests(): int + { + return $this->suites[0]->tests; + } + + public function getTotalAssertions(): int + { + return $this->suites[0]->assertions; + } + + public function getTotalFailures(): int + { + return $this->suites[0]->failures; + } + + public function getTotalErrors(): int + { + return $this->suites[0]->errors; + } + + public function getTotalWarnings(): int + { + return $this->suites[0]->warnings; + } + + public function getTotalTime(): float + { + return $this->suites[0]->time; + } + /** - * Return a value as a float or integer. - * - * @return float|int + * {@inheritDoc} */ - protected function getNumericValue(string $property) + public function getErrors(): array { - return $property === 'time' - ? (float) $this->suites[0]->$property - : (int) $this->suites[0]->$property; + $messages = []; + $suites = $this->isSingle ? $this->suites : $this->suites[0]->suites; + foreach ($suites as $suite) { + foreach ($suite->cases as $case) { + foreach ($case->errors as $msg) { + $messages[] = $msg['text']; + } + } + } + + return $messages; } /** - * Return messages for a given type. - * - * @return string[] + * {@inheritDoc} */ - protected function getMessages(string $type): array + public function getWarnings(): array { $messages = []; $suites = $this->isSingle ? $this->suites : $this->suites[0]->suites; foreach ($suites as $suite) { - $messages = array_merge( - $messages, - array_reduce($suite->cases, static function (array $result, TestCase $case) use ($type): array { - return array_merge($result, array_reduce($case->$type, static function (array $msgs, array $msg): array { - $msgs[] = $msg['text']; - - return $msgs; - }, [])); - }, []) - ); + foreach ($suite->cases as $case) { + foreach ($case->warnings as $msg) { + $messages[] = $msg['text']; + } + } + } + + return $messages; + } + + /** + * {@inheritDoc} + */ + public function getFailures(): array + { + $messages = []; + $suites = $this->isSingle ? $this->suites : $this->suites[0]->suites; + foreach ($suites as $suite) { + foreach ($suite->cases as $case) { + foreach ($case->failures as $msg) { + $messages[] = $msg['text']; + } + } } return $messages; diff --git a/src/Logging/JUnit/TestCase.php b/src/Logging/JUnit/TestCase.php index 3420bc82..291c77d6 100644 --- a/src/Logging/JUnit/TestCase.php +++ b/src/Logging/JUnit/TestCase.php @@ -62,19 +62,6 @@ public function __construct( $this->time = $time; } - /** - * Add a defect type (error or failure). - * - * @param string $collName the name of the collection to add to - */ - private function addDefect(string $collName, string $type, string $text): void - { - $this->{$collName}[] = [ - 'type' => $type, - 'text' => trim($text), - ]; - } - /** * Factory method that creates a TestCase object * from a SimpleXMLElement. @@ -106,7 +93,11 @@ public static function caseFromNode(SimpleXMLElement $node): self $message = (string) $defect; $message .= (string) $system_output; - $case->addDefect($group, (string) $defect['type'], $message); + + $case->{$group}[] = [ + 'type' => (string) $defect['type'], + 'text' => trim($message), + ]; } } diff --git a/src/Logging/JUnit/TestSuite.php b/src/Logging/JUnit/TestSuite.php index 15279814..b7fb0bd9 100644 --- a/src/Logging/JUnit/TestSuite.php +++ b/src/Logging/JUnit/TestSuite.php @@ -4,8 +4,6 @@ namespace ParaTest\Logging\JUnit; -use SimpleXMLElement; - /** * A simple data structure for tracking * data associated with a testsuite node @@ -28,6 +26,9 @@ final class TestSuite /** @var int */ public $errors; + /** @var int */ + public $warnings; + /** @var int */ public $skipped; @@ -57,6 +58,7 @@ public function __construct( int $assertions, int $failures, int $errors, + int $warnings, int $skipped, float $time, string $file @@ -67,48 +69,23 @@ public function __construct( $this->failures = $failures; $this->skipped = $skipped; $this->errors = $errors; + $this->warnings = $warnings; $this->time = $time; $this->file = $file; } - /** - * Create a TestSuite from an associative - * array. - * - * @param array{name: string, file: string, assertions: int, tests: int, failures: int, errors: int, skipped: int, time: float} $arr - * - * @return TestSuite - */ - public static function suiteFromArray(array $arr): self - { - return new self( - $arr['name'], - $arr['tests'], - $arr['assertions'], - $arr['failures'], - $arr['errors'], - $arr['skipped'], - $arr['time'], - $arr['file'] - ); - } - - /** - * Create a TestSuite from a SimpleXMLElement. - * - * @return TestSuite - */ - public static function suiteFromNode(SimpleXMLElement $node): self + public static function empty(): self { return new self( - (string) $node['name'], - (int) $node['tests'], - (int) $node['assertions'], - (int) $node['failures'], - (int) $node['errors'], - (int) $node['skipped'], - (float) $node['time'], - (string) $node['file'] + '', + 0, + 0, + 0, + 0, + 0, + 0, + 0.0, + '', ); } } diff --git a/src/Logging/JUnit/Writer.php b/src/Logging/JUnit/Writer.php index 5d672142..2494eeaa 100644 --- a/src/Logging/JUnit/Writer.php +++ b/src/Logging/JUnit/Writer.php @@ -8,14 +8,14 @@ use DOMElement; use ParaTest\Logging\LogInterpreter; -use function array_merge; -use function array_reduce; use function assert; use function count; use function file_put_contents; use function get_object_vars; use function htmlspecialchars; use function preg_match; +use function sprintf; +use function str_replace; use const ENT_XML1; @@ -40,7 +40,7 @@ final class Writer * * @var string */ - private static $suiteAttrs = '/name|(?:test|assertion|failure|error)s|time|file/'; + private static $suiteAttrs = '/name|(?:test|assertion|failure|error|warning)s|skipped|time|file/'; /** * A pattern for matching testcase attrs. @@ -49,21 +49,6 @@ final class Writer */ private static $caseAttrs = '/name|class|file|line|assertions|time/'; - /** - * A default suite to ease flattening of - * suite structures. - * - * @var array - */ - private static $defaultSuite = [ - 'tests' => 0, - 'assertions' => 0, - 'failures' => 0, - 'skipped' => 0, - 'errors' => 0, - 'time' => 0, - ]; - public function __construct(LogInterpreter $interpreter, string $name) { $this->name = $name; @@ -124,6 +109,10 @@ private function appendSuite(DOMElement $root, TestSuite $suite): DOMElement continue; } + if ($name === 'time') { + $value = sprintf('%F', $value); + } + $suiteNode->setAttribute($name, (string) $value); } @@ -141,9 +130,9 @@ private function appendCase(DOMElement $suiteNode, TestCase $case): DOMElement $caseNode = $this->document->createElement('testcase'); $vars = get_object_vars($case); foreach ($vars as $name => $value) { - $match = preg_match(static::$caseAttrs, $name); - assert($match !== false); - if ($match === 0) { + $matchCount = preg_match(static::$caseAttrs, $name); + assert($matchCount !== false); + if ($matchCount === 0) { continue; } @@ -151,12 +140,24 @@ private function appendCase(DOMElement $suiteNode, TestCase $case): DOMElement continue; } + if ($name === 'time') { + $value = sprintf('%F', $value); + } + $caseNode->setAttribute($name, (string) $value); + + if ($name !== 'class') { + continue; + } + + $caseNode->setAttribute('classname', str_replace('\\', '.', (string) $value)); } $suiteNode->appendChild($caseNode); $this->appendDefects($caseNode, $case->failures, 'failure'); $this->appendDefects($caseNode, $case->errors, 'error'); + $this->appendDefects($caseNode, $case->warnings, 'warning'); + $this->appendDefects($caseNode, $case->skipped, 'skipped'); return $caseNode; } @@ -169,8 +170,13 @@ private function appendCase(DOMElement $suiteNode, TestCase $case): DOMElement private function appendDefects(DOMElement $caseNode, array $defects, string $type): void { foreach ($defects as $defect) { - $defectNode = $this->document->createElement($type, htmlspecialchars($defect['text'], ENT_XML1) . "\n"); - $defectNode->setAttribute('type', $defect['type']); + if ($type === 'skipped') { + $defectNode = $this->document->createElement($type); + } else { + $defectNode = $this->document->createElement($type, htmlspecialchars($defect['text'], ENT_XML1) . "\n"); + $defectNode->setAttribute('type', $defect['type']); + } + $caseNode->appendChild($defectNode); } } @@ -203,22 +209,34 @@ private function getSuiteRoot(array $suites): DOMElement * Get the attributes used on the root testsuite * node. * - * @param array $suites + * @param TestSuite[] $suites * - * @return mixed + * @return array */ - private function getSuiteRootAttributes(array $suites) + private function getSuiteRootAttributes(array $suites): array { - return array_reduce($suites, static function (array $result, TestSuite $suite): array { + $result = [ + 'name' => $this->name, + 'tests' => 0, + 'assertions' => 0, + 'errors' => 0, + 'warnings' => 0, + 'failures' => 0, + 'skipped' => 0, + 'time' => 0, + ]; + + foreach ($suites as $suite) { $result['tests'] += $suite->tests; $result['assertions'] += $suite->assertions; + $result['errors'] += $suite->errors; + $result['warnings'] += $suite->warnings; $result['failures'] += $suite->failures; $result['skipped'] += $suite->skipped; - $result['errors'] += $suite->errors; $result['time'] += $suite->time; + } - return $result; - }, array_merge(['name' => $this->name], self::$defaultSuite)); + return $result; } /** diff --git a/src/Logging/LogInterpreter.php b/src/Logging/LogInterpreter.php index aca8a561..a2d2585f 100644 --- a/src/Logging/LogInterpreter.php +++ b/src/Logging/LogInterpreter.php @@ -12,10 +12,8 @@ use function array_reduce; use function array_values; use function count; -use function reset; -use function ucfirst; -final class LogInterpreter extends MetaProvider +final class LogInterpreter implements MetaProvider { /** * A collection of Reader objects @@ -23,28 +21,15 @@ final class LogInterpreter extends MetaProvider * * @var Reader[] */ - protected $readers = []; - - /** - * Reset the array pointer of the internal - * readers collection. - */ - public function rewind(): void - { - reset($this->readers); - } + private $readers = []; /** * Add a new Reader to be included * in the final results. - * - * @return $this */ - public function addReader(Reader $reader): self + public function addReader(Reader $reader): void { $this->readers[] = $reader; - - return $this; } /** @@ -65,10 +50,7 @@ public function getReaders(): array */ public function isSuccessful(): bool { - $failures = $this->getNumericValue('failures'); - $errors = $this->getNumericValue('errors'); - - return $failures === 0 && $errors === 0; + return $this->getTotalFailures() === 0 && $this->getTotalErrors() === 0; } /** @@ -119,80 +101,109 @@ private function extendEmptyCasesFromSuites(array $cases, TestSuite $suite): voi /** * Flattens all cases into their respective suites. * - * @return TestSuite[] $suites a collection of suites and their cases + * @return TestSuite[] A collection of suites and their cases */ public function flattenCases(): array { $dict = []; foreach ($this->getCases() as $case) { if (! isset($dict[$case->file])) { - $dict[$case->file] = new TestSuite($case->class, 0, 0, 0, 0, 0, 0, $case->file); + $dict[$case->file] = TestSuite::empty(); } + $dict[$case->file]->name = $case->class; + $dict[$case->file]->file = $case->file; $dict[$case->file]->cases[] = $case; ++$dict[$case->file]->tests; $dict[$case->file]->assertions += $case->assertions; $dict[$case->file]->failures += count($case->failures); $dict[$case->file]->errors += count($case->errors); + $dict[$case->file]->warnings += count($case->warnings); $dict[$case->file]->skipped += count($case->skipped); $dict[$case->file]->time += $case->time; - $dict[$case->file]->file = $case->file; } return array_values($dict); } - /** - * Returns a value as either a float or int. - * - * @return float|int - */ - protected function getNumericValue(string $property) + public function getTotalTests(): int + { + return array_reduce($this->readers, static function (int $result, Reader $reader): int { + return $result + $reader->getTotalTests(); + }, 0); + } + + public function getTotalAssertions(): int + { + return array_reduce($this->readers, static function (int $result, Reader $reader): int { + return $result + $reader->getTotalAssertions(); + }, 0); + } + + public function getTotalFailures(): int + { + return array_reduce($this->readers, static function (int $result, Reader $reader): int { + return $result + $reader->getTotalFailures(); + }, 0); + } + + public function getTotalErrors(): int + { + return array_reduce($this->readers, static function (int $result, Reader $reader): int { + return $result + $reader->getTotalErrors(); + }, 0); + } + + public function getTotalWarnings(): int { - return $property === 'time' - ? (float) $this->accumulate('getTotalTime') - : (int) $this->accumulate('getTotal' . ucfirst($property)); + return array_reduce($this->readers, static function (int $result, Reader $reader): int { + return $result + $reader->getTotalWarnings(); + }, 0); + } + + public function getTotalTime(): float + { + return array_reduce($this->readers, static function (float $result, Reader $reader): float { + return $result + $reader->getTotalTime(); + }, 0.0); } /** - * Gets messages of a given type and - * merges them into a single collection. - * - * @return string[] + * {@inheritDoc} */ - protected function getMessages(string $type): array + public function getErrors(): array { - return $this->mergeMessages('get' . ucfirst($type)); + $messages = []; + foreach ($this->readers as $reader) { + $messages = array_merge($messages, $reader->getErrors()); + } + + return $messages; } /** - * Flatten messages into a single collection - * based on an accessor method. - * - * @return string[] + * {@inheritDoc} */ - private function mergeMessages(string $method): array + public function getWarnings(): array { $messages = []; foreach ($this->readers as $reader) { - $messages = array_merge($messages, $reader->{$method}()); + $messages = array_merge($messages, $reader->getWarnings()); } return $messages; } /** - * Reduces a collection of readers down to a single - * result based on an accessor. - * - * @return mixed + * {@inheritDoc} */ - private function accumulate(string $method) + public function getFailures(): array { - return array_reduce($this->readers, static function (int $result, Reader $reader) use ($method): int { - $result += $reader->$method(); + $messages = []; + foreach ($this->readers as $reader) { + $messages = array_merge($messages, $reader->getFailures()); + } - return $result; - }, 0); + return $messages; } } diff --git a/src/Logging/MetaProvider.php b/src/Logging/MetaProvider.php index 9d5e21ef..5c10985d 100644 --- a/src/Logging/MetaProvider.php +++ b/src/Logging/MetaProvider.php @@ -4,75 +4,26 @@ namespace ParaTest\Logging; -use RuntimeException; +interface MetaProvider +{ + public function getTotalTests(): int; -use function preg_match; -use function strtolower; + public function getTotalAssertions(): int; -/** - * Adds __call behavior to a logging object - * for aggregating totals and messages - * - * @method int getTotalTests() - * @method int getTotalAssertions() - * @method int getTotalFailures() - * @method int getTotalErrors() - * @method int getTotalWarning() - * @method int getTotalTime() - * @method string[] getFailures() - * @method string[] getErrors() - * @method string[] getWarnings() - */ -abstract class MetaProvider -{ - /** - * This pattern is used to see whether a missing - * method is a "total" method or not. - * - * @var string - */ - protected static $totalMethod = '/^getTotal([\w]+)$/'; + public function getTotalFailures(): int; - /** - * This pattern is used to add message retrieval for a given - * type - i.e getFailures() or getErrors(). - * - * @var string - */ - protected static $messageMethod = '/^get((Failure|Error|Warning)s)$/'; + public function getTotalErrors(): int; - /** - * Simplify aggregation of totals or messages. - * - * @param mixed[] $args - * - * @return float|int|string[] - */ - final public function __call(string $method, array $args) - { - if (preg_match(self::$totalMethod, $method, $matches) > 0 && ($property = strtolower($matches[1])) !== '') { - return $this->getNumericValue($property); - } + public function getTotalWarnings(): int; - if (preg_match(self::$messageMethod, $method, $matches) > 0 && ($type = strtolower($matches[1])) !== '') { - return $this->getMessages($type); - } + public function getTotalTime(): float; - throw new RuntimeException("Method $method uknown"); - } + /** @return string[] */ + public function getErrors(): array; - /** - * Returns a value as either a float or int. - * - * @return float|int - */ - abstract protected function getNumericValue(string $property); + /** @return string[] */ + public function getWarnings(): array; - /** - * Gets messages of a given type and - * merges them into a single collection. - * - * @return string[] - */ - abstract protected function getMessages(string $type): array; + /** @return string[] */ + public function getFailures(): array; } diff --git a/src/Parser/ParsedClass.php b/src/Parser/ParsedClass.php index a8b0f284..cf6dfe61 100644 --- a/src/Parser/ParsedClass.php +++ b/src/Parser/ParsedClass.php @@ -4,10 +4,6 @@ namespace ParaTest\Parser; -use function array_filter; -use function count; -use function explode; - /** * @method class-string getName() */ @@ -30,7 +26,7 @@ final class ParsedClass extends ParsedObject /** * @param ParsedFunction[] $methods */ - public function __construct(string $doc, string $name, string $namespace, array $methods = []) + public function __construct(string $doc, string $name, string $namespace, array $methods) { parent::__construct($doc, $name); $this->namespace = $namespace; @@ -42,25 +38,11 @@ public function __construct(string $doc, string $name, string $namespace, array * optionally filtering on annotations present * on a method. * - * @param array $annotations - * * @return ParsedFunction[] */ - public function getMethods(array $annotations = []): array + public function getMethods(): array { - $methods = array_filter($this->methods, static function (ParsedFunction $method) use ($annotations): bool { - foreach ($annotations as $a => $v) { - foreach (explode(',', $v) as $subValue) { - if ($method->hasAnnotation($a, $subValue)) { - return true; - } - } - } - - return false; - }); - - return count($methods) > 0 ? $methods : $this->methods; + return $this->methods; } /** diff --git a/src/Parser/ParsedObject.php b/src/Parser/ParsedObject.php index 728c65fa..740aa33d 100644 --- a/src/Parser/ParsedObject.php +++ b/src/Parser/ParsedObject.php @@ -4,16 +4,13 @@ namespace ParaTest\Parser; -use function preg_match; -use function sprintf; - abstract class ParsedObject { /** @var string */ - protected $docBlock; + protected $name; /** @var string */ - protected $name; + private $docBlock; public function __construct(string $doc, string $name) { @@ -36,20 +33,4 @@ final public function getDocBlock(): string { return $this->docBlock; } - - /** - * Returns whether or not the parsed object - * has an annotation matching the name and value - * if provided. - */ - final public function hasAnnotation(string $annotation, ?string $value = null): bool - { - $pattern = sprintf( - '/@%s%s/', - $annotation, - $value !== null ? "[\s]+$value" : '\b' - ); - - return preg_match($pattern, $this->docBlock) === 1; - } } diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php index 2e433860..365c7511 100644 --- a/src/Parser/Parser.php +++ b/src/Parser/Parser.php @@ -7,7 +7,6 @@ use InvalidArgumentException; use PHPUnit\Framework\TestCase; use ReflectionClass; -use ReflectionException; use ReflectionMethod; use function array_diff; @@ -67,15 +66,7 @@ public function __construct(string $srcPath) throw new NoClassInFileException(); } - try { - $this->refl = new ReflectionClass($class); - } catch (ReflectionException $reflectionException) { - throw new InvalidArgumentException( - 'Unable to instantiate ReflectionClass. ' . $class . ' not found in: ' . $srcPath, - 0, - $reflectionException - ); - } + $this->refl = new ReflectionClass($class); } /** @@ -140,16 +131,11 @@ private function getClassName(string $filename, array $previousDeclaredClasses): $newClasses = array_values(array_diff($classes, $previousDeclaredClasses)); $className = $this->searchForUnitTestClass($newClasses, $filename); - if (isset($className)) { - return $className; - } - - $className = $this->searchForUnitTestClass($classes, $filename); - if (isset($className)) { + if ($className !== null) { return $className; } - return null; + return $this->searchForUnitTestClass($classes, $filename); } /** diff --git a/src/Runners/PHPUnit/BaseRunner.php b/src/Runners/PHPUnit/BaseRunner.php index 1f869cce..754ce721 100644 --- a/src/Runners/PHPUnit/BaseRunner.php +++ b/src/Runners/PHPUnit/BaseRunner.php @@ -5,6 +5,7 @@ namespace ParaTest\Runners\PHPUnit; use ParaTest\Coverage\CoverageMerger; +use ParaTest\Coverage\CoverageReporter; use ParaTest\Logging\JUnit\Writer; use ParaTest\Logging\LogInterpreter; use Symfony\Component\Console\Output\OutputInterface; @@ -39,24 +40,42 @@ abstract class BaseRunner implements RunnerInterface */ protected $exitcode = -1; + /** @var OutputInterface */ + protected $output; + /** * CoverageMerger to hold track of the accumulated coverage. * * @var CoverageMerger|null */ - protected $coverage = null; - - /** @var OutputInterface */ - protected $output; + private $coverage = null; public function __construct(Options $options, OutputInterface $output) { $this->options = $options; + $this->output = $output; $this->interpreter = new LogInterpreter(); $this->printer = new ResultPrinter($this->interpreter, $output, $options); - $this->output = $output; + + if (! $this->options->hasCoverage()) { + return; + } + + $this->coverage = new CoverageMerger($this->options->coverageTestLimit()); + } + + final public function run(): void + { + $this->load(new SuiteLoader($this->options)); + $this->printer->start(); + + $this->doRun(); + + $this->complete(); } + abstract protected function doRun(): void; + /** * Builds the collection of pending ExecutableTest objects * to run. If functional mode is enabled $this->pending will @@ -66,7 +85,7 @@ public function __construct(Options $options, OutputInterface $output) private function load(SuiteLoader $loader): void { $this->beforeLoadChecks(); - $loader->load($this->options->path()); + $loader->load(); $executables = $this->options->functional() ? $loader->getTestMethods() : $loader->getSuites(); $this->pending = array_merge($this->pending, $executables); foreach ($this->pending as $pending) { @@ -76,6 +95,23 @@ private function load(SuiteLoader $loader): void abstract protected function beforeLoadChecks(): void; + /** + * Finalizes the run process. This method + * prints all results, rewinds the log interpreter, + * logs any results to JUnit, and cleans up temporary + * files. + */ + private function complete(): void + { + $this->printer->printResults(); + $this->log(); + $this->logCoverage(); + $readers = $this->interpreter->getReaders(); + foreach ($readers as $reader) { + $reader->removeLog(); + } + } + /** * Returns the highest exit code encountered * throughout the course of test execution. @@ -112,7 +148,9 @@ final protected function logCoverage(): void $coverageMerger = $this->getCoverage(); assert($coverageMerger !== null); - $reporter = $coverageMerger->getReporter(); + $codeCoverage = $coverageMerger->getCodeCoverageObject(); + assert($codeCoverage !== null); + $reporter = new CoverageReporter($codeCoverage); if (($coverageClover = $this->options->coverageClover()) !== null) { $reporter->clover($coverageClover); @@ -141,15 +179,6 @@ final protected function logCoverage(): void $reporter->php($coveragePhp); } - private function initCoverage(): void - { - if (! $this->options->hasCoverage()) { - return; - } - - $this->coverage = new CoverageMerger($this->options->coverageTestLimit()); - } - final protected function hasCoverage(): bool { return $this->options->hasCoverage(); @@ -159,11 +188,4 @@ final protected function getCoverage(): ?CoverageMerger { return $this->coverage; } - - final protected function initialize(): void - { - $this->initCoverage(); - $this->load(new SuiteLoader($this->options)); - $this->printer->start(); - } } diff --git a/src/Runners/PHPUnit/BaseWrapperRunner.php b/src/Runners/PHPUnit/BaseWrapperRunner.php index 1767ff09..56580411 100644 --- a/src/Runners/PHPUnit/BaseWrapperRunner.php +++ b/src/Runners/PHPUnit/BaseWrapperRunner.php @@ -4,63 +4,28 @@ namespace ParaTest\Runners\PHPUnit; -use RuntimeException; +use InvalidArgumentException; +use PHPUnit\TextUI\TestRunner; abstract class BaseWrapperRunner extends BaseRunner { - private const PHPUNIT_FAILURES = 1; - - private const PHPUNIT_ERRORS = 2; - - /** @var resource[] */ - protected $streams = []; - - /** @var resource[] */ - protected $modified = []; - final protected function beforeLoadChecks(): void { if ($this->options->functional()) { - throw new RuntimeException( + throw new InvalidArgumentException( 'The `functional` option is not supported yet in the WrapperRunner. Only full classes can be run due ' . 'to the current PHPUnit commands causing classloading issues.' ); } } - final protected function complete(): void - { - $this->setExitCode(); - $this->printer->printResults(); - $this->interpreter->rewind(); - $this->log(); - $this->logCoverage(); - $readers = $this->interpreter->getReaders(); - foreach ($readers as $reader) { - $reader->removeLog(); - } - } - - private function setExitCode(): void + final protected function setExitCode(): void { + $this->exitcode = TestRunner::SUCCESS_EXIT; if ($this->interpreter->getTotalErrors() > 0) { - $this->exitcode = self::PHPUNIT_ERRORS; + $this->exitcode = TestRunner::EXCEPTION_EXIT; } elseif ($this->interpreter->getTotalFailures() > 0) { - $this->exitcode = self::PHPUNIT_FAILURES; - } else { - $this->exitcode = 0; + $this->exitcode = TestRunner::FAILURE_EXIT; } } - - /* - private function testIsStillRunning($test) - { - if(!$test->isDoneRunning()) return true; - $this->setExitCode($test); - $test->stop(); - if (static::PHPUNIT_FATAL_ERROR === $test->getExitCode()) - throw new \Exception($test->getStderr(), $test->getExitCode()); - return false; - } - */ } diff --git a/src/Runners/PHPUnit/EmptyRunnerStub.php b/src/Runners/PHPUnit/EmptyRunnerStub.php deleted file mode 100644 index bd52654f..00000000 --- a/src/Runners/PHPUnit/EmptyRunnerStub.php +++ /dev/null @@ -1,20 +0,0 @@ -printer->start(); - $this->output->write(self::OUTPUT); - } - - protected function beforeLoadChecks(): void - { - } -} diff --git a/src/Runners/PHPUnit/ExecutableTest.php b/src/Runners/PHPUnit/ExecutableTest.php index d6f55f1b..a84aedae 100644 --- a/src/Runners/PHPUnit/ExecutableTest.php +++ b/src/Runners/PHPUnit/ExecutableTest.php @@ -4,13 +4,9 @@ namespace ParaTest\Runners\PHPUnit; -use PHPUnit\TextUI\XmlConfiguration\Configuration; -use Symfony\Component\Process\Process; - use function array_map; use function array_merge; use function assert; -use function sys_get_temp_dir; use function tempnam; use function unlink; @@ -21,7 +17,7 @@ abstract class ExecutableTest * * @var string */ - protected $path; + private $path; /** * A path to the temp file created @@ -29,29 +25,32 @@ abstract class ExecutableTest * * @var string|null */ - protected $temp; + private $temp; /** * Path where the coveragereport is stored. * * @var string|null */ - protected $coverageFileName; + private $coverageFileName; /** * Last executed process command. * * @var string */ - protected $lastCommand = ''; + private $lastCommand = ''; /** @var bool */ private $needsCoverage; + /** @var string */ + private $tmpDir; - public function __construct(string $path, bool $needsCoverage) + public function __construct(string $path, bool $needsCoverage, string $tmpDir) { $this->path = $path; $this->needsCoverage = $needsCoverage; + $this->tmpDir = $tmpDir; } /** @@ -75,7 +74,7 @@ final public function getPath(): string final public function getTempFile(): string { if ($this->temp === null) { - $temp = tempnam(sys_get_temp_dir(), 'PT_'); + $temp = tempnam($this->tmpDir, 'PT_'); assert($temp !== false); $this->temp = $temp; @@ -89,8 +88,17 @@ final public function getTempFile(): string */ final public function deleteFile(): void { - $outputFile = $this->getTempFile(); - unlink($outputFile); + if ($this->temp !== null) { + unlink($this->temp); + $this->temp = null; + } + + if ($this->coverageFileName === null) { + return; + } + + unlink($this->coverageFileName); + $this->coverageFileName = null; } /** @@ -112,13 +120,13 @@ final public function setLastCommand(string $command): void /** * Generate command line arguments with passed options suitable to handle through paratest. * - * @param string $binary executable binary name - * @param array $options command line options - * @param string[]|null $passthru + * @param string $binary executable binary name + * @param array $options command line options + * @param string[]|null $passthru * * @return string[] command line arguments */ - final public function commandArguments(string $binary, array $options = [], ?array $passthru = null): array + final public function commandArguments(string $binary, array $options, ?array $passthru): array { $options = array_merge($this->prepareOptions($options), ['log-junit' => $this->getTempFile()]); if ($this->needsCoverage) { @@ -131,7 +139,7 @@ final public function commandArguments(string $binary, array $options = [], ?arr } foreach ($options as $key => $value) { - $arguments[] = "--$key"; + $arguments[] = "--{$key}"; if ($value === null) { continue; } @@ -145,27 +153,13 @@ final public function commandArguments(string $binary, array $options = [], ?arr return $arguments; } - /** - * Generate command line with passed options suitable to handle through paratest. - * - * @param string $binary executable binary name - * @param array $options command line options - * @param string[]|null $passthru - * - * @return string command line - */ - final public function command(string $binary, array $options = [], ?array $passthru = null): string - { - return (new Process($this->commandArguments($binary, $options, $passthru)))->getCommandLine(); - } - /** * Get coverage filename. */ final public function getCoverageFileName(): string { if ($this->coverageFileName === null) { - $coverageFileName = tempnam(sys_get_temp_dir(), 'CV_'); + $coverageFileName = tempnam($this->tmpDir, 'CV_'); assert($coverageFileName !== false); $this->coverageFileName = $coverageFileName; @@ -174,20 +168,12 @@ final public function getCoverageFileName(): string return $this->coverageFileName; } - /** - * Set process temporary filename. - */ - final public function setTempFile(string $temp): void - { - $this->temp = $temp; - } - /** * A template method that can be overridden to add necessary options for a test. * - * @param array $options + * @param array $options * - * @return array + * @return array */ abstract protected function prepareOptions(array $options): array; } diff --git a/src/Runners/PHPUnit/FullSuite.php b/src/Runners/PHPUnit/FullSuite.php index e2b58da8..295b4ec4 100644 --- a/src/Runners/PHPUnit/FullSuite.php +++ b/src/Runners/PHPUnit/FullSuite.php @@ -9,17 +9,13 @@ final class FullSuite extends ExecutableTest { /** @var string */ - protected $suiteName; + private $suiteName; - /** @var string */ - protected $configPath; - - public function __construct(string $suiteName, string $configPath, bool $needsCoverage) + public function __construct(string $suiteName, bool $needsCoverage, string $tmpDir) { - parent::__construct('', $needsCoverage); + parent::__construct('', $needsCoverage, $tmpDir); - $this->suiteName = $suiteName; - $this->configPath = $configPath; + $this->suiteName = $suiteName; } /** diff --git a/src/Runners/PHPUnit/Options.php b/src/Runners/PHPUnit/Options.php index 81ea5a48..8afc9bd5 100644 --- a/src/Runners/PHPUnit/Options.php +++ b/src/Runners/PHPUnit/Options.php @@ -4,10 +4,10 @@ namespace ParaTest\Runners\PHPUnit; +use InvalidArgumentException; use ParaTest\Util\Str; use PHPUnit\TextUI\XmlConfiguration\Configuration; use PHPUnit\TextUI\XmlConfiguration\Loader; -use RuntimeException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; @@ -24,7 +24,6 @@ use function file_exists; use function file_get_contents; use function implode; -use function in_array; use function intdiv; use function is_dir; use function is_file; @@ -36,6 +35,8 @@ use function realpath; use function sprintf; use function strlen; +use function sys_get_temp_dir; +use function uniqid; use function unserialize; use const DIRECTORY_SEPARATOR; @@ -47,6 +48,9 @@ */ final class Options { + public const ENV_KEY_TOKEN = 'TEST_TOKEN'; + public const ENV_KEY_UNIQUE_TOKEN = 'UNIQUE_TEST_TOKEN'; + /** * @see \PHPUnit\Util\Configuration * @see https://github.com/sebastianbergmann/phpunit/commit/80754cf323fe96003a2567f5e57404fddecff3bf @@ -95,7 +99,7 @@ final class Options * A collection of post-processed option values. This is the collection * containing ParaTest specific options. * - * @var array + * @var array */ private $filtered; @@ -127,14 +131,6 @@ final class Options /** @var string[] */ private $excludeGroup; - /** - * A collection of option values directly corresponding - * to certain annotations - i.e group. - * - * @var array - */ - private $annotations = []; - /** * Running the suite defined in the config in parallel. * @@ -190,18 +186,18 @@ final class Options private $logJunit; /** @var string|null */ private $whitelist; + /** @var string */ + private $tmpDir; /** - * @param array $annotations - * @param array $filtered - * @param string[] $testsuite - * @param string[] $group - * @param string[] $excludeGroup - * @param string[]|null $passthru - * @param string[]|null $passthruPhp + * @param array $filtered + * @param string[] $testsuite + * @param string[] $group + * @param string[] $excludeGroup + * @param string[]|null $passthru + * @param string[]|null $passthruPhp */ private function __construct( - array $annotations, ?string $bootstrap, bool $colors, ?Configuration $configuration, @@ -229,10 +225,10 @@ private function __construct( string $runner, bool $stopOnFailure, array $testsuite, + string $tmpDir, int $verbose, ?string $whitelist ) { - $this->annotations = $annotations; $this->bootstrap = $bootstrap; $this->colors = $colors; $this->configuration = $configuration; @@ -260,6 +256,7 @@ private function __construct( $this->runner = $runner; $this->stopOnFailure = $stopOnFailure; $this->testsuite = $testsuite; + $this->tmpDir = $tmpDir; $this->verbose = $verbose; $this->whitelist = $whitelist; } @@ -300,7 +297,7 @@ public static function fromConsoleInput(InputInterface $input, string $cwd): sel : []; if (isset($options['filter']) && strlen($options['filter']) > 0 && ! $options['functional']) { - throw new RuntimeException('Option --filter is not implemented for non functional mode'); + throw new InvalidArgumentException('Option --filter is not implemented for non functional mode'); } $configuration = null; @@ -326,10 +323,11 @@ public static function fromConsoleInput(InputInterface $input, string $cwd): sel $filtered['exclude-group'] = implode(',', $excludeGroup); } - $annotations = self::initAnnotations($filtered); + if ($options['whitelist'] !== null) { + $filtered['whitelist'] = $options['whitelist']; + } return new self( - $annotations, $options['bootstrap'], $options['colors'], $configuration, @@ -357,6 +355,7 @@ public static function fromConsoleInput(InputInterface $input, string $cwd): sel $options['runner'], $options['stop-on-failure'], $testsuite, + $options['tmp-dir'], (int) $options['verbose'], $options['whitelist'] ); @@ -552,6 +551,13 @@ public static function setInputDefinition(InputDefinition $inputDefinition): voi InputOption::VALUE_REQUIRED, 'Filter which testsuite to run' ), + new InputOption( + 'tmp-dir', + null, + InputOption::VALUE_REQUIRED, + 'Temporary directory for internal ParaTest files', + sys_get_temp_dir() + ), new InputOption( 'verbose', 'v', @@ -587,7 +593,7 @@ private static function getPhpunitBinary(): string return $phpunit; } - return 'phpunit'; + return 'phpunit'; // @codeCoverageIgnore } /** @@ -597,9 +603,9 @@ private static function getPhpunitBinary(): string */ private static function vendorDir(): string { - $vendor = dirname(dirname(dirname(__DIR__))) . DIRECTORY_SEPARATOR . 'vendor'; + $vendor = dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'vendor'; if (! file_exists($vendor)) { - $vendor = dirname(dirname(dirname(dirname(dirname(__DIR__))))); + $vendor = dirname(__DIR__, 5); // @codeCoverageIgnore } return $vendor; @@ -640,29 +646,6 @@ private static function isAbsolutePath(string $path): bool return $path[0] === DIRECTORY_SEPARATOR || preg_match('~\A[A-Z]:(?![^/\\\\])~i', $path) > 0; } - /** - * Load options that are represented by annotations - * inside of tests i.e @group group1 = --group group1. - * - * @param array $filtered - * - * @return array - */ - private static function initAnnotations(array $filtered): array - { - $annotations = []; - $annotatedOptions = ['group']; - foreach ($filtered as $key => $value) { - if (! in_array($key, $annotatedOptions, true)) { - continue; - } - - $annotations[$key] = $value; - } - - return $annotations; - } - /** * Return number of (logical) CPU cores, use 2 as fallback. * @@ -679,6 +662,7 @@ public static function getNumberOfCPUCores(): int assert($cpuinfo !== false); preg_match_all('/^processor/m', $cpuinfo, $matches); $cores = count($matches[0]); + // @codeCoverageIgnoreStart } elseif (DIRECTORY_SEPARATOR === '\\') { // Windows if (($process = @popen('wmic cpu get NumberOfCores', 'rb')) !== false) { @@ -692,6 +676,8 @@ public static function getNumberOfCPUCores(): int pclose($process); } + // @codeCoverageIgnoreEnd + return $cores; } @@ -754,7 +740,7 @@ public function stopOnFailure(): bool return $this->stopOnFailure; } - /** @return array */ + /** @return array */ public function filtered(): array { return $this->filtered; @@ -803,12 +789,6 @@ public function excludeGroup(): array return $this->excludeGroup; } - /** @return array */ - public function annotations(): array - { - return $this->annotations; - } - public function parallelSuite(): bool { return $this->parallelSuite; @@ -876,8 +856,27 @@ public function logJunit(): ?string return $this->logJunit; } + public function tmpDir(): string + { + return $this->tmpDir; + } + public function whitelist(): ?string { return $this->whitelist; } + + /** + * @return array{PARATEST: int, TEST_TOKEN?: int, UNIQUE_TEST_TOKEN?: string} + */ + public function fillEnvWithTokens(int $inc): array + { + $env = ['PARATEST' => 1]; + if (! $this->noTestTokens()) { + $env[self::ENV_KEY_TOKEN] = $inc; + $env[self::ENV_KEY_UNIQUE_TOKEN] = uniqid($inc . '_'); + } + + return $env; + } } diff --git a/src/Runners/PHPUnit/ResultPrinter.php b/src/Runners/PHPUnit/ResultPrinter.php index b017d73a..15a16e8e 100644 --- a/src/Runners/PHPUnit/ResultPrinter.php +++ b/src/Runners/PHPUnit/ResultPrinter.php @@ -106,13 +106,10 @@ public function __construct(LogInterpreter $results, OutputInterface $output, Op /** * Adds an ExecutableTest to the tracked results. */ - public function addTest(ExecutableTest $suite): self + public function addTest(ExecutableTest $suite): void { $this->suites[] = $suite; - $increment = $suite->getTestCount(); - $this->totalCases += $increment; - - return $this; + $this->totalCases += $suite->getTestCount(); } /** @@ -152,15 +149,6 @@ public function println(string $string = ''): void * Prints all results and removes any log files * used for aggregating results. */ - public function flush(): void - { - $this->printResults(); - $this->clearLogs(); - } - - /** - * Print final results. - */ public function printResults(): void { $this->output->write($this->getHeader()); @@ -506,15 +494,4 @@ private function red(string $text): string return $text; } - - /** - * Deletes all the temporary log files for ExecutableTest objects - * being printed. - */ - private function clearLogs(): void - { - foreach ($this->suites as $suite) { - $suite->deleteFile(); - } - } } diff --git a/src/Runners/PHPUnit/Runner.php b/src/Runners/PHPUnit/Runner.php index a0f98beb..761b9bbc 100644 --- a/src/Runners/PHPUnit/Runner.php +++ b/src/Runners/PHPUnit/Runner.php @@ -5,25 +5,22 @@ namespace ParaTest\Runners\PHPUnit; use Exception; -use Habitat\Habitat; +use ParaTest\Coverage\EmptyCoverageFileException; use ParaTest\Runners\PHPUnit\Worker\RunnerWorker; -use RuntimeException; +use PHPUnit\TextUI\TestRunner; use Symfony\Component\Console\Output\OutputInterface; -use Throwable; use function array_filter; use function array_keys; +use function array_merge; use function array_shift; use function assert; use function count; -use function sprintf; -use function uniqid; +use function getenv; use function usleep; final class Runner extends BaseRunner { - private const PHPUNIT_FATAL_ERROR = 255; - /** * A collection of ExecutableTest objects that have processes * currently running. @@ -36,7 +33,7 @@ final class Runner extends BaseRunner * A collection of available tokens based on the number * of processes specified in $options. * - * @var array + * @var array */ private $tokens = []; @@ -49,52 +46,21 @@ public function __construct(Options $opts, OutputInterface $output) /** * The money maker. Runs all ExecutableTest objects in separate processes. */ - public function run(): void + protected function doRun(): void { - $this->initialize(); - while (count($this->running) > 0 || count($this->pending) > 0) { foreach ($this->running as $key => $test) { - try { - if (! $this->testIsStillRunning($test)) { - unset($this->running[$key]); - $this->releaseToken($key); - } - } catch (Throwable $e) { - if ($this->options->verbose() > 0) { - $this->output->writeln("An error for $key: {$e->getMessage()}"); - $this->output->writeln("Command: {$test->getExecutableTest()->getLastCommand()}"); - $this->output->writeln('StdErr: ' . $test->getStderr()); - $this->output->writeln('StdOut: ' . $test->getStdout()); - } - - throw $e; + if ($this->testIsStillRunning($test)) { + continue; } + + unset($this->running[$key]); + $this->releaseToken($key); } $this->fillRunQueue(); usleep(10000); } - - $this->complete(); - } - - /** - * Finalizes the run process. This method - * prints all results, rewinds the log interpreter, - * logs any results to JUnit, and cleans up temporary - * files. - */ - private function complete(): void - { - $this->printer->printResults(); - $this->interpreter->rewind(); - $this->log(); - $this->logCoverage(); - $readers = $this->interpreter->getReaders(); - foreach ($readers as $reader) { - $reader->removeLog(); - } } /** @@ -104,22 +70,13 @@ private function complete(): void */ private function fillRunQueue(): void { - while (count($this->pending) > 0 && count($this->running) < $this->options->processes()) { - $tokenData = $this->getNextAvailableToken(); - if ($tokenData === false) { - continue; - } - + while ( + count($this->pending) > 0 + && count($this->running) < $this->options->processes() + && ($tokenData = $this->getNextAvailableToken()) !== false + ) { $this->acquireToken($tokenData['token']); - $env = []; - if (! $this->options->noTestTokens()) { - $env = [ - 'TEST_TOKEN' => $tokenData['token'], - 'UNIQUE_TEST_TOKEN' => $tokenData['unique'], - ]; - } - - $env += Habitat::getAll(); + $env = array_merge(getenv(), $this->options->fillEnvWithTokens($tokenData['token'])); $executebleTest = array_shift($this->pending); /** @psalm-suppress RedundantConditionGivenDocblockType **/ @@ -164,20 +121,26 @@ private function testIsStillRunning(RunnerWorker $worker): bool } $executableTest = $worker->getExecutableTest(); - if ($worker->getExitCode() === self::PHPUNIT_FATAL_ERROR) { - $errorOutput = $worker->getStderr(); - if ($errorOutput === '') { - $errorOutput = $worker->getStdout(); - } - - throw new RuntimeException(sprintf("Fatal error in %s:\n%s", $executableTest->getPath(), $errorOutput)); + if ( + $worker->getExitCode() > 0 + && $worker->getExitCode() !== TestRunner::FAILURE_EXIT + && $worker->getExitCode() !== TestRunner::EXCEPTION_EXIT + ) { + throw new WorkerCrashedException($worker->getCrashReport()); } - $this->printer->printFeedback($executableTest); if ($this->hasCoverage()) { - $this->addCoverage($executableTest); + $coverageMerger = $this->getCoverage(); + assert($coverageMerger !== null); + try { + $coverageMerger->addCoverageFromFile($executableTest->getCoverageFileName()); + } catch (EmptyCoverageFileException $emptyCoverageFileException) { + throw new WorkerCrashedException($worker->getCrashReport(), 0, $emptyCoverageFileException); + } } + $this->printer->printFeedback($executableTest); + return false; } @@ -204,7 +167,10 @@ private function initTokens(): void { $this->tokens = []; for ($i = 1; $i <= $this->options->processes(); ++$i) { - $this->tokens[$i] = ['token' => $i, 'unique' => uniqid(sprintf('%s_', $i)), 'available' => true]; + $this->tokens[$i] = [ + 'token' => $i, + 'available' => true, + ]; } } @@ -212,7 +178,7 @@ private function initTokens(): void * Gets the next token that is available to be acquired * from a finished process. * - * @return false|array{token: int, unique: string, available: bool} + * @return false|array{token: int, available: bool} */ private function getNextAvailableToken() { @@ -253,14 +219,6 @@ private function acquireToken(int $tokenIdentifier): void $this->tokens[$keys[0]]['available'] = false; } - private function addCoverage(ExecutableTest $test): void - { - $coverageFile = $test->getCoverageFileName(); - $coverageMerger = $this->getCoverage(); - assert($coverageMerger !== null); - $coverageMerger->addCoverageFromFile($coverageFile); - } - protected function beforeLoadChecks(): void { } diff --git a/src/Runners/PHPUnit/SqliteRunner.php b/src/Runners/PHPUnit/SqliteRunner.php index c1be2d88..ac0070d5 100644 --- a/src/Runners/PHPUnit/SqliteRunner.php +++ b/src/Runners/PHPUnit/SqliteRunner.php @@ -9,16 +9,16 @@ use RuntimeException; use Symfony\Component\Console\Output\OutputInterface; +use function array_map; use function assert; use function count; use function dirname; use function implode; use function realpath; use function serialize; -use function sys_get_temp_dir; use function tempnam; -use function uniqid; use function unlink; +use function unserialize; use function usleep; use const DIRECTORY_SEPARATOR; @@ -39,7 +39,10 @@ public function __construct(Options $opts, OutputInterface $output) { parent::__construct($opts, $output); - $this->dbFileName = (string) ($opts->filtered()['database'] ?? tempnam(sys_get_temp_dir(), 'paratest_db_')); + $dbFileName = tempnam($opts->tmpDir(), 'paratest_db_'); + assert($dbFileName !== false); + + $this->dbFileName = $dbFileName; $this->db = new PDO('sqlite:' . $this->dbFileName); } @@ -49,15 +52,14 @@ public function __destruct() unlink($this->dbFileName); } - public function run(): void + protected function doRun(): void { - $this->initialize(); $this->createTable(); $this->assignAllPendingTests(); $this->startWorkers(); $this->waitForAllToFinish(); - $this->complete(); $this->checkIfWorkersCrashed(); + $this->setExitCode(); } /** @@ -72,15 +74,8 @@ private function startWorkers(): void for ($i = 1; $i <= $this->options->processes(); ++$i) { $worker = new SqliteWorker($this->output, $this->dbFileName); - if ($this->options->noTestTokens()) { - $token = null; - $uniqueToken = null; - } else { - $token = $i; - $uniqueToken = uniqid(); - } - $worker->start($wrapper, $token, $uniqueToken); + $worker->start($wrapper, $this->options, $i); $this->workers[] = $worker; } } @@ -111,17 +106,18 @@ private function waitForAllToFinish(): void */ private function createTable(): void { - $statement = 'CREATE TABLE tests ( - id INTEGER PRIMARY KEY, - command TEXT NOT NULL UNIQUE, - file_name TEXT NOT NULL, - reserved_by_process_id INTEGER, - completed INTEGER DEFAULT 0 - )'; - - if ($this->db->exec($statement) === false) { - throw new RuntimeException('Error while creating sqlite database table: ' . $this->db->errorCode()); - } + $statement = ' + CREATE TABLE tests ( + id INTEGER PRIMARY KEY, + command TEXT NOT NULL UNIQUE, + file_name TEXT NOT NULL, + reserved_by_process_id INTEGER, + completed INTEGER DEFAULT 0 + ) + '; + + $tableCreationResult = $this->db->exec($statement); + assert($tableCreationResult !== false); } /** @@ -134,7 +130,8 @@ private function assignAllPendingTests(): void ->execute([ ':command' => serialize($test->commandArguments( $this->options->phpunit(), - $this->options->filtered() + $this->options->filtered(), + $this->options->passthru() )), ':fileName' => $fileName, ]); @@ -151,7 +148,14 @@ private function printOutput(): void $tests = $stmt->fetchAll(); assert($tests !== false); foreach ($tests as $test) { - $this->printer->printFeedback($this->pending[$test['file_name']]); + $executableTest = $this->pending[$test['file_name']]; + if ($this->hasCoverage()) { + $coverageMerger = $this->getCoverage(); + assert($coverageMerger !== null); + $coverageMerger->addCoverageFromFile($executableTest->getCoverageFileName()); + } + + $this->printer->printFeedback($executableTest); $this->db->prepare('DELETE FROM tests WHERE id = :id')->execute([ 'id' => $test['id'], ]); @@ -171,8 +175,12 @@ private function checkIfWorkersCrashed(): void $commandStmt = $this->db->query('SELECT command FROM tests'); assert($commandStmt !== false); + $commands = (array) $commandStmt->fetchAll(PDO::FETCH_COLUMN); + $commands = array_map(static function (string $serializedCommand): string { + return implode(' ', array_map('escapeshellarg', unserialize($serializedCommand))); + }, $commands); - throw new RuntimeException( + throw new WorkerCrashedException( 'Some workers have crashed.' . PHP_EOL . '----------------------' . PHP_EOL . 'All workers have quit, but some tests are still to be executed.' . PHP_EOL @@ -180,7 +188,7 @@ private function checkIfWorkersCrashed(): void . '----------------------' . PHP_EOL . 'Failed test command(s):' . PHP_EOL . '----------------------' . PHP_EOL - . implode(PHP_EOL, (array) $commandStmt->fetchAll(PDO::FETCH_COLUMN)) + . implode(PHP_EOL, $commands) ); } } diff --git a/src/Runners/PHPUnit/Suite.php b/src/Runners/PHPUnit/Suite.php index 3d0119d8..12ec9cf9 100644 --- a/src/Runners/PHPUnit/Suite.php +++ b/src/Runners/PHPUnit/Suite.php @@ -23,9 +23,9 @@ final class Suite extends ExecutableTest /** * @param TestMethod[] $functions */ - public function __construct(string $path, array $functions, bool $needsCoverage) + public function __construct(string $path, array $functions, bool $needsCoverage, string $tmpDir) { - parent::__construct($path, $needsCoverage); + parent::__construct($path, $needsCoverage, $tmpDir); $this->functions = $functions; } diff --git a/src/Runners/PHPUnit/SuiteLoader.php b/src/Runners/PHPUnit/SuiteLoader.php index eba6722a..36c9a845 100644 --- a/src/Runners/PHPUnit/SuiteLoader.php +++ b/src/Runners/PHPUnit/SuiteLoader.php @@ -30,6 +30,7 @@ use function sprintf; use function strrpos; use function substr; +use function trim; use function version_compare; use const PHP_VERSION; @@ -60,16 +61,12 @@ final class SuiteLoader */ private $configuration; - /** @var Options|null */ - public $options; + /** @var Options */ + private $options; - public function __construct(?Options $options = null) + public function __construct(Options $options) { - $this->options = $options; - if ($options === null) { - return; - } - + $this->options = $options; $this->configuration = $options->configuration(); } @@ -107,18 +104,17 @@ public function getTestMethods(): array * * @throws RuntimeException */ - public function load(?string $path = null): void + public function load(): void { $this->loadConfiguration(); - if ($path !== null) { + if (($path = $this->options->path()) !== null) { $this->files = array_merge( $this->files, (new Facade())->getFilesAsArray($path, ['Test.php']) ); } elseif ( - $this->options !== null - && $this->options->parallelSuite() + $this->options->parallelSuite() && $this->configuration !== null && ! $this->configuration->testSuite()->isEmpty() ) { @@ -130,13 +126,13 @@ public function load(?string $path = null): void && ! $this->configuration->testSuite()->isEmpty() ) { $testSuiteCollection = $this->configuration->testSuite()->asArray(); - if ($this->options !== null && count($this->options->testsuite()) > 0) { + if (count($this->options->testsuite()) > 0) { $suitesName = array_map(static function (TestSuite $testSuite): string { return $testSuite->name(); }, $testSuiteCollection); foreach ($this->options->testsuite() as $testSuiteName) { if (! in_array($testSuiteName, $suitesName, true)) { - throw new RuntimeException("Suite path $testSuiteName could not be found"); + throw new RuntimeException("Suite path {$testSuiteName} could not be found"); } } @@ -170,9 +166,8 @@ public function load(?string $path = null): void private function initSuites(): void { if (is_array($this->suitesName)) { - assert($this->configuration !== null); foreach ($this->suitesName as $suiteName) { - $this->loadedSuites[$suiteName] = $this->createFullSuite($suiteName, $this->configuration->filename()); + $this->loadedSuites[$suiteName] = $this->createFullSuite($suiteName); } } else { foreach ($this->files as $path) { @@ -202,7 +197,8 @@ private function executableTests(string $path, ParsedClass $class): array $executableTests[] = new TestMethod( $path, $methodBatch, - $this->options !== null && $this->options->hasCoverage() + $this->options->hasCoverage(), + $this->options->tmpDir() ); } @@ -219,8 +215,8 @@ private function executableTests(string $path, ParsedClass $class): array */ private function getMethodBatches(ParsedClass $class): array { - $classMethods = $class->getMethods($this->options !== null ? $this->options->annotations() : []); - $maxBatchSize = $this->options !== null && $this->options->functional() ? $this->options->maxBatchSize() : 0; + $classMethods = $class->getMethods(); + $maxBatchSize = $this->options->functional() ? $this->options->maxBatchSize() : 0; assert($maxBatchSize !== null); $batches = []; @@ -328,7 +324,7 @@ private function getMethodTests(ParsedClass $class, ParsedFunction $method): arr */ private function testMatchGroupOptions(array $groups): bool { - if ($this->options === null || count($this->options->group()) === 0) { + if (count($this->options->group()) === 0) { return true; } @@ -342,13 +338,11 @@ private function testMatchGroupOptions(array $groups): bool private function testMatchFilterOptions(string $className, string $name): bool { - if ($this->options === null || ($filter = $this->options->filter()) === null) { + if (($filter = $this->options->filter()) === null) { return true; } - $re = substr($filter, 0, 1) === '/' - ? $filter - : '/' . $filter . '/'; + $re = '/' . trim($filter, '/') . '/'; $fullName = $className . '::' . $name; return preg_match($re, $fullName) === 1; @@ -362,16 +356,17 @@ private function createSuite(string $path, ParsedClass $class): Suite $path, $class ), - $this->options !== null && $this->options->hasCoverage() + $this->options->hasCoverage(), + $this->options->tmpDir() ); } - private function createFullSuite(string $suiteName, string $configPath): FullSuite + private function createFullSuite(string $suiteName): FullSuite { return new FullSuite( $suiteName, - $configPath, - $this->options !== null && $this->options->hasCoverage() + $this->options->hasCoverage(), + $this->options->tmpDir() ); } @@ -388,7 +383,7 @@ private function loadFilesFromTestSuite(TestSuite $testSuiteCollection): void $directory->phpVersionOperator()->asString() ) ) { - continue; + continue; // @codeCoverageIgnore } $exclude = []; @@ -413,7 +408,7 @@ private function loadFilesFromTestSuite(TestSuite $testSuiteCollection): void $file->phpVersionOperator()->asString() ) ) { - continue; + continue; // @codeCoverageIgnore } $this->files[] = $file->path(); @@ -427,7 +422,7 @@ private function loadConfiguration(): void } $bootstrap = null; - if ($this->options !== null && $this->options->bootstrap() !== null) { + if ($this->options->bootstrap() !== null) { $bootstrap = $this->options->bootstrap(); } elseif ($this->configuration !== null && $this->configuration->phpunit()->hasBootstrap()) { $bootstrap = $this->configuration->phpunit()->bootstrap(); diff --git a/src/Runners/PHPUnit/SuitePath.php b/src/Runners/PHPUnit/SuitePath.php deleted file mode 100644 index 8a4abad2..00000000 --- a/src/Runners/PHPUnit/SuitePath.php +++ /dev/null @@ -1,61 +0,0 @@ -path = $path; - $this->excludedPaths = $excludedPaths; - $this->suffix = $suffix; - } - - public function getPath(): string - { - return $this->path; - } - - /** - * @return string[] - */ - public function getExcludedPaths(): array - { - return $this->excludedPaths; - } - - public function getSuffix(): string - { - return $this->suffix; - } - - public function getPattern(): string - { - return '|' . preg_quote($this->getSuffix()) . '$|'; - } -} diff --git a/src/Runners/PHPUnit/TestMethod.php b/src/Runners/PHPUnit/TestMethod.php index f88eae57..f8f09f4a 100644 --- a/src/Runners/PHPUnit/TestMethod.php +++ b/src/Runners/PHPUnit/TestMethod.php @@ -4,8 +4,6 @@ namespace ParaTest\Runners\PHPUnit; -use PHPUnit\TextUI\XmlConfiguration\Configuration; - use function array_reduce; use function count; use function implode; @@ -26,7 +24,7 @@ final class TestMethod extends ExecutableTest * * @var string[] */ - protected $filters; + private $filters; /** * Passed filters must be unescaped and must represent test name, optionally including @@ -35,24 +33,14 @@ final class TestMethod extends ExecutableTest * @param string $testPath path to phpunit test case file * @param string[] $filters array of filters or single filter */ - public function __construct(string $testPath, array $filters, bool $needsCoverage) + public function __construct(string $testPath, array $filters, bool $needsCoverage, string $tmpDir) { - parent::__construct($testPath, $needsCoverage); + parent::__construct($testPath, $needsCoverage, $tmpDir); // for compatibility with other code (tests), which can pass string (one filter) // instead of array of filters $this->filters = $filters; } - /** - * Returns the test method's filters. - * - * @return string[] - */ - public function getFilters(): array - { - return $this->filters; - } - /** * Returns the test method's name. * @@ -69,9 +57,9 @@ public function getName(): string * This sets up the --filter switch used to run a single PHPUnit test method. * This method also provide escaping for method name to be used as filter regexp. * - * @param array $options + * @param array $options * - * @return array + * @return array */ protected function prepareOptions(array $options): array { diff --git a/src/Runners/PHPUnit/Worker/BaseWorker.php b/src/Runners/PHPUnit/Worker/BaseWorker.php index 6482a21d..bf2ced8a 100644 --- a/src/Runners/PHPUnit/Worker/BaseWorker.php +++ b/src/Runners/PHPUnit/Worker/BaseWorker.php @@ -5,21 +5,20 @@ namespace ParaTest\Runners\PHPUnit\Worker; use ParaTest\Runners\PHPUnit\Options; -use RuntimeException; +use ParaTest\Runners\PHPUnit\WorkerCrashedException; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\PhpExecutableFinder; use function array_map; +use function array_merge; use function assert; use function count; use function end; use function escapeshellarg; use function explode; -use function fclose; use function fread; use function getenv; use function implode; -use function is_numeric; use function is_resource; use function proc_get_status; use function proc_open; @@ -33,12 +32,6 @@ abstract class BaseWorker { - /** @var string[][] */ - protected static $descriptorspec = [ - 0 => ['pipe', 'r'], - 1 => ['pipe', 'w'], - 2 => ['pipe', 'w'], - ]; /** @var resource|null */ protected $proc; /** @var resource[] */ @@ -50,6 +43,12 @@ abstract class BaseWorker /** @var string[] */ protected $commands = []; + /** @var string[][] */ + private static $descriptorspec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; /** @var int|null */ private $exitCode = null; /** @var string */ @@ -62,52 +61,39 @@ public function __construct(OutputInterface $output) $this->output = $output; } - /** - * @param string[] $parameters - */ final public function start( string $wrapperBinary, - ?int $token = 1, - ?string $uniqueToken = null, - array $parameters = [], - ?Options $options = null + Options $options, + int $token ): void { - $env = getenv(); - $env['PARATEST'] = 1; - if (is_numeric($token)) { - $env['XDEBUG_CONFIG'] = 'true'; - $env['TEST_TOKEN'] = $token; - } - - if ($uniqueToken !== null) { - $env['UNIQUE_TEST_TOKEN'] = $uniqueToken; - } + $env = array_merge(getenv(), $options->fillEnvWithTokens($token)); $finder = new PhpExecutableFinder(); $phpExecutable = $finder->find(); assert($phpExecutable !== false); $bin = escapeshellarg($phpExecutable); - if ($options !== null && ($passthruPhp = $options->passthruPhp()) !== null) { - $bin .= ' ' . implode(' ', $passthruPhp) . ' '; + if (($passthruPhp = $options->passthruPhp()) !== null) { + $bin .= ' ' . implode(' ', $passthruPhp) . ' '; } $bin .= ' ' . escapeshellarg($wrapperBinary); + $parameters = []; $this->configureParameters($parameters); if (count($parameters) > 0) { $bin .= ' ' . implode(' ', array_map('escapeshellarg', $parameters)); } $pipes = []; - if ($options !== null && $options->verbose() > 0) { - $this->output->writeln("Starting WrapperWorker via: $bin"); + if ($options->verbose() > 0) { + $this->output->writeln("Starting WrapperWorker via: {$bin}"); } // Taken from \Symfony\Component\Process\Process::prepareWindowsCommandLine // Needed to handle spaces in the binary path, boring to test in CI if (DIRECTORY_SEPARATOR === '\\') { - $bin = sprintf('cmd /V:ON /E:ON /D /C (%s)', $bin); + $bin = sprintf('cmd /V:ON /E:ON /D /C (%s)', $bin); // @codeCoverageIgnore } $process = proc_open($bin, self::$descriptorspec, $pipes, null, $env); @@ -139,17 +125,8 @@ final public function isRunning(): bool return $status !== false ? $status['running'] : false; } - final public function isStarted(): bool - { - return $this->proc !== null && $this->pipes !== []; - } - - final public function isCrashed(): bool + final public function checkNotCrashed(): void { - if (! $this->isStarted()) { - return false; - } - assert($this->proc !== null); $status = proc_get_status($this->proc); assert($status !== false); @@ -157,25 +134,19 @@ final public function isCrashed(): bool $this->updateStateFromAvailableOutput(); $this->setExitCode($status['running'], $status['exitcode']); - if ($this->exitCode === null) { - return false; + if ($this->exitCode === null || $this->exitCode === 0) { + return; } - return $this->exitCode !== 0; - } - - final public function checkNotCrashed(): void - { - if ($this->isCrashed()) { - throw new RuntimeException($this->getCrashReport()); - } + throw new WorkerCrashedException($this->getCrashReport()); } final public function getCrashReport(): string { - $lastCommand = count($this->commands) !== 0 ? ' Last executed command: ' . end($this->commands) : ''; + $lastCommand = count($this->commands) !== 0 ? 'Last executed command: ' . end($this->commands) : ''; - return 'This worker has crashed.' . $lastCommand . PHP_EOL + return 'This worker has crashed.' . PHP_EOL + . $lastCommand . PHP_EOL . 'Output:' . PHP_EOL . '----------------------' . PHP_EOL . $this->alreadyReadOutput . PHP_EOL @@ -183,21 +154,9 @@ final public function getCrashReport(): string . $this->readAllStderr(); } - final public function stop(): void - { - $this->doStop(); - fclose($this->pipes[0]); - } - - abstract protected function doStop(): void; - final protected function setExitCode(bool $running, int $exitcode): void { - if ($running) { - return; - } - - if ($this->exitCode !== null) { + if ($running || $this->exitCode !== null) { return; } diff --git a/src/Runners/PHPUnit/Worker/RunnerWorker.php b/src/Runners/PHPUnit/Worker/RunnerWorker.php index da10ad8a..49a80497 100644 --- a/src/Runners/PHPUnit/Worker/RunnerWorker.php +++ b/src/Runners/PHPUnit/Worker/RunnerWorker.php @@ -11,6 +11,7 @@ use function array_merge; use function assert; +use function sprintf; use function strlen; use const DIRECTORY_SEPARATOR; @@ -32,16 +33,6 @@ public function getExecutableTest(): ExecutableTest return $this->executableTest; } - /** - * Return the test process' stderr contents. - */ - public function getStderr(): string - { - assert($this->process !== null); - - return $this->process->getErrorOutput(); - } - /** * Stop the process and return it's * exit code. @@ -76,12 +67,10 @@ public function getExitCode(): ?int /** * Executes the test by creating a separate process. * - * @param array $options - * @param array $environmentVariables - * @param string[]|null $passthru - * @param string[]|null $passthruPhp - * - * @return $this + * @param array $options + * @param array $environmentVariables + * @param string[]|null $passthru + * @param string[]|null $passthruPhp */ public function run( string $binary, @@ -89,7 +78,7 @@ public function run( array $environmentVariables = [], ?array $passthru = null, ?array $passthruPhp = null - ) { + ): void { $process = $this->getProcess($binary, $options, $environmentVariables, $passthru, $passthruPhp); $cmd = $process->getCommandLine(); @@ -98,18 +87,16 @@ public function run( $this->process = $process; $this->process->start(); - - return $this; } /** * Build the full executable as we would do on the command line, e.g. * php -d zend_extension=xdebug.so vendor/bin/phpunit --teststuite suite1 --prepend xdebug-filter.php. * - * @param array $options - * @param array $environmentVariables - * @param string[]|null $passthru - * @param string[]|null $passthruPhp + * @param array $options + * @param array $environmentVariables + * @param string[]|null $passthru + * @param string[]|null $passthruPhp */ private function getProcess( string $binary, @@ -127,21 +114,9 @@ private function getProcess( $args = array_merge($args, $this->executableTest->commandArguments($binary, $options, $passthru)); - $environmentVariables['PARATEST'] = 1; - return new Process($args, null, $environmentVariables); } - /** - * Get process stdout content. - */ - public function getStdout(): string - { - assert($this->process !== null); - - return $this->process->getOutput(); - } - /** * Assert that command line length is valid. * @@ -155,12 +130,15 @@ public function getStdout(): string */ private function assertValidCommandLineLength(string $cmd): void { - if (DIRECTORY_SEPARATOR === '\\') { // windows + if (DIRECTORY_SEPARATOR === '\\') { + // @codeCoverageIgnoreStart // symfony's process wrapper $cmd = 'cmd /V:ON /E:ON /C "(' . $cmd . ')'; if (strlen($cmd) > 32767) { throw new RuntimeException('Command line is too long, try to decrease max batch size'); } + + // @codeCoverageIgnoreEnd } /* @@ -170,4 +148,27 @@ private function assertValidCommandLineLength(string $cmd): void * - osx/linux: getconf ARG_MAX */ } + + public function getCrashReport(): string + { + assert($this->process !== null); + + $error = sprintf( + 'The command "%s" failed.' . "\n\nExit Code: %s(%s)\n\nWorking directory: %s", + $this->process->getCommandLine(), + (string) $this->process->getExitCode(), + (string) $this->process->getExitCodeText(), + (string) $this->process->getWorkingDirectory() + ); + + if (! $this->process->isOutputDisabled()) { + $error .= sprintf( + "\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", + $this->process->getOutput(), + $this->process->getErrorOutput() + ); + } + + return $error; + } } diff --git a/src/Runners/PHPUnit/Worker/SqliteWorker.php b/src/Runners/PHPUnit/Worker/SqliteWorker.php index 6bba71a3..faee26d9 100644 --- a/src/Runners/PHPUnit/Worker/SqliteWorker.php +++ b/src/Runners/PHPUnit/Worker/SqliteWorker.php @@ -24,8 +24,4 @@ protected function configureParameters(array &$parameters): void { $parameters[] = $this->dbFileName; } - - protected function doStop(): void - { - } } diff --git a/src/Runners/PHPUnit/Worker/WrapperWorker.php b/src/Runners/PHPUnit/Worker/WrapperWorker.php index 1f19a584..a38239c9 100644 --- a/src/Runners/PHPUnit/Worker/WrapperWorker.php +++ b/src/Runners/PHPUnit/Worker/WrapperWorker.php @@ -9,7 +9,9 @@ use ParaTest\Runners\PHPUnit\ResultPrinter; use RuntimeException; +use function array_map; use function assert; +use function fclose; use function fgets; use function fwrite; use function implode; @@ -20,6 +22,9 @@ final class WrapperWorker extends BaseWorker { + public const COMMAND_EXIT = "EXIT\n"; + public const COMMAND_FINISHED = "FINISHED\n"; + /** @var ExecutableTest|null */ private $currentlyExecuting; @@ -43,26 +48,22 @@ public function stdout() */ public function execute(array $testCmdArguments): void { - $this->checkStarted(); - $this->commands[] = implode(' ', $testCmdArguments); + $this->commands[] = implode(' ', array_map('escapeshellarg', $testCmdArguments)); fwrite($this->pipes[0], serialize($testCmdArguments) . "\n"); ++$this->inExecution; } /** - * @param array $phpunitOptions + * @param array $phpunitOptions */ public function assign(ExecutableTest $test, string $phpunit, array $phpunitOptions, Options $options): void { - if ($this->currentlyExecuting !== null) { - throw new RuntimeException('Worker already has a test assigned - did you forget to call reset()?'); - } - + assert($this->currentlyExecuting === null); $this->currentlyExecuting = $test; $commandArguments = $test->commandArguments($phpunit, $phpunitOptions, $options->passthru()); $command = implode(' ', $commandArguments); if ($options->verbose() > 0) { - $this->output->write("\nExecuting test via: $command\n"); + $this->output->write("\nExecuting test via: {$command}\n"); } $test->setLastCommand($command); @@ -83,19 +84,17 @@ public function reset(): void $this->currentlyExecuting = null; } - private function checkStarted(): void - { - if (! $this->isStarted()) { - throw new RuntimeException('You have to start the Worker first!'); - } - } - - protected function doStop(): void + public function stop(): void { - fwrite($this->pipes[0], "EXIT\n"); + fwrite($this->pipes[0], self::COMMAND_EXIT); + fclose($this->pipes[0]); } /** + * @internal + * + * @codeCoverageIgnore + * * This is an utility function for tests. * Refactor or write it only in the test case. */ @@ -108,7 +107,7 @@ public function waitForFinishedJob(): void $tellsUsItHasFinished = false; stream_set_blocking($this->pipes[1], true); while ($line = fgets($this->pipes[1])) { - if (strstr($line, "FINISHED\n") !== false) { + if (strstr($line, self::COMMAND_FINISHED) !== false) { $tellsUsItHasFinished = true; --$this->inExecution; break; @@ -123,6 +122,8 @@ public function waitForFinishedJob(): void /** * @internal * + * @codeCoverageIgnore + * * This function consumes a lot of CPU while waiting for * the worker to finish. Use it only in testing paratest * itself. diff --git a/src/Runners/PHPUnit/WorkerCrashedException.php b/src/Runners/PHPUnit/WorkerCrashedException.php new file mode 100644 index 00000000..bef7cb46 --- /dev/null +++ b/src/Runners/PHPUnit/WorkerCrashedException.php @@ -0,0 +1,11 @@ +initialize(); $this->startWorkers(); $this->assignAllPendingTests(); $this->sendStopMessages(); $this->waitForAllToFinish(); - $this->complete(); + $this->setExitCode(); } private function startWorkers(): void @@ -53,15 +57,8 @@ private function startWorkers(): void assert($wrapper !== false); for ($i = 1; $i <= $this->options->processes(); ++$i) { $worker = new WrapperWorker($this->output); - if ($this->options->noTestTokens()) { - $token = null; - $uniqueToken = null; - } else { - $token = $i; - $uniqueToken = uniqid(); - } - $worker->start($wrapper, $token, $uniqueToken, [], $this->options); + $worker->start($wrapper, $this->options, $i); $this->streams[] = $worker->stdout(); $this->workers[] = $worker; } @@ -71,31 +68,21 @@ private function assignAllPendingTests(): void { $phpunit = $this->options->phpunit(); $phpunitOptions = $this->options->filtered(); - // $phpunitOptions['no-globals-backup'] = null; // removed in phpunit 6.0 + while (count($this->pending)) { $this->waitForStreamsToChange($this->streams); foreach ($this->progressedWorkers() as $key => $worker) { if (! $worker->isFree()) { - continue; + continue; // @codeCoverageIgnore } - try { - $this->flushWorker($worker); - $pending = array_shift($this->pending); - if ($pending !== null) { - $worker->assign($pending, $phpunit, $phpunitOptions, $this->options); - } - } catch (Throwable $e) { - if ($this->options->verbose() > 0) { - $worker->stop(); - $this->output->writeln( - "Error while assigning pending tests for worker $key: {$e->getMessage()}" - ); - $this->output->write($worker->getCrashReport()); - } - - throw $e; + $this->flushWorker($worker); + $pending = array_shift($this->pending); + if ($pending === null) { + continue; } + + $worker->assign($pending, $phpunit, $phpunitOptions, $this->options); } } } @@ -105,18 +92,14 @@ private function assignAllPendingTests(): void * * @param resource[] $modified */ - private function waitForStreamsToChange(array $modified): int + private function waitForStreamsToChange(array $modified): void { $write = []; $except = []; $result = stream_select($modified, $write, $except, 1); - if ($result === false) { - throw new RuntimeException('stream_select() returned an error while waiting for all workers to finish.'); - } + assert($result !== false); $this->modified = $modified; - - return $result; } /** @@ -151,8 +134,13 @@ private function flushWorker(WrapperWorker $worker): void if ($this->hasCoverage()) { $coverageMerger = $this->getCoverage(); assert($coverageMerger !== null); - - $coverageMerger->addCoverageFromFile($worker->getCoverageFileName()); + if (($coverageFileName = $worker->getCoverageFileName()) !== null) { + try { + $coverageMerger->addCoverageFromFile($coverageFileName); + } catch (EmptyCoverageFileException $emptyCoverageFileException) { + throw new WorkerCrashedException($worker->getCrashReport(), 0, $emptyCoverageFileException); + } + } } $worker->printFeedback($this->printer); @@ -171,23 +159,14 @@ private function waitForAllToFinish(): void $toStop = $this->workers; while (count($toStop) > 0) { $toCheck = $this->streamsOf($toStop); - $new = $this->waitForStreamsToChange($toCheck); + $this->waitForStreamsToChange($toCheck); foreach ($this->progressedWorkers() as $index => $worker) { - try { - if (! $worker->isRunning()) { - $this->flushWorker($worker); - unset($toStop[$index]); - } - } catch (Throwable $e) { - if ($this->options->verbose() > 0) { - $worker->stop(); - unset($toStop[$index]); - $this->output->writeln("Error while waiting to finish for worker $index: {$e->getMessage()}"); - $this->output->write($worker->getCrashReport()); - } - - throw $e; + if ($worker->isRunning()) { + continue; } + + $this->flushWorker($worker); + unset($toStop[$index]); } } } diff --git a/test/Functional/Coverage/CoverageMergerTest.php b/test/Functional/Coverage/CoverageMergerTest.php deleted file mode 100644 index 44ff87f1..00000000 --- a/test/Functional/Coverage/CoverageMergerTest.php +++ /dev/null @@ -1,172 +0,0 @@ -targetDir = str_replace('.', '_', sys_get_temp_dir() . DS . uniqid('paratest-', true)); - $this->removeDirectory($this->targetDir); - mkdir($this->targetDir); - } - - protected function tearDown(): void - { - $this->removeDirectory($this->targetDir); - - parent::tearDown(); - } - - /** - * @param string[] $coverageFiles - * - * @dataProvider getCoverageFileProvider - */ - public function testCoverageFromFileIsDeletedAfterAdd(array $coverageFiles): void - { - $filename = $this->copyCoverageFile($coverageFiles[0], $this->targetDir); - - $coverageMerger = new CoverageMerger(); - $coverageMerger->addCoverageFromFile($filename); - - static::assertFileDoesNotExist($filename); - } - - /** - * @param string[] $coverageFiles - * @param class-string $expectedClass - * - * @dataProvider getCoverageFileProvider - */ - public function testCodeCoverageObjectIsCreatedFromCoverageFile(array $coverageFiles, string $expectedClass): void - { - $filename = $this->copyCoverageFile($coverageFiles[0], $this->targetDir); - - $coverageMerger = new CoverageMerger(); - $coverageMerger->addCoverageFromFile($filename); - - $coverage = $this->getCoverage($coverageMerger); - - static::assertInstanceOf($expectedClass, $coverage); - static::assertArrayHasKey( - 'ParaTest\\Runners\\PHPUnit\\RunnerTest::testConstructor', - $coverage->getTests(), - 'Code coverage was not added from file' - ); - } - - /** - * @param string[] $coverageFiles - * - * @dataProvider getCoverageFileProvider - */ - public function testCoverageIsMergedOnSecondAddCoverageFromFile(array $coverageFiles): void - { - $filename1 = $this->copyCoverageFile($coverageFiles[0], $this->targetDir); - $filename2 = $this->copyCoverageFile($coverageFiles[1], $this->targetDir); - - $coverageMerger = new CoverageMerger(); - $coverageMerger->addCoverageFromFile($filename1); - - $coverage = $this->getCoverage($coverageMerger); - - static::assertArrayHasKey( - 'ParaTest\\Runners\\PHPUnit\\RunnerTest::testConstructor', - $coverage->getTests(), - 'Code coverage was not added from first file' - ); - static::assertArrayNotHasKey( - 'ParaTest\\Runners\\PHPUnit\\ResultPrinterTest::testConstructor', - $coverage->getTests() - ); - - $coverageMerger->addCoverageFromFile($filename2); - - static::assertArrayHasKey( - 'ParaTest\\Runners\\PHPUnit\\RunnerTest::testConstructor', - $coverage->getTests(), - 'Code coverage from first file was removed' - ); - static::assertArrayHasKey( - 'ParaTest\\Runners\\PHPUnit\\ResultPrinterTest::testConstructor', - $coverage->getTests(), - 'Code coverage was not added from second file' - ); - } - - public function testCoverageFileIsEmpty(): void - { - $this->expectException(RuntimeException::class); - $regex = '/Coverage file .*? is empty. This means a PHPUnit process has crashed./'; - $this->expectExceptionMessageMatches($regex); - $filename = $this->copyCoverageFile('coverage-tests' . DS . 'empty_test.cov', $this->targetDir); - - $coverageMerger = new CoverageMerger(); - $coverageMerger->addCoverageFromFile($filename); - } - - public function testCoverageFileIsNull(): void - { - $coverageMerger = new CoverageMerger(); - $coverageMerger->addCoverageFromFile(null); - - static::assertNull($coverageMerger->getCodeCoverageObject()); - } - - public function testCoverageFileDoesNotExist(): void - { - $coverageMerger = new CoverageMerger(); - $coverageMerger->addCoverageFromFile('no-such-file.cov'); - - static::assertNull($coverageMerger->getCodeCoverageObject()); - } - - /** - * @return array> - */ - public static function getCoverageFileProvider(): array - { - $version = 'CodeCoverage >4.0'; - $filenames = [ - 'coverage-tests/runner_test.cov', - 'coverage-tests/result_printer_test.cov', - ]; - $coverageClass = CodeCoverage::class; - - return [ - $version => [ - 'filenames' => $filenames, - 'expected coverage class' => $coverageClass, - ], - ]; - } - - private function getCoverage(CoverageMerger $coverageMerger): CodeCoverage - { - return $this->getObjectValue($coverageMerger, 'coverage'); - } -} diff --git a/test/Functional/Coverage/CoverageReporterTest.php b/test/Functional/Coverage/CoverageReporterTest.php deleted file mode 100644 index 92770fcc..00000000 --- a/test/Functional/Coverage/CoverageReporterTest.php +++ /dev/null @@ -1,233 +0,0 @@ -targetDir = str_replace('.', '_', sys_get_temp_dir() . DS . uniqid('paratest-', true)); - $this->removeDirectory($this->targetDir); - mkdir($this->targetDir); - } - - protected function tearDown(): void - { - $this->removeDirectory($this->targetDir); - - parent::tearDown(); - } - - /** - * @param string[] $coverageFiles - * @param class-string $expectedReportClass - * - * @dataProvider getReporterProvider - */ - public function testGetReporter(array $coverageFiles, string $expectedReportClass): void - { - $filename1 = $this->copyCoverageFile($coverageFiles[0], $this->targetDir); - $filename2 = $this->copyCoverageFile($coverageFiles[1], $this->targetDir); - - $coverageMerger = new CoverageMerger(); - $coverageMerger->addCoverageFromFile($filename1); - $coverageMerger->addCoverageFromFile($filename2); - - $reporter = $coverageMerger->getReporter(); - - static::assertInstanceOf($expectedReportClass, $reporter); - } - - /** - * @param string[] $coverageFiles - * - * @dataProvider getReporterProvider - */ - public function testGeneratePhp(array $coverageFiles): void - { - $filename1 = $this->copyCoverageFile($coverageFiles[0], $this->targetDir); - $filename2 = $this->copyCoverageFile($coverageFiles[1], $this->targetDir); - - $coverageMerger = new CoverageMerger(); - $coverageMerger->addCoverageFromFile($filename1); - $coverageMerger->addCoverageFromFile($filename2); - - $target = $this->targetDir . DS . 'coverage.php'; - - static::assertFileDoesNotExist($target); - - $coverageMerger->getReporter()->php($target); - - static::assertFileExists($target); - } - - /** - * @param string[] $coverageFiles - * - * @dataProvider getReporterProvider - */ - public function testGenerateClover(array $coverageFiles): void - { - $filename1 = $this->copyCoverageFile($coverageFiles[0], $this->targetDir); - $filename2 = $this->copyCoverageFile($coverageFiles[1], $this->targetDir); - - $coverageMerger = new CoverageMerger(); - $coverageMerger->addCoverageFromFile($filename1); - $coverageMerger->addCoverageFromFile($filename2); - - $target = $this->targetDir . DS . 'coverage.xml'; - - static::assertFileDoesNotExist($target); - - $coverageMerger->getReporter()->clover($target); - - static::assertFileExists($target); - - $reportXml = (new Xml\Loader())->loadFile($target); - static::assertTrue($reportXml->hasChildNodes(), 'Incorrect clover report xml was generated'); - } - - /** - * @param string[] $coverageFiles - * - * @dataProvider getReporterProvider - */ - public function testGenerateCrap4J(array $coverageFiles): void - { - $filename1 = $this->copyCoverageFile($coverageFiles[0], $this->targetDir); - $filename2 = $this->copyCoverageFile($coverageFiles[1], $this->targetDir); - - $coverageMerger = new CoverageMerger(); - $coverageMerger->addCoverageFromFile($filename1); - $coverageMerger->addCoverageFromFile($filename2); - - $target = $this->targetDir . DS . 'coverage.xml'; - - static::assertFileDoesNotExist($target); - - $coverageMerger->getReporter()->crap4j($target); - - static::assertFileExists($target); - - $reportXml = (new Xml\Loader())->loadFile($target); - static::assertTrue($reportXml->hasChildNodes(), 'Incorrect crap4j report xml was generated'); - $documentElement = $reportXml->documentElement; - static::assertNotNull($documentElement); - static::assertEquals('crap_result', $documentElement->tagName); - } - - /** - * @param string[] $coverageFiles - * - * @dataProvider getReporterProvider - */ - public function testGenerateHtml(array $coverageFiles): void - { - $filename1 = $this->copyCoverageFile($coverageFiles[0], $this->targetDir); - $filename2 = $this->copyCoverageFile($coverageFiles[1], $this->targetDir); - - $coverageMerger = new CoverageMerger(); - $coverageMerger->addCoverageFromFile($filename1); - $coverageMerger->addCoverageFromFile($filename2); - - $target = $this->targetDir . DS . 'coverage'; - - static::assertFileDoesNotExist($target); - - $coverageMerger->getReporter()->html($target); - - static::assertFileExists($target); - static::assertFileExists($target . DS . 'index.html', 'Index html file was not generated'); - } - - /** - * @param string[] $coverageFiles - * - * @dataProvider getReporterProvider - */ - public function testGenerateText(array $coverageFiles): void - { - $filename1 = $this->copyCoverageFile($coverageFiles[0], $this->targetDir); - $filename2 = $this->copyCoverageFile($coverageFiles[1], $this->targetDir); - - $coverageMerger = new CoverageMerger(); - $coverageMerger->addCoverageFromFile($filename1); - $coverageMerger->addCoverageFromFile($filename2); - - $output = $coverageMerger->getReporter()->text(); - - static::assertStringContainsString('Code Coverage Report:', $output); - } - - /** - * @param string[] $coverageFiles - * - * @dataProvider getReporterProvider - */ - public function testGenerateXml(array $coverageFiles): void - { - $filename1 = $this->copyCoverageFile($coverageFiles[0], $this->targetDir); - $filename2 = $this->copyCoverageFile($coverageFiles[1], $this->targetDir); - - $coverageMerger = new CoverageMerger(); - $coverageMerger->addCoverageFromFile($filename1); - $coverageMerger->addCoverageFromFile($filename2); - - $target = $this->targetDir . DS . 'coverage.xml'; - - static::assertFileDoesNotExist($target); - - $coverageMerger->getReporter()->xml($target); - - static::assertFileExists($target); - static::assertFileExists($target . DS . 'index.xml', 'Index xml file was not generated'); - - $reportXml = (new Xml\Loader())->loadFile($target . DS . 'index.xml'); - static::assertTrue($reportXml->hasChildNodes(), 'Incorrect xml was generated'); - } - - /** - * @return array> - */ - public static function getReporterProvider(): array - { - $version = 'CodeCoverage >4.0'; - $windowsExt = defined('PHP_WINDOWS_VERSION_BUILD') ? '-windows' : ''; - $filenames = [ - 'coverage-tests' . DS . 'runner_test' . $windowsExt . '.cov', - 'coverage-tests' . DS . 'result_printer_test' . $windowsExt . '.cov', - ]; - $reporterClass = CoverageReporter::class; - - return [ - $version => [ - 'filenames' => $filenames, - 'expected reporter class' => $reporterClass, - ], - ]; - } -} diff --git a/test/Functional/DataProviderTest.php b/test/Functional/DataProviderTest.php index 1deddbe2..d7f9e5df 100644 --- a/test/Functional/DataProviderTest.php +++ b/test/Functional/DataProviderTest.php @@ -4,6 +4,9 @@ namespace ParaTest\Tests\Functional; +/** + * @coversNothing + */ final class DataProviderTest extends FunctionalTestBase { /** @var ParaTestInvoker */ diff --git a/test/Functional/FunctionalTestBase.php b/test/Functional/FunctionalTestBase.php index 995d286d..2d1affd8 100644 --- a/test/Functional/FunctionalTestBase.php +++ b/test/Functional/FunctionalTestBase.php @@ -17,7 +17,7 @@ final protected function fixture(string $fixture): string { $fixture = FIXTURES . DS . $fixture; if (! file_exists($fixture)) { - throw new InvalidArgumentException("Fixture $fixture not found"); + throw new InvalidArgumentException("Fixture {$fixture} not found"); } return $fixture; @@ -66,6 +66,6 @@ final protected function guardSqliteExtensionLoaded(): void return; } - static::markTestSkipped("Skipping test: Extension '$sqliteExtension' not found."); + static::markTestSkipped("Skipping test: Extension '{$sqliteExtension}' not found."); } } diff --git a/test/Functional/GroupTest.php b/test/Functional/GroupTest.php index df6f7d95..05b4f74e 100644 --- a/test/Functional/GroupTest.php +++ b/test/Functional/GroupTest.php @@ -4,6 +4,9 @@ namespace ParaTest\Tests\Functional; +/** + * @coversNothing + */ final class GroupTest extends FunctionalTestBase { /** @var ParaTestInvoker */ diff --git a/test/Functional/OutputTest.php b/test/Functional/OutputTest.php index 5a15d0bf..f1e65871 100644 --- a/test/Functional/OutputTest.php +++ b/test/Functional/OutputTest.php @@ -6,6 +6,9 @@ use function getcwd; +/** + * @coversNothing + */ final class OutputTest extends FunctionalTestBase { /** @var ParaTestInvoker */ diff --git a/test/Functional/PHPUnitOtherWarningsTest.php b/test/Functional/PHPUnitOtherWarningsTest.php index c61e4c84..a8972c2e 100644 --- a/test/Functional/PHPUnitOtherWarningsTest.php +++ b/test/Functional/PHPUnitOtherWarningsTest.php @@ -10,6 +10,8 @@ * PHPUnit deprecated method calls, mocking non-existent methods and some other cases produce warnings that are output * slightly different. Now the paratest doesn't parse the output directly but relies on the JUnit XML logs. * This test checks whether the parates recognizes those warnings. + * + * @coversNothing */ final class PHPUnitOtherWarningsTest extends FunctionalTestBase { diff --git a/test/Functional/PHPUnitTest.php b/test/Functional/PHPUnitTest.php index 34276db3..fcb1f15f 100644 --- a/test/Functional/PHPUnitTest.php +++ b/test/Functional/PHPUnitTest.php @@ -4,9 +4,9 @@ namespace ParaTest\Tests\Functional; -use ParaTest\Runners\PHPUnit\EmptyRunnerStub; use ParaTest\Runners\PHPUnit\SqliteRunner; use ParaTest\Runners\PHPUnit\WrapperRunner; +use ParaTest\Tests\Unit\Runners\PHPUnit\EmptyRunnerStub; use ParseError; use function array_merge; @@ -19,6 +19,9 @@ use function sprintf; use function unlink; +/** + * @coversNothing + */ final class PHPUnitTest extends FunctionalTestBase { public function testWithJustBootstrap(): void diff --git a/test/Functional/PHPUnitWarningsTest.php b/test/Functional/PHPUnitWarningsTest.php index cd4748bf..d3fc9c3b 100644 --- a/test/Functional/PHPUnitWarningsTest.php +++ b/test/Functional/PHPUnitWarningsTest.php @@ -6,6 +6,8 @@ /** * Specifically tests warnings in PHPUnit. + * + * @coversNothing */ final class PHPUnitWarningsTest extends FunctionalTestBase { diff --git a/test/Functional/Runners/PHPUnit/RunnerIntegrationTest.php b/test/Functional/Runners/PHPUnit/RunnerIntegrationTest.php deleted file mode 100644 index 7fe9791a..00000000 --- a/test/Functional/Runners/PHPUnit/RunnerIntegrationTest.php +++ /dev/null @@ -1,136 +0,0 @@ - */ - private $bareOptions; - /** @var Options */ - private $options; - - protected function setUp(): void - { - static::skipIfCodeCoverageNotEnabled(); - - $testcoverageFiles = sys_get_temp_dir() . DS . 'coverage-runner-integration*'; - $glob = glob($testcoverageFiles); - assert($glob !== false); - foreach ($glob as $file) { - unlink($file); - } - - $this->bareOptions = [ - '--path' => FIXTURES . DS . 'failing-tests', - '--phpunit' => PHPUNIT, - '--coverage-clover' => sys_get_temp_dir() . DS . 'coverage-runner-integration.clover', - '--coverage-crap4j' => sys_get_temp_dir() . DS . 'coverage-runner-integration.crap4j', - '--coverage-php' => sys_get_temp_dir() . DS . 'coverage-runner-integration.php', - '--bootstrap' => BOOTSTRAP, - '--whitelist' => FIXTURES . DS . 'failing-tests', - ]; - $this->options = $this->createOptionsFromArgv($this->bareOptions); - $this->output = new BufferedOutput(); - $this->runner = new Runner($this->options, $this->output); - } - - /** - * @return string[] - */ - private function globTempDir(string $pattern): array - { - $glob = glob(sys_get_temp_dir() . DS . $pattern); - assert($glob !== false); - - return $glob; - } - - public function testGeneratesCoverageTypes(): void - { - static::assertFileDoesNotExist($this->bareOptions['--coverage-clover']); - static::assertFileDoesNotExist($this->bareOptions['--coverage-crap4j']); - static::assertFileDoesNotExist($this->bareOptions['--coverage-php']); - - $this->runner->run(); - - static::assertFileExists($this->bareOptions['--coverage-clover']); - static::assertFileExists($this->bareOptions['--coverage-crap4j']); - static::assertFileExists($this->bareOptions['--coverage-php']); - } - - public function testRunningTestsShouldLeaveNoTempFiles(): void - { - $countBefore = count($this->globTempDir('PT_*')); - $countCoverageBefore = count($this->globTempDir('CV_*')); - - $this->runner->run(); - - $countAfter = count($this->globTempDir('PT_*')); - $countCoverageAfter = count($this->globTempDir('CV_*')); - - static::assertEquals( - $countAfter, - $countBefore, - "Test Runner failed to clean up the 'PT_*' file in " . sys_get_temp_dir() - ); - static::assertEquals( - $countCoverageAfter, - $countCoverageBefore, - "Test Runner failed to clean up the 'CV_*' file in " . sys_get_temp_dir() - ); - } - - public function testLogJUnitCreatesXmlFile(): void - { - $outputPath = FIXTURES . DS . 'logs' . DS . 'test-output.xml'; - - $this->bareOptions['--log-junit'] = $outputPath; - - $runner = new Runner($this->createOptionsFromArgv($this->bareOptions), $this->output); - - $runner->run(); - - static::assertFileExists($outputPath); - $this->assertJunitXmlIsCorrect($outputPath); - unlink($outputPath); - } - - public function assertJunitXmlIsCorrect(string $path): void - { - $doc = simplexml_load_file($path); - assert($doc !== false); - $suites = $doc->xpath('//testsuite'); - $cases = $doc->xpath('//testcase'); - $failures = $doc->xpath('//failure'); - $errors = $doc->xpath('//error'); - - // these numbers represent the tests in fixtures/failing-tests - // so will need to be updated when tests are added or removed - static::assertNotFalse($suites); - static::assertCount(6, $suites); - static::assertNotFalse($cases); - static::assertCount(16, $cases); - static::assertNotFalse($failures); - static::assertCount(6, $failures); - static::assertNotFalse($errors); - static::assertCount(1, $errors); - } -} diff --git a/test/Functional/Runners/PHPUnit/WorkerTest.php b/test/Functional/Runners/PHPUnit/WorkerTest.php index 8215be9b..0b8a3449 100644 --- a/test/Functional/Runners/PHPUnit/WorkerTest.php +++ b/test/Functional/Runners/PHPUnit/WorkerTest.php @@ -4,57 +4,37 @@ namespace ParaTest\Tests\Functional\Runners\PHPUnit; +use ParaTest\Runners\PHPUnit\Options; use ParaTest\Runners\PHPUnit\Worker\WrapperWorker; +use ParaTest\Runners\PHPUnit\WorkerCrashedException; use ParaTest\Tests\TestBase; -use ReflectionProperty; use SimpleXMLElement; use Symfony\Component\Console\Output\BufferedOutput; use function count; -use function file_exists; use function file_get_contents; -use function get_class; -use function proc_get_status; -use function proc_open; -use function sys_get_temp_dir; -use function unlink; +use function uniqid; +/** + * @covers \ParaTest\Runners\PHPUnit\Worker\BaseWorker + */ final class WorkerTest extends TestBase { - /** @var string[][] */ - protected static $descriptorspec = [ - 0 => ['pipe', 'r'], - 1 => ['pipe', 'w'], - 2 => ['pipe', 'w'], - ]; /** @var string */ - protected $bootstrap; + private $bootstrap; /** @var string */ private $phpunitWrapper; /** @var BufferedOutput */ private $output; + /** @var Options */ + private $options; - public function setUp(): void + public function setUpTest(): void { - parent::setUp(); $this->bootstrap = PARATEST_ROOT . DS . 'test' . DS . 'bootstrap.php'; $this->phpunitWrapper = PARATEST_ROOT . DS . 'bin' . DS . 'phpunit-wrapper.php'; $this->output = new BufferedOutput(); - } - - public function tearDown(): void - { - $this->deleteIfExists(sys_get_temp_dir() . DS . 'test.xml'); - $this->deleteIfExists(sys_get_temp_dir() . DS . 'test2.xml'); - } - - private function deleteIfExists(string $file): void - { - if (! file_exists($file)) { - return; - } - - unlink($file); + $this->options = $this->createOptionsFromArgv([]); } /** @@ -62,10 +42,10 @@ private function deleteIfExists(string $file): void */ public function testReadsAPHPUnitCommandFromStdInAndExecutesItItsOwnProcess(): void { - $testLog = sys_get_temp_dir() . DS . 'test.xml'; + $testLog = TMP_DIR . DS . 'test.xml'; $testCmd = $this->getCommand('passing-tests' . DS . 'TestOfUnits.php', $testLog); $worker = new WrapperWorker($this->output); - $worker->start($this->phpunitWrapper); + $worker->start($this->phpunitWrapper, $this->options, 1); $worker->execute($testCmd); $worker->stop(); @@ -79,10 +59,10 @@ public function testReadsAPHPUnitCommandFromStdInAndExecutesItItsOwnProcess(): v */ public function testKnowsWhenAJobIsFinished(): void { - $testLog = sys_get_temp_dir() . DS . 'test.xml'; + $testLog = TMP_DIR . DS . 'test.xml'; $testCmd = $this->getCommand('passing-tests' . DS . 'TestOfUnits.php', $testLog); $worker = new WrapperWorker($this->output); - $worker->start($this->phpunitWrapper); + $worker->start($this->phpunitWrapper, $this->options, 1); $worker->execute($testCmd); $worker->waitForFinishedJob(); @@ -94,10 +74,10 @@ public function testKnowsWhenAJobIsFinished(): void */ public function testTellsWhenItsFree(): void { - $testLog = sys_get_temp_dir() . DS . 'test.xml'; + $testLog = TMP_DIR . DS . 'test.xml'; $testCmd = $this->getCommand('passing-tests' . DS . 'TestOfUnits.php', $testLog); $worker = new WrapperWorker($this->output); - $worker->start($this->phpunitWrapper); + $worker->start($this->phpunitWrapper, $this->options, 1); static::assertTrue($worker->isFree()); $worker->execute($testCmd); @@ -115,7 +95,7 @@ public function testTellsWhenItsStopped(): void $worker = new WrapperWorker($this->output); static::assertFalse($worker->isRunning()); - $worker->start($this->phpunitWrapper); + $worker->start($this->phpunitWrapper, $this->options, 1); static::assertTrue($worker->isRunning()); $worker->stop(); @@ -130,56 +110,25 @@ public function testProcessIsMarkedAsCrashedWhenItFinishesWithNonZeroExitCode(): { // fake state: process has already exited (with non-zero exit code) but worker did not yet notice $worker = new WrapperWorker($this->output); - $this->setPerReflection($worker, 'proc', $this->createSomeClosedProcess()); - $this->setPerReflection($worker, 'pipes', [0 => true]); - static::assertTrue($worker->isCrashed()); - } - - /** - * @return resource - */ - private function createSomeClosedProcess() - { - $descriptorspec = [ - 0 => ['pipe', 'r'], - 1 => ['pipe', 'w'], - 2 => ['pipe', 'w'], - ]; - - $proc = proc_open('thisCommandHasAnExitcodeNotEqualZero', $descriptorspec, $pipes, '/tmp'); - static::assertIsResource($proc); - $running = true; - while ($running) { - $status = proc_get_status($proc); - static::assertNotFalse($status); - $running = $status['running']; - } + $worker->start(uniqid('thisCommandHasAnExitcodeNotEqualZero'), $this->createOptionsFromArgv([]), 1); + $worker->waitForStop(); - return $proc; - } + static::expectException(WorkerCrashedException::class); + static::expectExceptionMessageMatches('/thisCommandHasAnExitcodeNotEqualZero/'); - /** - * @param mixed $value - */ - private function setPerReflection(object $instance, string $property, $value): void - { - $reflectionProperty = new ReflectionProperty(get_class($instance), $property); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($instance, $value); + $worker->checkNotCrashed(); } public function testCanExecuteMultiplePHPUnitCommands(): void { - $bin = 'bin/phpunit-wrapper.php'; - $worker = new WrapperWorker($this->output); - $worker->start($this->phpunitWrapper); + $worker->start($this->phpunitWrapper, $this->options, 1); - $testLog = sys_get_temp_dir() . DS . 'test.xml'; + $testLog = TMP_DIR . DS . 'test.xml'; $testCmd = $this->getCommand('passing-tests' . DS . 'TestOfUnits.php', $testLog); $worker->execute($testCmd); - $testLog2 = sys_get_temp_dir() . DS . 'test2.xml'; + $testLog2 = TMP_DIR . DS . 'test2.xml'; $testCmd2 = $this->getCommand('failing-tests' . DS . 'UnitTestWithErrorTest.php', $testLog2); $worker->execute($testCmd2); diff --git a/test/Functional/SkippedOrIncompleteTest.php b/test/Functional/SkippedOrIncompleteTest.php index 09b7b7fd..b76434a0 100644 --- a/test/Functional/SkippedOrIncompleteTest.php +++ b/test/Functional/SkippedOrIncompleteTest.php @@ -10,6 +10,7 @@ /** * @todo SkippedOrIncompleteTest can't be used in default mode with group filter * (not implemented yet) so we have to split tests per file. + * @coversNothing */ final class SkippedOrIncompleteTest extends FunctionalTestBase { @@ -118,7 +119,7 @@ private function assertContainsNSkippedTests(int $n, string $output): void static::assertEquals( $n, $numberOfS, - "The test should have skipped $n tests, instead it skipped $numberOfS, $matches[1]" + "The test should have skipped {$n} tests, instead it skipped {$numberOfS}, {$matches[1]}" ); } } diff --git a/test/Functional/SqliteRunnerTest.php b/test/Functional/SqliteRunnerTest.php deleted file mode 100644 index adb8df7a..00000000 --- a/test/Functional/SqliteRunnerTest.php +++ /dev/null @@ -1,87 +0,0 @@ -guardSqliteExtensionLoaded(); - parent::setUp(); - } - - public function testResultsAreCorrect(): void - { - $generator = new TestGenerator(); - $generator->generate(self::TEST_CLASSES, self::TEST_METHODS_PER_CLASS); - - $proc = $this->invokeParatest($generator->path, ['--processes' => 3, '--runner' => SqliteRunner::class]); - - $expected = self::TEST_CLASSES * self::TEST_METHODS_PER_CLASS; - $this->assertTestsPassed($proc, (string) $expected, (string) $expected); - } - - public function testRunningFewerTestsThanTheWorkersIsPossible(): void - { - $generator = new TestGenerator(); - $generator->generate(1, 1); - - $proc = $this->invokeParatest($generator->path, ['--processes' => 2, '--runner' => SqliteRunner::class]); - - $this->assertTestsPassed($proc, '1', '1'); - } - - public function testExitCodes(): void - { - $options = ['--processes' => 1, '--runner' => SqliteRunner::class]; - $proc = $this->invokeParatest( - 'wrapper-runner-exit-code-tests/ErrorTest.php', - $options - ); - $output = $proc->getOutput(); - - static::assertStringContainsString('Tests: 1', $output); - static::assertStringContainsString('Failures: 0', $output); - static::assertStringContainsString('Errors: 1', $output); - static::assertEquals(2, $proc->getExitCode()); - - $proc = $this->invokeParatest( - 'wrapper-runner-exit-code-tests/FailureTest.php', - $options - ); - $output = $proc->getOutput(); - - static::assertStringContainsString('Tests: 1', $output); - static::assertStringContainsString('Failures: 1', $output); - static::assertStringContainsString('Errors: 0', $output); - static::assertEquals(1, $proc->getExitCode()); - - $proc = $this->invokeParatest( - 'wrapper-runner-exit-code-tests/SuccessTest.php', - $options - ); - $output = $proc->getOutput(); - - static::assertStringContainsString('OK (1 test, 1 assertion)', $output); - static::assertEquals(0, $proc->getExitCode()); - - $options['--processes'] = 3; - $proc = $this->invokeParatest( - 'wrapper-runner-exit-code-tests', - $options - ); - $output = $proc->getOutput(); - static::assertStringContainsString('Tests: 3', $output); - static::assertStringContainsString('Failures: 1', $output); - static::assertStringContainsString('Errors: 1', $output); - static::assertEquals(2, $proc->getExitCode()); // There is at least one error so the exit code must be 2 - } -} diff --git a/test/Functional/TestGenerator.php b/test/Functional/TestGenerator.php index 6cd37607..b5d9ca44 100644 --- a/test/Functional/TestGenerator.php +++ b/test/Functional/TestGenerator.php @@ -43,7 +43,7 @@ private function generateTestString(string $testName, int $methods = 1): string { $namespace = sprintf('Generated%s', basename($this->path)); $php = '<' - . "?php\n\nnamespace $namespace;\n\nclass $testName extends \\PHPUnit\\Framework\\TestCase\n{\n"; + . "?php\n\nnamespace {$namespace};\n\nclass {$testName} extends \\PHPUnit\\Framework\\TestCase\n{\n"; for ($i = 0; $i < $methods; ++$i) { $php .= "\tpublic function testMethod{$i}(): void{"; diff --git a/test/Functional/WrapperRunnerTest.php b/test/Functional/WrapperRunnerTest.php deleted file mode 100644 index 8163939f..00000000 --- a/test/Functional/WrapperRunnerTest.php +++ /dev/null @@ -1,116 +0,0 @@ -generate(self::TEST_CLASSES, self::TEST_METHODS_PER_CLASS); - - $proc = $this->invokeParatest($generator->path, ['--processes' => 3, '--runner' => WrapperRunner::class]); - - $expected = self::TEST_CLASSES * self::TEST_METHODS_PER_CLASS; - $this->assertTestsPassed($proc, (string) $expected, (string) $expected); - } - - public function testRunningFewerTestsThanTheWorkersIsPossible(): void - { - $generator = new TestGenerator(); - $generator->generate(1, 1); - - $proc = $this->invokeParatest($generator->path, ['--processes' => 2, '--runner' => WrapperRunner::class]); - - $this->assertTestsPassed($proc, '1', '1'); - } - - public function testExitCodes(): void - { - $options = ['--processes' => 1, '--runner' => WrapperRunner::class]; - $proc = $this->invokeParatest( - 'wrapper-runner-exit-code-tests/ErrorTest.php', - $options - ); - $output = $proc->getOutput(); - - static::assertStringContainsString('Tests: 1', $output); - static::assertStringContainsString('Failures: 0', $output); - static::assertStringContainsString('Errors: 1', $output); - static::assertEquals(2, $proc->getExitCode()); - - $proc = $this->invokeParatest( - 'wrapper-runner-exit-code-tests/FailureTest.php', - $options - ); - $output = $proc->getOutput(); - - static::assertStringContainsString('Tests: 1', $output); - static::assertStringContainsString('Failures: 1', $output); - static::assertStringContainsString('Errors: 0', $output); - static::assertEquals(1, $proc->getExitCode()); - - $proc = $this->invokeParatest( - 'wrapper-runner-exit-code-tests/SuccessTest.php', - $options - ); - $output = $proc->getOutput(); - - static::assertStringContainsString('OK (1 test, 1 assertion)', $output); - static::assertEquals(0, $proc->getExitCode()); - - $options['--processes'] = 3; - $proc = $this->invokeParatest( - 'wrapper-runner-exit-code-tests', - $options - ); - $output = $proc->getOutput(); - static::assertStringContainsString('Tests: 3', $output); - static::assertStringContainsString('Failures: 1', $output); - static::assertStringContainsString('Errors: 1', $output); - static::assertEquals(2, $proc->getExitCode()); // There is at least one error so the exit code must be 2 - } - - public function testParallelSuiteOption(): void - { - $testDir = sys_get_temp_dir() . DS . 'parallel-suite'; - if (! is_dir($testDir)) { - mkdir($testDir); - } - - $glob = glob($testDir . DS . '*'); - self::assertNotFalse($glob); - foreach ($glob as $file) { - unlink($file); - } - - $proc = $this->invokeParatest( - null, - [ - '--configuration' => $this->fixture('phpunit-parallel-suite.xml'), - '--parallel-suite' => true, - '--processes' => 2, - '--runner' => WrapperRunner::class, - ] - ); - - $this->assertTestsPassed($proc); - } -} diff --git a/test/TestBase.php b/test/TestBase.php index b0f9d802..d335d94a 100644 --- a/test/TestBase.php +++ b/test/TestBase.php @@ -6,6 +6,9 @@ use InvalidArgumentException; use ParaTest\Runners\PHPUnit\Options; +use ParaTest\Runners\PHPUnit\Runner; +use ParaTest\Runners\PHPUnit\RunnerInterface; +use ParaTest\Tests\Functional\RunnerResult; use PHPUnit; use PHPUnit\Framework\SkippedTestError; use PHPUnit\Runner\Version; @@ -15,22 +18,41 @@ use ReflectionObject; use ReflectionProperty; use SebastianBergmann\Environment\Runtime; -use SplFileObject; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Output\BufferedOutput; +use Symfony\Component\Filesystem\Filesystem; use function copy; use function file_exists; use function get_class; -use function is_dir; +use function glob; use function preg_match; -use function rmdir; +use function sprintf; use function str_replace; use function uniqid; -use function unlink; abstract class TestBase extends PHPUnit\Framework\TestCase { + /** @var class-string */ + protected $runnerClass = Runner::class; + /** @var array */ + protected $bareOptions = []; + + final protected function setUp(): void + { + $glob = glob(TMP_DIR . DS . '*'); + static::assertNotFalse($glob); + + (new Filesystem())->remove($glob); + + $this->setUpTest(); + } + + protected function setUpTest(): void + { + } + /** * @param array $argv */ @@ -44,6 +66,37 @@ final protected function createOptionsFromArgv(array $argv, ?string $cwd = null) return Options::fromConsoleInput($input, $cwd ?? PARATEST_ROOT); } + final protected function runRunner(?string $runnerClass = null): RunnerResult + { + if ($runnerClass === null) { + $runnerClass = $this->runnerClass; + } + + $bareOptions = $this->bareOptions; + $bareOptions['--tmp-dir'] = TMP_DIR; + $output = new BufferedOutput(); + $wrapperRunner = new $runnerClass($this->createOptionsFromArgv($this->bareOptions), $output); + $wrapperRunner->run(); + + return new RunnerResult($wrapperRunner->getExitCode(), $output->fetch()); + } + + final protected function assertTestsPassed( + RunnerResult $proc, + ?string $testPattern = null, + ?string $assertionPattern = null + ): void { + static::assertMatchesRegularExpression( + sprintf( + '/OK \(%s tests?, %s assertions?\)/', + $testPattern ?? '\d+', + $assertionPattern ?? '\d+' + ), + $proc->getOutput(), + ); + static::assertEquals(0, $proc->getExitCode()); + } + /** * Get PHPUnit version. */ @@ -56,7 +109,7 @@ final protected function fixture(string $fixture): string { $fixture = FIXTURES . DS . $fixture; if (! file_exists($fixture)) { - throw new InvalidArgumentException("Fixture $fixture not found"); + throw new InvalidArgumentException("Fixture {$fixture} not found"); } return $fixture; @@ -159,35 +212,6 @@ final protected static function skipIfCodeCoverageNotEnabled(): void static::markTestSkipped('No code coverage driver available'); } - /** - * Remove dir and its files. - */ - final protected function removeDirectory(string $dirname): void - { - if (! file_exists($dirname) || ! is_dir($dirname)) { - return; - } - - $directory = new RecursiveDirectoryIterator( - $dirname, - RecursiveDirectoryIterator::SKIP_DOTS - ); - /** @var SplFileObject[] $iterator */ - $iterator = new RecursiveIteratorIterator( - $directory, - RecursiveIteratorIterator::CHILD_FIRST - ); - foreach ($iterator as $file) { - if ($file->isDir()) { - rmdir($file->getPathname()); - } else { - unlink($file->getPathname()); - } - } - - rmdir($dirname); - } - /** * Copy fixture file to tmp folder, cause coverage file will be deleted by merger. * diff --git a/test/Unit/Console/Commands/ParaTestCommandTest.php b/test/Unit/Console/Commands/ParaTestCommandTest.php index b83947e2..56a43dfb 100644 --- a/test/Unit/Console/Commands/ParaTestCommandTest.php +++ b/test/Unit/Console/Commands/ParaTestCommandTest.php @@ -6,20 +6,23 @@ use InvalidArgumentException; use ParaTest\Console\Commands\ParaTestCommand; -use ParaTest\Runners\PHPUnit\EmptyRunnerStub; use ParaTest\Tests\TestBase; +use ParaTest\Tests\Unit\Runners\PHPUnit\EmptyRunnerStub; use PHPUnit\TextUI\XmlConfiguration\Exception; use Symfony\Component\Console\Command\HelpCommand; use Symfony\Component\Console\Tester\CommandTester; use function sprintf; +/** + * @covers \ParaTest\Console\Commands\ParaTestCommand + */ final class ParaTestCommandTest extends TestBase { /** @var CommandTester */ private $commandTester; - public function setUp(): void + public function setUpTest(): void { $application = ParaTestCommand::applicationFactory(PARATEST_ROOT); $application->add(new HelpCommand()); diff --git a/test/Unit/Coverage/CoverageMergerTest.php b/test/Unit/Coverage/CoverageMergerTest.php index 80ceefb4..bf0e13b5 100644 --- a/test/Unit/Coverage/CoverageMergerTest.php +++ b/test/Unit/Coverage/CoverageMergerTest.php @@ -6,24 +6,29 @@ use ParaTest\Coverage\CoverageMerger; use ParaTest\Tests\TestBase; +use RuntimeException; use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\CodeCoverage\Driver\Driver; use SebastianBergmann\CodeCoverage\Filter; use SebastianBergmann\CodeCoverage\RawCodeCoverageData; +use SebastianBergmann\CodeCoverage\Report\PHP; +use function touch; + +/** + * @covers \ParaTest\Coverage\CoverageMerger + */ final class CoverageMergerTest extends TestBase { - protected function setUp(): void + protected function setUpTest(): void { static::skipIfCodeCoverageNotEnabled(); } /** - * Test merge for code coverage library 4 version. - * - * @requires function \SebastianBergmann\CodeCoverage\CodeCoverage::merge + * @dataProvider provideTestLimit */ - public function testSimpleMerge(): void + public function testMerge(int $testLimit): void { $firstFile = PARATEST_ROOT . DS . 'src' . DS . 'Logging' . DS . 'LogInterpreter.php'; $secondFile = PARATEST_ROOT . DS . 'src' . DS . 'Logging' . DS . 'MetaProvider.php'; @@ -55,69 +60,57 @@ public function testSimpleMerge(): void 'Test2' ); - $merger = new CoverageMerger(); - $this->call($merger, 'addCoverage', $coverage1); - $this->call($merger, 'addCoverage', $coverage2); + $target1 = TMP_DIR . DS . 'coverage1.php'; + $target2 = TMP_DIR . DS . 'coverage2.php'; + $phpReport = new PHP(); + $phpReport->process($coverage1, $target1); + $phpReport->process($coverage2, $target2); + + $merger = new CoverageMerger($testLimit); + $merger->addCoverageFromFile($target1); + $merger->addCoverageFromFile($target2); - $coverage = $this->getObjectValue($merger, 'coverage'); - static::assertInstanceOf(CodeCoverage::class, $coverage); + static::assertFileDoesNotExist($target1); + static::assertFileDoesNotExist($target2); + $coverage = $merger->getCodeCoverageObject(); + static::assertNotNull($coverage); $data = $coverage->getData()->lineCoverage(); - static::assertCount(2, $data[$firstFile][$firstFileFirstLine]); - static::assertEquals('Test1', $data[$firstFile][$firstFileFirstLine][0]); - static::assertEquals('Test2', $data[$firstFile][$firstFileFirstLine][1]); + if ($testLimit === 0) { + static::assertCount(2, $data[$firstFile][$firstFileFirstLine]); + static::assertEquals('Test1', $data[$firstFile][$firstFileFirstLine][0]); + static::assertEquals('Test2', $data[$firstFile][$firstFileFirstLine][1]); + } else { + static::assertCount(1, $data[$firstFile][$firstFileFirstLine]); + static::assertEquals('Test1', $data[$firstFile][$firstFileFirstLine][0]); + } static::assertCount(1, $data[$secondFile][$secondFileFirstLine]); static::assertEquals('Test1', $data[$secondFile][$secondFileFirstLine][0]); } /** - * Test merge with limits - * - * @requires function \SebastianBergmann\CodeCoverage\CodeCoverage::merge + * @return array */ - public function testSimpleMergeLimited(): void + public function provideTestLimit(): array { - $firstFile = PARATEST_ROOT . DS . 'src' . DS . 'Logging' . DS . 'LogInterpreter.php'; - $secondFile = PARATEST_ROOT . DS . 'src' . DS . 'Logging' . DS . 'MetaProvider.php'; - - // Every time the two above files are changed, the line numbers - // may change, and so these two numbers may need adjustments - $firstFileFirstLine = 45; - $secondFileFirstLine = 53; - - $filter = new Filter(); - $filter->includeFiles([$firstFile, $secondFile]); - - $data = RawCodeCoverageData::fromXdebugWithoutPathCoverage([ - $firstFile => [$firstFileFirstLine => 1], - $secondFile => [$secondFileFirstLine => 1], - ]); - $coverage1 = new CodeCoverage(Driver::forLineCoverage($filter), $filter); - $coverage1->append( - $data, - 'Test1' - ); - - $data = RawCodeCoverageData::fromXdebugWithoutPathCoverage([ - $firstFile => [$firstFileFirstLine => 1, 1 + $firstFileFirstLine => 1], - ]); - $coverage2 = new CodeCoverage(Driver::forLineCoverage($filter), $filter); - $coverage2->append( - $data, - 'Test2' - ); + return [ + 'unlimited' => [0], + 'limited' => [1], + ]; + } - $merger = new CoverageMerger($test_limit = 1); - $this->call($merger, 'addCoverage', $coverage1); - $this->call($merger, 'addCoverage', $coverage2); + public function testCoverageFileIsEmpty(): void + { + $filename = TMP_DIR . DS . 'coverage.php'; + touch($filename); + $coverageMerger = new CoverageMerger(0); - $coverage = $this->getObjectValue($merger, 'coverage'); - static::assertInstanceOf(CodeCoverage::class, $coverage); - $data = $coverage->getData()->lineCoverage(); + $this->expectException(RuntimeException::class); + $regex = '/Coverage file .*? is empty. This means a PHPUnit process has crashed./'; + $this->expectExceptionMessageMatches($regex); - static::assertCount(1, $data[$firstFile][$firstFileFirstLine]); - static::assertCount(1, $data[$secondFile][$secondFileFirstLine]); + $coverageMerger->addCoverageFromFile($filename); } } diff --git a/test/Unit/Coverage/CoverageReporterTest.php b/test/Unit/Coverage/CoverageReporterTest.php new file mode 100644 index 00000000..273e33b0 --- /dev/null +++ b/test/Unit/Coverage/CoverageReporterTest.php @@ -0,0 +1,104 @@ +includeFile(__FILE__); + $codeCoverage = new CodeCoverage(Driver::forLineCoverage($filter), $filter); + $codeCoverage->append(RawCodeCoverageData::fromXdebugWithoutPathCoverage([ + __FILE__ => [__LINE__ => 1], + ]), uniqid()); + + $this->coverageReporter = new CoverageReporter($codeCoverage); + } + + public function testGenerateClover(): void + { + $target = TMP_DIR . DS . 'clover'; + + $this->coverageReporter->clover($target); + + static::assertFileExists($target); + + $reportXml = (new Xml\Loader())->loadFile($target); + static::assertTrue($reportXml->hasChildNodes(), 'Incorrect clover report xml was generated'); + } + + public function testGenerateCrap4j(): void + { + $target = TMP_DIR . DS . 'crap4j'; + + $this->coverageReporter->crap4j($target); + + static::assertFileExists($target); + + $reportXml = (new Xml\Loader())->loadFile($target); + static::assertTrue($reportXml->hasChildNodes(), 'Incorrect crap4j report xml was generated'); + $documentElement = $reportXml->documentElement; + static::assertNotNull($documentElement); + static::assertEquals('crap_result', $documentElement->tagName); + } + + public function testGenerateHtml(): void + { + $target = TMP_DIR . DS . 'html'; + + $this->coverageReporter->html($target); + + static::assertFileExists($target); + static::assertFileExists($target . DS . 'index.html', 'Index html file was not generated'); + } + + public function testGeneratePhp(): void + { + $target = TMP_DIR . DS . 'php'; + + $this->coverageReporter->php($target); + + static::assertFileExists($target); + } + + public function testGenerateText(): void + { + $output = $this->coverageReporter->text(); + + static::assertStringContainsString('Code Coverage Report:', $output); + } + + public function testGenerateXml(): void + { + $target = TMP_DIR . DS . 'xml'; + + $this->coverageReporter->xml($target); + + static::assertFileExists($target); + static::assertFileExists($target . DS . 'index.xml', 'Index xml file was not generated'); + + $reportXml = (new Xml\Loader())->loadFile($target . DS . 'index.xml'); + static::assertTrue($reportXml->hasChildNodes(), 'Incorrect xml was generated'); + } +} diff --git a/test/Unit/Logging/JUnit/ReaderTest.php b/test/Unit/Logging/JUnit/ReaderTest.php index def48d78..ad270dd3 100644 --- a/test/Unit/Logging/JUnit/ReaderTest.php +++ b/test/Unit/Logging/JUnit/ReaderTest.php @@ -13,7 +13,13 @@ use function file_get_contents; use function file_put_contents; +use function implode; +/** + * @covers \ParaTest\Logging\JUnit\Reader + * @covers \ParaTest\Logging\JUnit\TestCase + * @covers \ParaTest\Logging\JUnit\TestSuite + */ final class ReaderTest extends TestBase { /** @var string */ @@ -27,7 +33,7 @@ final class ReaderTest extends TestBase /** @var Reader */ private $multi_errors; - public function setUp(): void + public function setUpTest(): void { $this->mixedPath = FIXTURES . DS . 'results' . DS . 'mixed-results.xml'; $this->mixed = new Reader($this->mixedPath); @@ -67,12 +73,12 @@ public function testMixedSuiteShouldConstructRootSuite(): TestSuite { $suites = $this->mixed->getSuites(); static::assertCount(1, $suites); - static::assertEquals('test/fixtures/tests/', $suites[0]->name); - static::assertEquals('7', $suites[0]->tests); - static::assertEquals('6', $suites[0]->assertions); - static::assertEquals('2', $suites[0]->failures); - static::assertEquals('1', $suites[0]->errors); - static::assertEquals('0.007625', $suites[0]->time); + static::assertSame('test/fixtures/tests/', $suites[0]->name); + static::assertSame(19, $suites[0]->tests); + static::assertSame(10, $suites[0]->assertions); + static::assertSame(3, $suites[0]->failures); + static::assertSame(3, $suites[0]->errors); + static::assertSame(0.001489, $suites[0]->time); return $suites[0]; } @@ -84,16 +90,16 @@ public function testMixedSuiteConstructsChildSuites(TestSuite $suite): TestSuite { static::assertCount(3, $suite->suites); $first = $suite->suites[0]; - static::assertEquals('UnitTestWithClassAnnotationTest', $first->name); - static::assertEquals( - '/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithClassAnnotationTest.php', + static::assertSame('Fixtures\\Tests\\UnitTestWithClassAnnotationTest', $first->name); + static::assertSame( + '/home/brian/Projects/parallel-phpunit/test/fixtures/failing-tests/UnitTestWithClassAnnotationTest.php', $first->file ); - static::assertEquals('3', $first->tests); - static::assertEquals('3', $first->assertions); - static::assertEquals('1', $first->failures); - static::assertEquals('0', $first->errors); - static::assertEquals('0.006109', $first->time); + static::assertSame(4, $first->tests); + static::assertSame(4, $first->assertions); + static::assertSame(1, $first->failures); + static::assertSame(0, $first->errors); + static::assertSame(0.000357, $first->time); return $first; } @@ -103,17 +109,17 @@ public function testMixedSuiteConstructsChildSuites(TestSuite $suite): TestSuite */ public function testMixedSuiteConstructsTestCases(TestSuite $suite): void { - static::assertCount(3, $suite->cases); + static::assertCount(4, $suite->cases); $first = $suite->cases[0]; - static::assertEquals('testTruth', $first->name); - static::assertEquals('UnitTestWithClassAnnotationTest', $first->class); - static::assertEquals( - '/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithClassAnnotationTest.php', + static::assertSame('testTruth', $first->name); + static::assertSame('Fixtures\\Tests\\UnitTestWithClassAnnotationTest', $first->class); + static::assertSame( + '/home/brian/Projects/parallel-phpunit/test/fixtures/failing-tests/UnitTestWithClassAnnotationTest.php', $first->file ); - static::assertEquals('10', $first->line); - static::assertEquals('1', $first->assertions); - static::assertEquals('0.001760', $first->time); + static::assertSame(21, $first->line); + static::assertSame(1, $first->assertions); + static::assertSame(0.000042, $first->time); } public function testMixedSuiteCasesLoadFailures(): void @@ -122,10 +128,10 @@ public function testMixedSuiteCasesLoadFailures(): void $case = $suites[0]->suites[0]->cases[1]; static::assertCount(1, $case->failures); $failure = $case->failures[0]; - static::assertEquals(ExpectationFailedException::class, $failure['type']); - static::assertEquals( - "UnitTestWithClassAnnotationTest::testFalsehood\nFailed asserting that true is false.\n\n" . - '/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithClassAnnotationTest.php:20', + static::assertSame(ExpectationFailedException::class, $failure['type']); + static::assertSame( + "Fixtures\\Tests\\UnitTestWithClassAnnotationTest::testFalsehood\nFailed asserting that true is false.\n\n" . + '/home/brian/Projects/parallel-phpunit/test/fixtures/failing-tests/UnitTestWithClassAnnotationTest.php:32', $failure['text'] ); } @@ -136,10 +142,10 @@ public function testMixedSuiteCasesLoadErrors(): void $case = $suites[0]->suites[1]->cases[0]; static::assertCount(1, $case->errors); $error = $case->errors[0]; - static::assertEquals('Exception', $error['type']); - static::assertEquals( + static::assertSame('Exception', $error['type']); + static::assertSame( "UnitTestWithErrorTest::testTruth\nException: Error!!!\n\n" . - '/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithErrorTest.php:12', + '/home/brian/Projects/parallel-phpunit/test/fixtures/failing-tests/UnitTestWithErrorTest.php:17', $error['text'] ); } @@ -148,16 +154,16 @@ public function testSingleSuiteShouldConstructRootSuite(): TestSuite { $suites = $this->single->getSuites(); static::assertCount(1, $suites); - static::assertEquals('UnitTestWithMethodAnnotationsTest', $suites[0]->name); - static::assertEquals( + static::assertSame('UnitTestWithMethodAnnotationsTest', $suites[0]->name); + static::assertSame( '/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithMethodAnnotationsTest.php', $suites[0]->file ); - static::assertEquals('3', $suites[0]->tests); - static::assertEquals('3', $suites[0]->assertions); - static::assertEquals('1', $suites[0]->failures); - static::assertEquals('0', $suites[0]->errors); - static::assertEquals('0.005895', $suites[0]->time); + static::assertSame(3, $suites[0]->tests); + static::assertSame(3, $suites[0]->assertions); + static::assertSame(1, $suites[0]->failures); + static::assertSame(0, $suites[0]->errors); + static::assertSame(0.005895, $suites[0]->time); return $suites[0]; } @@ -181,15 +187,15 @@ public function testSingleSuiteConstructsTestCases($suite): void { static::assertCount(3, $suite->cases); $first = $suite->cases[0]; - static::assertEquals('testTruth', $first->name); - static::assertEquals('UnitTestWithMethodAnnotationsTest', $first->class); - static::assertEquals( + static::assertSame('testTruth', $first->name); + static::assertSame('UnitTestWithMethodAnnotationsTest', $first->class); + static::assertSame( '/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithMethodAnnotationsTest.php', $first->file ); - static::assertEquals('7', $first->line); - static::assertEquals('1', $first->assertions); - static::assertEquals('0.001632', $first->time); + static::assertSame(7, $first->line); + static::assertSame(1, $first->assertions); + static::assertSame(0.001632, $first->time); } public function testSingleSuiteCasesLoadFailures(): void @@ -198,8 +204,8 @@ public function testSingleSuiteCasesLoadFailures(): void $case = $suites[0]->cases[1]; static::assertCount(1, $case->failures); $failure = $case->failures[0]; - static::assertEquals(ExpectationFailedException::class, $failure['type']); - static::assertEquals( + static::assertSame(ExpectationFailedException::class, $failure['type']); + static::assertSame( "UnitTestWithMethodAnnotationsTest::testFalsehood\nFailed asserting that true is false.\n\n" . '/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithMethodAnnotationsTest.php:18', $failure['text'] @@ -212,46 +218,48 @@ public function testEmptySuiteConstructsTestCase(): void static::assertCount(1, $suites); $suite = $suites[0]; - static::assertEquals('', $suite->name); - static::assertEquals('', $suite->file); - static::assertEquals(0, $suite->tests); - static::assertEquals(0, $suite->assertions); - static::assertEquals(0, $suite->failures); - static::assertEquals(0, $suite->errors); - static::assertEquals(0, $suite->time); + static::assertSame('', $suite->name); + static::assertSame('', $suite->file); + static::assertSame(0, $suite->tests); + static::assertSame(0, $suite->assertions); + static::assertSame(0, $suite->failures); + static::assertSame(0, $suite->errors); + static::assertSame(0.0, $suite->time); } public function testMixedGetTotals(): void { - static::assertEquals(7, $this->mixed->getTotalTests()); - static::assertEquals(6, $this->mixed->getTotalAssertions()); - static::assertEquals(2, $this->mixed->getTotalFailures()); - static::assertEquals(1, $this->mixed->getTotalErrors()); - static::assertEquals(0.007625, $this->mixed->getTotalTime()); + static::assertSame(19, $this->mixed->getTotalTests()); + static::assertSame(10, $this->mixed->getTotalAssertions()); + static::assertSame(3, $this->mixed->getTotalFailures()); + static::assertSame(2, $this->mixed->getTotalWarnings()); + static::assertSame(3, $this->mixed->getTotalErrors()); + static::assertSame(0.001489, $this->mixed->getTotalTime()); } public function testSingleGetTotals(): void { - static::assertEquals(3, $this->single->getTotalTests()); - static::assertEquals(3, $this->single->getTotalAssertions()); - static::assertEquals(1, $this->single->getTotalFailures()); - static::assertEquals(0, $this->single->getTotalErrors()); - static::assertEquals(0.005895, $this->single->getTotalTime()); + static::assertSame(3, $this->single->getTotalTests()); + static::assertSame(3, $this->single->getTotalAssertions()); + static::assertSame(1, $this->single->getTotalFailures()); + static::assertSame(0, $this->single->getTotalWarnings()); + static::assertSame(0, $this->single->getTotalErrors()); + static::assertSame(0.005895, $this->single->getTotalTime()); } public function testMixedGetFailureMessages(): void { $failures = $this->mixed->getFailures(); - static::assertCount(2, $failures); - static::assertEquals( - "UnitTestWithClassAnnotationTest::testFalsehood\nFailed asserting that true is false.\n\n" . - '/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithClassAnnotationTest.php:20', + static::assertCount(3, $failures); + static::assertSame( + "Fixtures\\Tests\\UnitTestWithClassAnnotationTest::testFalsehood\nFailed asserting that true is false.\n\n" . + '/home/brian/Projects/parallel-phpunit/test/fixtures/failing-tests/UnitTestWithClassAnnotationTest.php:32', $failures[0] ); - static::assertEquals( - "UnitTestWithMethodAnnotationsTest::testFalsehood\nFailed asserting that true is false." . - "\n\n/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithMethodAnnotationsTest." . - 'php:18', + static::assertSame( + "UnitTestWithErrorTest::testFalsehood\nFailed asserting that true is false." . + "\n\n/home/brian/Projects/parallel-phpunit/test/fixtures/failing-tests/UnitTestWithMethodAnnotationsTest." . + 'php:20', $failures[1] ); } @@ -259,19 +267,30 @@ public function testMixedGetFailureMessages(): void public function testMixedGetErrorMessages(): void { $errors = $this->mixed->getErrors(); - static::assertCount(1, $errors); - static::assertEquals( + static::assertCount(3, $errors); + static::assertSame( "UnitTestWithErrorTest::testTruth\nException: Error!!!\n\n" . - '/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithErrorTest.php:12', + '/home/brian/Projects/parallel-phpunit/test/fixtures/failing-tests/UnitTestWithErrorTest.php:17', $errors[0] ); } + public function testMixedGetWarningMessages(): void + { + $warnings = $this->mixed->getWarnings(); + static::assertCount(2, $warnings); + static::assertSame( + "UnitTestWithErrorTest::testWarning\n" . + 'Function 1 deprecated', + $warnings[0] + ); + } + public function testSingleGetMessages(): void { $failures = $this->single->getFailures(); static::assertCount(1, $failures); - static::assertEquals( + static::assertSame( "UnitTestWithMethodAnnotationsTest::testFalsehood\nFailed asserting that true is false.\n\n" . '/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithMethodAnnotationsTest.php:18', $failures[0] @@ -285,14 +304,14 @@ public function testGetMultiErrorsMessages(): void { $errors = $this->multi_errors->getErrors(); static::assertCount(2, $errors); - static::assertEquals( + static::assertSame( "Risky Test\n" . "/project/vendor/phpunit/phpunit/src/TextUI/Command.php:200\n" . "/project/vendor/phpunit/phpunit/src/TextUI/Command.php:159\n" . 'Custom error log on result test with multiple errors!', $errors[0] ); - static::assertEquals( + static::assertSame( "Risky Test\n" . "/project/vendor/phpunit/phpunit/src/TextUI/Command.php:200\n" . "/project/vendor/phpunit/phpunit/src/TextUI/Command.php:159\n" . @@ -304,7 +323,7 @@ public function testGetMultiErrorsMessages(): void public function testMixedGetFeedback(): void { $feedback = $this->mixed->getFeedback(); - static::assertEquals(['.', 'F', '.', 'E', '.', 'F', '.'], $feedback); + static::assertSame('.F..E.F.WSSE.F.WSSE', implode('', $feedback)); } public function testRemoveLog(): void @@ -343,7 +362,7 @@ public function testResultWithSystemOut(): void $resultFail = $node->getSuites()[0]->suites[2]->cases[1]->failures[0]['text']; $resultError = $node->getSuites()[0]->suites[1]->cases[1]->errors[0]['text']; - static::assertEquals($failLog, $resultFail); - static::assertEquals($errorLog, $resultError); + static::assertSame($failLog, $resultFail); + static::assertSame($errorLog, $resultError); } } diff --git a/test/Unit/Logging/JUnit/WriterTest.php b/test/Unit/Logging/JUnit/WriterTest.php index a9ed9f09..da8eabc6 100644 --- a/test/Unit/Logging/JUnit/WriterTest.php +++ b/test/Unit/Logging/JUnit/WriterTest.php @@ -13,6 +13,9 @@ use function file_get_contents; use function unlink; +/** + * @covers \ParaTest\Logging\JUnit\Writer + */ final class WriterTest extends TestBase { /** @var Writer */ @@ -22,7 +25,7 @@ final class WriterTest extends TestBase /** @var string */ protected $passing; - public function setUp(): void + public function setUpTest(): void { $this->interpreter = new LogInterpreter(); $this->writer = new Writer($this->interpreter, 'test/fixtures/tests/'); diff --git a/test/Unit/Logging/LogInterpreterTest.php b/test/Unit/Logging/LogInterpreterTest.php index f8847419..b0ef64e0 100644 --- a/test/Unit/Logging/LogInterpreterTest.php +++ b/test/Unit/Logging/LogInterpreterTest.php @@ -11,6 +11,9 @@ use function array_pop; +/** + * @covers \ParaTest\Logging\LogInterpreter + */ final class LogInterpreterTest extends ResultTester { /** @var LogInterpreter */ @@ -19,15 +22,14 @@ final class LogInterpreterTest extends ResultTester protected function setUpInterpreter(): void { $this->interpreter = new LogInterpreter(); - $this->interpreter - ->addReader(new Reader($this->mixedSuite->getTempFile())) - ->addReader(new Reader($this->passingSuite->getTempFile())); + $this->interpreter->addReader(new Reader($this->mixedSuite->getTempFile())); + $this->interpreter->addReader(new Reader($this->passingSuite->getTempFile())); } public function testConstructor(): void { $interpreter = new LogInterpreter(); - static::assertEquals([], $this->getObjectValue($interpreter, 'readers')); + static::assertSame([], $this->getObjectValue($interpreter, 'readers')); } public function testAddReaderIncrementsReaders(): void @@ -37,12 +39,6 @@ public function testAddReaderIncrementsReaders(): void static::assertCount(3, $this->getObjectValue($this->interpreter, 'readers')); } - public function testAddReaderReturnsSelf(): void - { - $self = $this->interpreter->addReader(new Reader($this->failureSuite->getTempFile())); - static::assertSame($self, $this->interpreter); - } - public function testGetReaders(): void { $reader = new Reader($this->failureSuite->getTempFile()); @@ -53,24 +49,14 @@ public function testGetReaders(): void static::assertSame($reader, $last); } - public function testGetTotalTests(): void - { - static::assertEquals(10, $this->interpreter->getTotalTests()); - } - - public function testGetTotalAssertions(): void - { - static::assertEquals(9, $this->interpreter->getTotalAssertions()); - } - - public function testGetTotalFailures(): void + public function testGetTotals(): void { - static::assertEquals(2, $this->interpreter->getTotalFailures()); - } - - public function testGetTotalErrors(): void - { - static::assertEquals(1, $this->interpreter->getTotalErrors()); + static::assertSame(22, $this->interpreter->getTotalTests()); + static::assertSame(13, $this->interpreter->getTotalAssertions()); + static::assertSame(3, $this->interpreter->getTotalFailures()); + static::assertSame(2, $this->interpreter->getTotalWarnings()); + static::assertSame(3, $this->interpreter->getTotalErrors()); + static::assertSame(0.006784, $this->interpreter->getTotalTime()); } public function testIsSuccessfulReturnsFalseIfFailuresPresentAndNoErrors(): void @@ -103,27 +89,40 @@ public function testGetErrorsReturnsArrayOfErrorMessages(): void { $errors = [ "UnitTestWithErrorTest::testTruth\nException: Error!!!\n\n/home/brian/Projects/parallel-phpunit/" . - 'test/fixtures/tests/UnitTestWithErrorTest.php:12', + 'test/fixtures/failing-tests/UnitTestWithErrorTest.php:17', + 'Risky Test', + 'Risky Test', ]; - static::assertEquals($errors, $this->interpreter->getErrors()); + static::assertSame($errors, $this->interpreter->getErrors()); + } + + public function testGetWarningsReturnsArrayOfErrorMessages(): void + { + $errors = [ + "UnitTestWithErrorTest::testWarning\nFunction 1 deprecated", + "UnitTestWithMethodAnnotationsTest::testWarning\nFunction 2 deprecated", + ]; + static::assertSame($errors, $this->interpreter->getWarnings()); } public function testGetFailuresReturnsArrayOfFailureMessages(): void { $failures = [ - "UnitTestWithClassAnnotationTest::testFalsehood\nFailed asserting that true is false.\n\n/" . - 'home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithClassAnnotationTest.php:20', + "Fixtures\\Tests\\UnitTestWithClassAnnotationTest::testFalsehood\nFailed asserting that true is false.\n\n/" . + 'home/brian/Projects/parallel-phpunit/test/fixtures/failing-tests/UnitTestWithClassAnnotationTest.php:32', + "UnitTestWithErrorTest::testFalsehood\nFailed asserting that true is false.\n\n" . + '/home/brian/Projects/parallel-phpunit/test/fixtures/failing-tests/UnitTestWithMethodAnnotationsTest.php:20', "UnitTestWithMethodAnnotationsTest::testFalsehood\nFailed asserting that true is false.\n\n" . - '/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithMethodAnnotationsTest.php:18', + '/home/brian/Projects/parallel-phpunit/test/fixtures/failing-tests/UnitTestWithMethodAnnotationsTest.php:20', ]; - static::assertEquals($failures, $this->interpreter->getFailures()); + static::assertSame($failures, $this->interpreter->getFailures()); } public function testGetCasesReturnsAllCases(): void { $cases = $this->interpreter->getCases(); - static::assertCount(10, $cases); + static::assertCount(22, $cases); } public function testGetCasesExtendEmptyCasesFromSuites(): void @@ -135,25 +134,25 @@ public function testGetCasesExtendEmptyCasesFromSuites(): void static::assertCount(10, $cases); foreach ($cases as $name => $case) { if ($case->name === 'testNumericDataProvider5 with data set #3') { - static::assertEquals($case->class, 'DataProviderTest1'); + static::assertSame($case->class, 'DataProviderTest1'); } elseif ($case->name === 'testNamedDataProvider5 with data set #3') { - static::assertEquals($case->class, 'DataProviderTest2'); + static::assertSame($case->class, 'DataProviderTest2'); } else { - static::assertEquals($case->class, 'DataProviderTest'); + static::assertSame($case->class, 'DataProviderTest'); } if ($case->name === 'testNumericDataProvider5 with data set #4') { - static::assertEquals( + static::assertSame( $case->file, '/var/www/project/vendor/brianium/paratest/test/fixtures/dataprovider-tests/DataProviderTest1.php' ); } elseif ($case->name === 'testNamedDataProvider5 with data set #4') { - static::assertEquals( + static::assertSame( $case->file, '/var/www/project/vendor/brianium/paratest/test/fixtures/dataprovider-tests/DataProviderTest2.php' ); } else { - static::assertEquals( + static::assertSame( $case->file, '/var/www/project/vendor/brianium/paratest/test/fixtures/dataprovider-tests/DataProviderTest.php' ); @@ -180,15 +179,17 @@ public function testFlattenCasesReturnsCorrectNumberOfSuites(): array public function testFlattenedSuiteHasCorrectTotals(array $suites): void { $first = $suites[0]; - static::assertEquals('UnitTestWithClassAnnotationTest', $first->name); - static::assertEquals( - '/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithClassAnnotationTest.php', + static::assertSame('Fixtures\\Tests\\UnitTestWithClassAnnotationTest', $first->name); + static::assertSame( + '/home/brian/Projects/parallel-phpunit/test/fixtures/failing-tests/UnitTestWithClassAnnotationTest.php', $first->file ); - static::assertEquals('3', $first->tests); - static::assertEquals('3', $first->assertions); - static::assertEquals('1', $first->failures); - static::assertEquals('0', $first->errors); - static::assertEquals('0.006109', $first->time); + static::assertSame(4, $first->tests); + static::assertSame(4, $first->assertions); + static::assertSame(1, $first->failures); + static::assertSame(0, $first->warnings); + static::assertSame(0, $first->skipped); + static::assertSame(0, $first->errors); + static::assertSame(0.000357, $first->time); } } diff --git a/test/Unit/Parser/GetClassTest.php b/test/Unit/Parser/GetClassTest.php deleted file mode 100644 index c0341a68..00000000 --- a/test/Unit/Parser/GetClassTest.php +++ /dev/null @@ -1,74 +0,0 @@ -fixture('passing-tests/PreviouslyLoadedTest.php'); - require_once $testFile; - - $class = $this->parseFile($testFile); - static::assertEquals('PreviouslyLoadedTest', $class->getName()); - } - - public function testParsedClassHasName(): void - { - $class = $this->parseFile($this->fixture('failing-tests/UnitTestWithClassAnnotationTest.php')); - static::assertEquals('Fixtures\\Tests\\UnitTestWithClassAnnotationTest', $class->getName()); - } - - public function testParsedAnonymousClassNameHasNoNullByte(): void - { - $class = $this->parseFile($this->fixture('failing-tests/AnonymousClass.inc')); - static::assertStringNotContainsString("\x00", $class->getName()); - } - - public function testParsedClassHasDocBlock(): void - { - $class = $this->parseFile($this->fixture('failing-tests/UnitTestWithClassAnnotationTest.php')); - static::assertEquals('/** - * @runParallel - * @pizzaBox - */', $class->getDocBlock()); - } - - public function testParsedClassHasNamespace(): void - { - $class = $this->parseFile($this->fixture('failing-tests/UnitTestWithClassAnnotationTest.php')); - static::assertEquals('Fixtures\\Tests', $class->getNamespace()); - } - - public function testParsedClassHasCorrectNumberOfTestMethods(): void - { - $class = $this->parseFile($this->fixture('failing-tests/UnitTestWithClassAnnotationTest.php')); - static::assertCount(4, $class->getMethods()); - } - - public function testParsedClassWithParentHasCorrectNumberOfTestMethods(): void - { - $class = $this->parseFile($this->fixture('failing-tests/UnitTestWithErrorTest.php')); - static::assertCount(4, $class->getMethods()); - } - - /** - * Parses a test case and returns the test class. - * - * @param mixed $path - */ - private function parseFile($path): ParsedClass - { - $parser = new Parser($path); - $parserClass = $parser->getClass(); - static::assertNotNull($parserClass); - - return $parserClass; - } -} diff --git a/test/Unit/Parser/ParsedClassTest.php b/test/Unit/Parser/ParsedClassTest.php index ac5dcfc3..a60ea21d 100644 --- a/test/Unit/Parser/ParsedClassTest.php +++ b/test/Unit/Parser/ParsedClassTest.php @@ -8,14 +8,17 @@ use ParaTest\Parser\ParsedFunction; use ParaTest\Tests\TestBase; +/** + * @covers \ParaTest\Parser\ParsedClass + */ final class ParsedClassTest extends TestBase { /** @var ParsedClass */ - protected $class; + private $class; /** @var ParsedFunction[] */ - protected $methods; + private $methods; - public function setUp(): void + public function setUpTest(): void { $this->methods = [ new ParsedFunction( @@ -32,43 +35,12 @@ public function setUp(): void ), new ParsedFunction('', 'testFunction3'), ]; - $this->class = new ParsedClass('', 'MyTestClass', '', $this->methods); + $this->class = new ParsedClass('', 'MyTestClass', 'MyNamespace', $this->methods); } - public function testGetMethodsReturnsMethods(): void + public function testGetters(): void { - static::assertEquals($this->methods, $this->class->getMethods()); - } - - public function testGetMethodsMultipleAnnotationsReturnsMethods(): void - { - $goodMethod = new ParsedFunction( - '/** - * @group group1 - */', - 'testFunction' - ); - $goodMethod2 = new ParsedFunction( - '/** - * @group group2 - */', - 'testFunction2' - ); - $badMethod = new ParsedFunction( - '/** - * @group group3 - */', - 'testFunction2' - ); - $annotatedClass = new ParsedClass('', 'MyTestClass', '', [$goodMethod, $goodMethod2, $badMethod]); - $methods = $annotatedClass->getMethods(['group' => 'group1,group2']); - static::assertEquals([$goodMethod, $goodMethod2], $methods); - } - - public function testGetMethodsExceptsAdditionalAnnotationFilter(): void - { - $group1 = $this->class->getMethods(['group' => 'group1']); - static::assertCount(1, $group1); - static::assertEquals($this->methods[0], $group1[0]); + static::assertSame('MyNamespace', $this->class->getNamespace()); + static::assertSame($this->methods, $this->class->getMethods()); } } diff --git a/test/Unit/Parser/ParsedObjectTest.php b/test/Unit/Parser/ParsedObjectTest.php index 4e14533e..b3434177 100644 --- a/test/Unit/Parser/ParsedObjectTest.php +++ b/test/Unit/Parser/ParsedObjectTest.php @@ -7,37 +7,26 @@ use ParaTest\Parser\ParsedClass; use ParaTest\Tests\TestBase; +/** + * @covers \ParaTest\Parser\ParsedObject + * @covers \ParaTest\Parser\ParsedFunction + */ final class ParsedObjectTest extends TestBase { /** @var ParsedClass */ - protected $parsedClass; + private $parsedClass; + /** @var string */ + private $docBlock; - public function setUp(): void + public function setUpTest(): void { - $this->parsedClass = new ParsedClass("/**\n * @test\n @group group1\n*\/", 'MyClass', 'My\\Name\\Space'); + $this->docBlock = "/**\n * @test\n @group group1\n*\\/"; + $this->parsedClass = new ParsedClass($this->docBlock, self::class, 'My\\Name\\Space', []); } - public function testHasAnnotationReturnsTrueWhenAnnotationPresent(): void + public function testGetters(): void { - $hasAnnotation = $this->parsedClass->hasAnnotation('test'); - static::assertTrue($hasAnnotation); - } - - public function testHasAnnotationReturnsFalseWhenAnnotationNotPresent(): void - { - $hasAnnotation = $this->parsedClass->hasAnnotation('pizza'); - static::assertFalse($hasAnnotation); - } - - public function testHasAnnotationReturnsTrueWhenAnnotationAndValueMatch(): void - { - $hasAnnotation = $this->parsedClass->hasAnnotation('group', 'group1'); - static::assertTrue($hasAnnotation); - } - - public function testHasAnnotationReturnsFalseWhenAnnotationAndValueDontMatch(): void - { - $hasAnnotation = $this->parsedClass->hasAnnotation('group', 'group2'); - static::assertFalse($hasAnnotation); + static::assertSame(self::class, $this->parsedClass->getName()); + static::assertSame($this->docBlock, $this->parsedClass->getDocBlock()); } } diff --git a/test/Unit/Parser/ParserTest.php b/test/Unit/Parser/ParserTest.php index dc500d1f..679dddc4 100644 --- a/test/Unit/Parser/ParserTest.php +++ b/test/Unit/Parser/ParserTest.php @@ -6,33 +6,44 @@ use InvalidArgumentException; use ParaTest\Parser\NoClassInFileException; +use ParaTest\Parser\ParsedClass; use ParaTest\Parser\Parser; use ParaTest\Tests\TestBase; +use function uniqid; + +/** + * @covers \ParaTest\Parser\Parser + */ final class ParserTest extends TestBase { public function testConstructorThrowsExceptionIfFileNotFound(): void { $this->expectException(InvalidArgumentException::class); - new Parser('/path/to/nowhere'); + new Parser(uniqid('/path/to/nowhere')); } public function testConstructorThrowsExceptionIfClassNotFoundInFile(): void { $fileWithoutAClass = FIXTURES . DS . 'fileWithoutClasses.php'; + $this->expectException(NoClassInFileException::class); new Parser($fileWithoutAClass); } + public function testExcludeAbstractClasses(): void + { + $parser = new Parser($this->fixture('warning-tests' . DS . 'AbstractTest.php')); + + static::assertNull($parser->getClass()); + } + public function testPrefersClassByFileName(): void { - $filename = FIXTURES . DS . 'special-classes' . DS . 'SomeNamespace' . DS . 'ParserTestClass.php'; - $parser = new Parser($filename); - $parserClass = $parser->getClass(); - static::assertNotNull($parserClass); - static::assertEquals('SomeNamespace\\ParserTestClass', $parserClass->getName()); + $class = $this->parseFile($this->fixture('special-classes' . DS . 'SomeNamespace' . DS . 'ParserTestClass.php')); + static::assertEquals('SomeNamespace\\ParserTestClass', $class->getName()); } public function testClassFallsBackOnExisting(): void @@ -43,4 +54,61 @@ public function testClassFallsBackOnExisting(): void static::assertNotNull($parserClass); static::assertEquals('ParserTestClassFallsBack', $parserClass->getName()); } + + public function testPreviouslyLoadedTestClassCanBeParsed(): void + { + $class = $this->parseFile($this->fixture('passing-tests' . DS . 'PreviouslyLoadedTest.php')); + static::assertEquals('PreviouslyLoadedTest', $class->getName()); + } + + public function testParsedClassHasName(): void + { + $class = $this->parseFile($this->fixture('failing-tests' . DS . 'UnitTestWithClassAnnotationTest.php')); + static::assertEquals('Fixtures\\Tests\\UnitTestWithClassAnnotationTest', $class->getName()); + } + + public function testParsedAnonymousClassNameHasNoNullByte(): void + { + $class = $this->parseFile($this->fixture('failing-tests' . DS . 'AnonymousClass.inc')); + static::assertStringNotContainsString("\x00", $class->getName()); + } + + public function testParsedClassHasDocBlock(): void + { + $class = $this->parseFile($this->fixture('failing-tests' . DS . 'UnitTestWithClassAnnotationTest.php')); + static::assertEquals('/** + * @runParallel + * @pizzaBox + */', $class->getDocBlock()); + } + + public function testParsedClassHasNamespace(): void + { + $class = $this->parseFile($this->fixture('failing-tests' . DS . 'UnitTestWithClassAnnotationTest.php')); + static::assertEquals('Fixtures\\Tests', $class->getNamespace()); + } + + public function testParsedClassHasCorrectNumberOfTestMethods(): void + { + $class = $this->parseFile($this->fixture('failing-tests' . DS . 'UnitTestWithClassAnnotationTest.php')); + static::assertCount(4, $class->getMethods()); + } + + public function testParsedClassWithParentHasCorrectNumberOfTestMethods(): void + { + $class = $this->parseFile($this->fixture('failing-tests' . DS . 'UnitTestWithErrorTest.php')); + static::assertCount(8, $class->getMethods()); + } + + /** + * Parses a test case and returns the test class. + */ + private function parseFile(string $path): ParsedClass + { + $parser = new Parser($path); + $parserClass = $parser->getClass(); + static::assertNotNull($parserClass); + + return $parserClass; + } } diff --git a/test/Unit/ResultTester.php b/test/Unit/ResultTester.php index d5a87ee4..cadec05e 100644 --- a/test/Unit/ResultTester.php +++ b/test/Unit/ResultTester.php @@ -8,6 +8,9 @@ use ParaTest\Runners\PHPUnit\TestMethod; use ParaTest\Tests\TestBase; +use function file_get_contents; +use function file_put_contents; + abstract class ResultTester extends TestBase { /** @var Suite */ @@ -22,13 +25,16 @@ abstract class ResultTester extends TestBase protected $dataProviderSuite; /** @var Suite */ protected $errorSuite; + /** @var Suite */ + protected $skipped; - final public function setUp(): void + final public function setUpTest(): void { $this->errorSuite = $this->getSuiteWithResult('single-werror.xml', 1); $this->otherErrorSuite = $this->getSuiteWithResult('single-werror2.xml', 1); $this->failureSuite = $this->getSuiteWithResult('single-wfailure.xml', 3); $this->mixedSuite = $this->getSuiteWithResult('mixed-results.xml', 7); + $this->skipped = $this->getSuiteWithResult('single-skipped.xml', 1); $this->passingSuite = $this->getSuiteWithResult('single-passing.xml', 3); $this->dataProviderSuite = $this->getSuiteWithResult('data-provider-result.xml', 50); @@ -39,14 +45,13 @@ abstract protected function setUpInterpreter(): void; final protected function getSuiteWithResult(string $result, int $methodCount): Suite { - $result = FIXTURES . DS . 'results' . DS . $result; $functions = []; for ($i = 0; $i < $methodCount; ++$i) { - $functions[] = new TestMethod((string) $i, [], false); + $functions[] = new TestMethod((string) $i, [], false, TMP_DIR); } - $suite = new Suite('', $functions, false); - $suite->setTempFile($result); + $suite = new Suite('', $functions, false, TMP_DIR); + file_put_contents($suite->getTempFile(), (string) file_get_contents(FIXTURES . DS . 'results' . DS . $result)); return $suite; } diff --git a/test/Unit/Runners/PHPUnit/BaseRunnerTest.php b/test/Unit/Runners/PHPUnit/BaseRunnerTest.php new file mode 100644 index 00000000..eae75057 --- /dev/null +++ b/test/Unit/Runners/PHPUnit/BaseRunnerTest.php @@ -0,0 +1,129 @@ +bareOptions = [ + '--path' => FIXTURES . DS . 'failing-tests', + '--coverage-clover' => TMP_DIR . DS . 'coverage.clover', + '--coverage-crap4j' => TMP_DIR . DS . 'coverage.crap4j', + '--coverage-html' => TMP_DIR . DS . 'coverage.html', + '--coverage-php' => TMP_DIR . DS . 'coverage.php', + '--coverage-text' => true, + '--coverage-xml' => TMP_DIR . DS . 'coverage.xml', + '--bootstrap' => BOOTSTRAP, + '--whitelist' => FIXTURES . DS . 'failing-tests', + ]; + } + + /** + * @return string[] + */ + private function globTempDir(string $pattern): array + { + $glob = glob(TMP_DIR . DS . $pattern); + assert($glob !== false); + + return $glob; + } + + public function testGeneratesCoverageTypes(): void + { + static::assertFileDoesNotExist((string) $this->bareOptions['--coverage-clover']); + static::assertFileDoesNotExist((string) $this->bareOptions['--coverage-crap4j']); + static::assertFileDoesNotExist((string) $this->bareOptions['--coverage-html']); + static::assertFileDoesNotExist((string) $this->bareOptions['--coverage-php']); + static::assertFileDoesNotExist((string) $this->bareOptions['--coverage-xml']); + + $runnerResult = $this->runRunner(); + + static::assertFileExists((string) $this->bareOptions['--coverage-clover']); + static::assertFileExists((string) $this->bareOptions['--coverage-crap4j']); + static::assertFileExists((string) $this->bareOptions['--coverage-html']); + static::assertFileExists((string) $this->bareOptions['--coverage-php']); + static::assertFileExists((string) $this->bareOptions['--coverage-xml']); + + static::assertStringContainsString('Code Coverage Report:', $runnerResult->getOutput()); + } + + public function testRunningTestsShouldLeaveNoTempFiles(): void + { + // Needed for one line coverage on early exit CS Fix :\ + unset($this->bareOptions['--coverage-php']); + + $countBefore = count($this->globTempDir('PT_*')); + $countCoverageBefore = count($this->globTempDir('CV_*')); + + $this->runRunner(); + + $countAfter = count($this->globTempDir('PT_*')); + $countCoverageAfter = count($this->globTempDir('CV_*')); + + static::assertSame( + $countAfter, + $countBefore, + "Test Runner failed to clean up the 'PT_*' file in " . TMP_DIR + ); + static::assertSame( + $countCoverageAfter, + $countCoverageBefore, + "Test Runner failed to clean up the 'CV_*' file in " . TMP_DIR + ); + } + + public function testLogJUnitCreatesXmlFile(): void + { + $outputPath = TMP_DIR . DS . 'test-output.xml'; + + $this->bareOptions['--log-junit'] = $outputPath; + + $this->runRunner(); + + static::assertFileExists($outputPath); + $this->assertJunitXmlIsCorrect($outputPath); + } + + public function assertJunitXmlIsCorrect(string $path): void + { + $doc = simplexml_load_file($path); + assert($doc !== false); + $suites = $doc->xpath('//testsuite'); + $cases = $doc->xpath('//testcase'); + $failures = $doc->xpath('//failure'); + $warnings = $doc->xpath('//warning'); + $skipped = $doc->xpath('//skipped'); + $errors = $doc->xpath('//error'); + + // these numbers represent the tests in fixtures/failing-tests + // so will need to be updated when tests are added or removed + static::assertNotFalse($suites); + static::assertCount(6, $suites); + static::assertNotFalse($cases); + static::assertCount(24, $cases); + static::assertNotFalse($failures); + static::assertCount(6, $failures); + static::assertNotFalse($warnings); + static::assertCount(2, $warnings); + static::assertNotFalse($skipped); + static::assertCount(4, $skipped); + static::assertNotFalse($errors); + static::assertCount(3, $errors); + } +} diff --git a/test/Unit/Runners/PHPUnit/EmptyRunnerStub.php b/test/Unit/Runners/PHPUnit/EmptyRunnerStub.php new file mode 100644 index 00000000..dbb9f9b2 --- /dev/null +++ b/test/Unit/Runners/PHPUnit/EmptyRunnerStub.php @@ -0,0 +1,39 @@ +options = $options; + $this->output = $output; + } + + public function run(): void + { + $this->output->writeln('Path: ' . $this->options->path()); + $this->output->writeln('Configuration: ' . (($conf = $this->options->configuration()) !== null + ? $conf->filename() + : '' + )); + $this->output->writeln(self::OUTPUT); + } + + public function getExitCode(): int + { + return 0; + } +} diff --git a/test/Unit/Runners/PHPUnit/ExecutableTestTest.php b/test/Unit/Runners/PHPUnit/ExecutableTestTest.php index 15e542de..afca3252 100644 --- a/test/Unit/Runners/PHPUnit/ExecutableTestTest.php +++ b/test/Unit/Runners/PHPUnit/ExecutableTestTest.php @@ -6,49 +6,61 @@ use ParaTest\Tests\TestBase; -use function defined; -use function unlink; +use function uniqid; +/** + * @covers \ParaTest\Runners\PHPUnit\ExecutableTest + */ final class ExecutableTestTest extends TestBase { /** @var ExecutableTestChild */ protected $executableTestChild; - public function setUp(): void + public function setUpTest(): void { - $this->executableTestChild = new ExecutableTestChild('pathToFile', true); + $this->executableTestChild = new ExecutableTestChild('pathToFile', true, TMP_DIR); } public function testConstructor(): void { - static::assertEquals('pathToFile', $this->getObjectValue($this->executableTestChild, 'path')); + static::assertEquals('pathToFile', $this->executableTestChild->getPath()); } public function testCommandRedirectsCoverage(): void { - $options = ['a' => 'b']; - $binary = '/usr/bin/phpunit'; - - $command = $this->executableTestChild->command($binary, $options); - - if (defined('PHP_WINDOWS_VERSION_BUILD')) { - static::assertMatchesRegularExpression( - '#^"/usr/bin/phpunit" --a b .+#', - $command - ); - } else { - static::assertMatchesRegularExpression( - "#^'/usr/bin/phpunit' '--a' 'b' .+#", - $command - ); - } + $binary = uniqid('phpunit'); + $options = ['a' => 'b', 'no-coverage' => null]; + $passthru = ['--no-extensions']; + + $commandArguments = $this->executableTestChild->commandArguments($binary, $options, $passthru); + + $expected = [ + $binary, + '--no-extensions', + '--a', + 'b', + '--no-coverage', + '--log-junit', + $this->executableTestChild->getTempFile(), + '--coverage-php', + $this->executableTestChild->getCoverageFileName(), + 'pathToFile', + ]; + + static::assertSame($expected, $commandArguments); } public function testGetTempFileShouldCreateTempFile(): void { - $file = $this->executableTestChild->getTempFile(); - static::assertFileExists($file); - unlink($file); + $logFile = $this->executableTestChild->getTempFile(); + static::assertFileExists($logFile); + $this->executableTestChild->deleteFile(); + static::assertFileDoesNotExist($logFile); + + $ccFile = $this->executableTestChild->getCoverageFileName(); + static::assertFileExists($ccFile); + $this->executableTestChild->deleteFile(); + static::assertFileDoesNotExist($ccFile); } public function testGetTempFileShouldReturnSameFileIfAlreadyCalled(): void @@ -56,6 +68,14 @@ public function testGetTempFileShouldReturnSameFileIfAlreadyCalled(): void $file = $this->executableTestChild->getTempFile(); $fileAgain = $this->executableTestChild->getTempFile(); static::assertEquals($file, $fileAgain); - unlink($file); + } + + public function testStoreLastCommand(): void + { + static::assertEmpty($this->executableTestChild->getLastCommand()); + + $this->executableTestChild->setLastCommand($lastCommand = uniqid()); + + static::assertSame($lastCommand, $this->executableTestChild->getLastCommand()); } } diff --git a/test/Unit/Runners/PHPUnit/FullSuiteTest.php b/test/Unit/Runners/PHPUnit/FullSuiteTest.php new file mode 100644 index 00000000..a3ef88a8 --- /dev/null +++ b/test/Unit/Runners/PHPUnit/FullSuiteTest.php @@ -0,0 +1,28 @@ +commandArguments(uniqid(), [], null); + + static::assertContains('--testsuite', $commandArguments); + static::assertContains($name, $commandArguments); + static::assertSame(1, $fullSuite->getTestCount()); + } +} diff --git a/test/Unit/Runners/PHPUnit/OptionsTest.php b/test/Unit/Runners/PHPUnit/OptionsTest.php index 028008e1..aba078a3 100644 --- a/test/Unit/Runners/PHPUnit/OptionsTest.php +++ b/test/Unit/Runners/PHPUnit/OptionsTest.php @@ -4,29 +4,30 @@ namespace ParaTest\Tests\Unit\Runners\PHPUnit; +use InvalidArgumentException; use ParaTest\Runners\PHPUnit\Options; use ParaTest\Tests\TestBase; use Symfony\Component\Console\Input\InputDefinition; use function defined; use function file_put_contents; -use function glob; use function intdiv; -use function is_dir; -use function mkdir; +use function mt_rand; use function sort; -use function unlink; +use function str_replace; +use function sys_get_temp_dir; +/** + * @covers \ParaTest\Runners\PHPUnit\Options + */ final class OptionsTest extends TestBase { /** @var Options */ private $options; /** @var array */ private $unfiltered; - /** @var string */ - private $testCwd; - public function setUp(): void + public function setUpTest(): void { $this->unfiltered = [ '--processes' => 5, @@ -37,27 +38,8 @@ public function setUp(): void '--exclude-group' => 'group2', '--bootstrap' => '/path/to/bootstrap', ]; - $this->options = $this->createOptionsFromArgv($this->unfiltered); - $this->testCwd = __DIR__ . DS . 'generated-configs'; - if (! is_dir($this->testCwd)) { - mkdir($this->testCwd, 0777, true); - } - - $this->cleanUpGeneratedFiles(); - } - - protected function tearDown(): void - { - $this->cleanUpGeneratedFiles(); - } - private function cleanUpGeneratedFiles(): void - { - $glob = glob($this->testCwd . DS . '*'); - self::assertNotFalse($glob); - foreach ($glob as $file) { - unlink($file); - } + $this->options = $this->createOptionsFromArgv($this->unfiltered); } public function testOptionsAreOrdered(): void @@ -91,17 +73,14 @@ public function testFilteredOptionsIsSet(): void static::assertEquals([$this->unfiltered['--group']], $this->options->group()); } - public function testAnnotationsReturnsAnnotations(): void - { - static::assertCount(1, $this->options->annotations()); - static::assertEquals('group1', $this->options->annotations()['group']); - } - - public function testAnnotationsDefaultsToEmptyArray(): void + public function testFilterOptionRequiresFunctionalMode(): void { - $options = $this->createOptionsFromArgv([]); + $this->expectException(InvalidArgumentException::class); - static::assertEmpty($options->annotations()); + $this->createOptionsFromArgv([ + '--functional' => false, + '--filter' => 'testMe', + ]); } public function testHalfProcessesMode(): void @@ -113,17 +92,17 @@ public function testHalfProcessesMode(): void public function testConfigurationShouldReturnXmlIfConfigNotSpecifiedAndFileExistsInCwd(): void { - $this->assertConfigurationFileFiltered('phpunit.xml', $this->testCwd); + $this->assertConfigurationFileFiltered('phpunit.xml', TMP_DIR); } public function testConfigurationShouldReturnXmlDistIfConfigAndXmlNotSpecifiedAndFileExistsInCwd(): void { - $this->assertConfigurationFileFiltered('phpunit.xml.dist', $this->testCwd); + $this->assertConfigurationFileFiltered('phpunit.xml.dist', TMP_DIR); } public function testConfigurationShouldReturnSpecifiedConfigurationIfFileExists(): void { - $this->assertConfigurationFileFiltered('phpunit-myconfig.xml', $this->testCwd, 'phpunit-myconfig.xml'); + $this->assertConfigurationFileFiltered('phpunit-myconfig.xml', TMP_DIR, 'phpunit-myconfig.xml'); } public function testConfigurationKeyIsNotPresentIfNoConfigGiven(): void @@ -135,16 +114,13 @@ public function testConfigurationKeyIsNotPresentIfNoConfigGiven(): void public function testPassthru(): void { + $argv = [ + '--passthru' => "'--prepend' 'xdebug-filter.php'", + '--passthru-php' => "'-d' 'zend_extension=xdebug.so'", + ]; if (defined('PHP_WINDOWS_VERSION_BUILD')) { - $argv = [ - '--passthru' => '"--prepend" "xdebug-filter.php"', - '--passthru-php' => '"-d" "zend_extension=xdebug.so"', - ]; - } else { - $argv = [ - '--passthru' => "'--prepend' 'xdebug-filter.php'", - '--passthru-php' => "'-d' 'zend_extension=xdebug.so'", - ]; + $argv['--passthru'] = str_replace('\'', '"', $argv['--passthru']); + $argv['--passthru-php'] = str_replace('\'', '"', $argv['--passthru-php']); } $options = $this->createOptionsFromArgv($argv); @@ -162,12 +138,12 @@ public function testPassthru(): void public function testConfigurationShouldReturnXmlIfConfigSpecifiedAsDirectoryAndFileExists(): void { - $this->assertConfigurationFileFiltered('phpunit.xml', $this->testCwd, $this->testCwd); + $this->assertConfigurationFileFiltered('phpunit.xml', TMP_DIR, TMP_DIR); } public function testConfigurationShouldReturnXmlDistIfConfigSpecifiedAsDirectoryAndFileExists(): void { - $this->assertConfigurationFileFiltered('phpunit.xml.dist', $this->testCwd, $this->testCwd); + $this->assertConfigurationFileFiltered('phpunit.xml.dist', TMP_DIR, TMP_DIR); } private function assertConfigurationFileFiltered( @@ -175,17 +151,17 @@ private function assertConfigurationFileFiltered( string $path, ?string $configurationParameter = null ): void { - file_put_contents($this->testCwd . DS . $configFileName, ''); + file_put_contents(TMP_DIR . DS . $configFileName, ''); $this->unfiltered['path'] = $path; if ($configurationParameter !== null) { $this->unfiltered['--configuration'] = $configurationParameter; } - $options = $this->createOptionsFromArgv($this->unfiltered, $this->testCwd); + $options = $this->createOptionsFromArgv($this->unfiltered, TMP_DIR); $configuration = $options->configuration(); static::assertNotNull($configuration); static::assertEquals( - $this->testCwd . DS . $configFileName, + TMP_DIR . DS . $configFileName, $configuration->filename() ); } @@ -206,6 +182,7 @@ public function testDefaultOptions(): void static::assertNull($options->coverageXml()); static::assertEmpty($options->excludeGroup()); static::assertNull($options->filter()); + static::assertEmpty($options->filtered()); static::assertFalse($options->functional()); static::assertEmpty($options->group()); static::assertNull($options->logJunit()); @@ -220,6 +197,8 @@ public function testDefaultOptions(): void static::assertStringContainsString('Runner', $options->runner()); static::assertFalse($options->stopOnFailure()); static::assertEmpty($options->testsuite()); + static::assertSame(sys_get_temp_dir(), $options->tmpDir()); + static::assertSame(0, $options->verbose()); static::assertNull($options->whitelist()); } @@ -252,6 +231,8 @@ public function testProvidedOptions(): void '--runner' => 'MYRUNNER', '--stop-on-failure' => true, '--testsuite' => 'TESTSUITE', + '--tmp-dir' => TMP_DIR, + '--verbose' => 1, '--whitelist' => 'WHITELIST', ]; @@ -283,6 +264,42 @@ public function testProvidedOptions(): void static::assertSame('MYRUNNER', $options->runner()); static::assertTrue($options->stopOnFailure()); static::assertSame(['TESTSUITE'], $options->testsuite()); + static::assertSame(TMP_DIR, $options->tmpDir()); + static::assertSame(1, $options->verbose()); static::assertSame('WHITELIST', $options->whitelist()); + + static::assertSame([ + 'bootstrap' => 'BOOTSTRAP', + 'configuration' => $options->configuration()->filename(), + 'group' => 'GROUP', + 'exclude-group' => 'EXCLUDE-GROUP', + 'whitelist' => 'WHITELIST', + ], $options->filtered()); + + static::assertTrue($options->hasCoverage()); + } + + public function testFillEnvWithTokens(): void + { + $options = $this->createOptionsFromArgv(['--no-test-tokens' => false]); + + $inc = mt_rand(10, 99); + $env = $options->fillEnvWithTokens($inc); + + static::assertSame(1, $env['PARATEST']); + static::assertArrayHasKey(Options::ENV_KEY_TOKEN, $env); + static::assertSame($inc, $env[Options::ENV_KEY_TOKEN]); + static::assertArrayHasKey(Options::ENV_KEY_UNIQUE_TOKEN, $env); + static::assertIsString($env[Options::ENV_KEY_UNIQUE_TOKEN]); + static::assertStringContainsString($inc . '_', $env[Options::ENV_KEY_UNIQUE_TOKEN]); + + $options = $this->createOptionsFromArgv(['--no-test-tokens' => true]); + + $inc = mt_rand(10, 99); + $env = $options->fillEnvWithTokens($inc); + + static::assertSame(1, $env['PARATEST']); + static::assertArrayNotHasKey(Options::ENV_KEY_TOKEN, $env); + static::assertArrayNotHasKey(Options::ENV_KEY_UNIQUE_TOKEN, $env); } } diff --git a/test/Unit/Runners/PHPUnit/ResultPrinterTest.php b/test/Unit/Runners/PHPUnit/ResultPrinterTest.php index 8f1a9cb5..361d644f 100644 --- a/test/Unit/Runners/PHPUnit/ResultPrinterTest.php +++ b/test/Unit/Runners/PHPUnit/ResultPrinterTest.php @@ -10,14 +10,17 @@ use ParaTest\Runners\PHPUnit\Suite; use ParaTest\Runners\PHPUnit\TestMethod; use ParaTest\Tests\Unit\ResultTester; +use RuntimeException; use Symfony\Component\Console\Output\BufferedOutput; use function defined; -use function file_exists; use function file_put_contents; use function sprintf; -use function unlink; +use function uniqid; +/** + * @covers \ParaTest\Runners\PHPUnit\ResultPrinter + */ final class ResultPrinterTest extends ResultTester { /** @var ResultPrinter */ @@ -40,22 +43,13 @@ protected function setUpInterpreter(): void $this->output = new BufferedOutput(); $this->options = $this->createOptionsFromArgv([]); $this->printer = new ResultPrinter($this->interpreter, $this->output, $this->options); - $pathToConfig = $this->getPathToConfig(); - if (file_exists($pathToConfig)) { - unlink($pathToConfig); - } $this->passingSuiteWithWrongTestCountEstimation = $this->getSuiteWithResult('single-passing.xml', 1); } - private function getPathToConfig(): string - { - return __DIR__ . DS . 'phpunit-myconfig.xml'; - } - public function testConstructor(): void { - static::assertEquals([], $this->getObjectValue($this->printer, 'suites')); + static::assertSame([], $this->getObjectValue($this->printer, 'suites')); static::assertInstanceOf( LogInterpreter::class, $this->getObjectValue($this->printer, 'results') @@ -64,20 +58,11 @@ public function testConstructor(): void public function testAddTestShouldAddTest(): void { - $suite = new Suite('/path/to/ResultSuite.php', [], false); + $suite = new Suite('/path/to/ResultSuite.php', [], false, TMP_DIR); $this->printer->addTest($suite); - static::assertEquals([$suite], $this->getObjectValue($this->printer, 'suites')); - } - - public function testAddTestReturnsSelf(): void - { - $suite = new Suite('/path/to/ResultSuite.php', [], false); - - $self = $this->printer->addTest($suite); - - static::assertSame($this->printer, $self); + static::assertSame([$suite], $this->getObjectValue($this->printer, 'suites')); } public function testStartPrintsOptionInfo(): void @@ -95,26 +80,27 @@ public function testStartSetsWidthAndMaxColumn(): void { $funcs = []; for ($i = 0; $i < 120; ++$i) { - $funcs[] = new TestMethod((string) $i, [], false); + $funcs[] = new TestMethod((string) $i, [], false, TMP_DIR); } - $suite = new Suite('/path', $funcs, false); + $suite = new Suite('/path', $funcs, false, TMP_DIR); $this->printer->addTest($suite); $this->getStartOutput(); $numTestsWidth = $this->getObjectValue($this->printer, 'numTestsWidth'); - static::assertEquals(3, $numTestsWidth); + static::assertSame(3, $numTestsWidth); $maxExpectedColumun = 63; if (defined('PHP_WINDOWS_VERSION_BUILD')) { $maxExpectedColumun -= 1; } $maxColumn = $this->getObjectValue($this->printer, 'maxColumn'); - static::assertEquals($maxExpectedColumun, $maxColumn); + static::assertSame($maxExpectedColumun, $maxColumn); } public function testStartPrintsOptionInfoAndConfigurationDetailsIfConfigFilePresent(): void { - $pathToConfig = $this->getPathToConfig(); + $pathToConfig = TMP_DIR . DS . 'phpunit-myconfig.xml'; + file_put_contents($pathToConfig, ''); $this->printer = new ResultPrinter($this->interpreter, $this->output, $this->createOptionsFromArgv(['--configuration' => $pathToConfig])); $contents = $this->getStartOutput(); @@ -150,24 +136,24 @@ public function testStartPrintsOptionInfoWithSingularForOneProcess(): void public function testAddSuiteAddsFunctionCountToTotalTestCases(): void { $suite = new Suite('/path', [ - new TestMethod('funcOne', [], false), - new TestMethod('funcTwo', [], false), - ], false); + new TestMethod('funcOne', [], false, TMP_DIR), + new TestMethod('funcTwo', [], false, TMP_DIR), + ], false, TMP_DIR); $this->printer->addTest($suite); - static::assertEquals(2, $this->printer->getTotalCases()); + static::assertSame(2, $this->printer->getTotalCases()); } public function testAddTestMethodIncrementsCountByOne(): void { - $method = new TestMethod('/path', ['testThisMethod'], false); + $method = new TestMethod('/path', ['testThisMethod'], false, TMP_DIR); $this->printer->addTest($method); - static::assertEquals(1, $this->printer->getTotalCases()); + static::assertSame(1, $this->printer->getTotalCases()); } public function testGetHeader(): void { - $this->printer->addTest($this->errorSuite) - ->addTest($this->failureSuite); + $this->printer->addTest($this->errorSuite); + $this->printer->addTest($this->failureSuite); $this->prepareReaders(); @@ -176,15 +162,15 @@ public function testGetHeader(): void static::assertMatchesRegularExpression( "/\n\nTime: ([.:]?[0-9]{1,3})+ ?" . '(minute|minutes|second|seconds|ms|)?,' . - " Memory:[\s][0-9]+([.][0-9]{1,2})? ?M[Bb]\n\n/", + " Memory:[\\s][0-9]+([.][0-9]{1,2})? ?M[Bb]\n\n/", $header ); } public function testGetErrorsSingleError(): void { - $this->printer->addTest($this->errorSuite) - ->addTest($this->failureSuite); + $this->printer->addTest($this->errorSuite); + $this->printer->addTest($this->failureSuite); $this->prepareReaders(); @@ -195,13 +181,13 @@ public function testGetErrorsSingleError(): void $eq .= "Exception: Error!!!\n\n"; $eq .= "/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithErrorTest.php:12\n"; - static::assertEquals($eq, $errors); + static::assertSame($eq, $errors); } public function testGetErrorsMultipleErrors(): void { - $this->printer->addTest($this->errorSuite) - ->addTest($this->otherErrorSuite); + $this->printer->addTest($this->errorSuite); + $this->printer->addTest($this->otherErrorSuite); $this->prepareReaders(); @@ -215,7 +201,7 @@ public function testGetErrorsMultipleErrors(): void $eq .= "Exception: Another Error!!!\n\n"; $eq .= "/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithOtherErrorTest.php:12\n"; - static::assertEquals($eq, $errors); + static::assertSame($eq, $errors); } public function testGetFailures(): void @@ -226,30 +212,33 @@ public function testGetFailures(): void $failures = $this->printer->getFailures(); - $eq = "There were 2 failures:\n\n"; - $eq .= "1) UnitTestWithClassAnnotationTest::testFalsehood\n"; + $eq = "There were 3 failures:\n\n"; + $eq .= "1) Fixtures\\Tests\\UnitTestWithClassAnnotationTest::testFalsehood\n"; + $eq .= "Failed asserting that true is false.\n\n"; + $eq .= "/home/brian/Projects/parallel-phpunit/test/fixtures/failing-tests/UnitTestWithClassAnnotationTest.php:32\n"; + $eq .= "\n2) UnitTestWithErrorTest::testFalsehood\n"; $eq .= "Failed asserting that true is false.\n\n"; - $eq .= "/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithClassAnnotationTest.php:20\n"; - $eq .= "\n2) UnitTestWithMethodAnnotationsTest::testFalsehood\n"; + $eq .= "/home/brian/Projects/parallel-phpunit/test/fixtures/failing-tests/UnitTestWithMethodAnnotationsTest.php:20\n"; + $eq .= "\n3) UnitTestWithMethodAnnotationsTest::testFalsehood\n"; $eq .= "Failed asserting that true is false.\n\n"; - $eq .= "/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithMethodAnnotationsTest.php:18\n"; + $eq .= "/home/brian/Projects/parallel-phpunit/test/fixtures/failing-tests/UnitTestWithMethodAnnotationsTest.php:20\n"; - static::assertEquals($eq, $failures); + static::assertSame($eq, $failures); } public function testGetFooterWithFailures(): void { - $this->printer->addTest($this->errorSuite) - ->addTest($this->mixedSuite); + $this->printer->addTest($this->errorSuite); + $this->printer->addTest($this->mixedSuite); $this->prepareReaders(); $footer = $this->printer->getFooter(); $eq = "\nFAILURES!\n"; - $eq .= "Tests: 8, Assertions: 6, Failures: 2, Errors: 2.\n"; + $eq .= "Tests: 20, Assertions: 10, Failures: 3, Errors: 4.\n"; - static::assertEquals($eq, $footer); + static::assertSame($eq, $footer); } public function testGetFooterWithSuccess(): void @@ -262,7 +251,7 @@ public function testGetFooterWithSuccess(): void $eq = "OK (3 tests, 3 assertions)\n"; - static::assertEquals($eq, $footer); + static::assertSame($eq, $footer); } public function testPrintFeedbackForMixed(): void @@ -270,7 +259,7 @@ public function testPrintFeedbackForMixed(): void $this->printer->addTest($this->mixedSuite); $this->printer->printFeedback($this->mixedSuite); $contents = $this->output->fetch(); - static::assertEquals('.F.E.F.', $contents); + static::assertSame('.F..E.F.WSSE.F.WSSE', $contents); } public function testPrintFeedbackForMoreThan100Suites(): void @@ -307,7 +296,7 @@ public function testPrintFeedbackForMoreThan100Suites(): void $expected .= '.'; } - static::assertEquals($expected, $feedback); + static::assertSame($expected, $feedback); } public function testResultPrinterAdjustsTotalCountForDataProviders(): void @@ -344,7 +333,75 @@ public function testResultPrinterAdjustsTotalCountForDataProviders(): void $expected .= '.'; } - static::assertEquals($expected, $feedback); + static::assertSame($expected, $feedback); + } + + public function testColors(): void + { + $this->options = $this->createOptionsFromArgv(['--colors' => true]); + $this->printer = new ResultPrinter($this->interpreter, $this->output, $this->options); + $this->printer->addTest($this->mixedSuite); + + $this->printer->start(); + $this->printer->printFeedback($this->mixedSuite); + $this->printer->printResults(); + + static::assertStringContainsString('FAILURES', $this->output->fetch()); + } + + public function testColorsForSkipped(): void + { + $this->options = $this->createOptionsFromArgv(['--colors' => true]); + $this->printer = new ResultPrinter($this->interpreter, $this->output, $this->options); + $this->printer->addTest($this->skipped); + + $this->printer->start(); + $this->printer->printFeedback($this->skipped); + $this->printer->printResults(); + + static::assertStringContainsString('OK', $this->output->fetch()); + } + + public function testColorsForPassing(): void + { + $this->options = $this->createOptionsFromArgv(['--colors' => true]); + $this->printer = new ResultPrinter($this->interpreter, $this->output, $this->options); + $this->printer->addTest($this->passingSuite); + + $this->printer->start(); + $this->printer->printFeedback($this->passingSuite); + $this->printer->printResults(); + + static::assertStringContainsString('OK', $this->output->fetch()); + } + + /** + * This test ensure Code Coverage over printSkippedAndIncomplete + * but the real case for this test case is missing at the time of writing + * + * @see \ParaTest\Runners\PHPUnit\ResultPrinter::printSkippedAndIncomplete + */ + public function testParallelSuiteProgressOverhead(): void + { + $suite = $this->getSuiteWithResult('mixed-results.xml', 100); + $this->printer->addTest($suite); + + $this->printer->start(); + $this->printer->printFeedback($suite); + $this->printer->printResults(); + + static::assertStringContainsString('FAILURES', $this->output->fetch()); + } + + public function testEmptyLogFileRaiseExceptionWithLastCommand(): void + { + $test = new ExecutableTestChild(uniqid(), false, TMP_DIR); + $test->setLastCommand(uniqid()); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches(sprintf('/%s/', $test->getLastCommand())); + + $this->printer->printFeedback($test); } private function getStartOutput(): string diff --git a/test/Unit/Runners/PHPUnit/RunnerTest.php b/test/Unit/Runners/PHPUnit/RunnerTest.php index 05ce0e54..c487519f 100644 --- a/test/Unit/Runners/PHPUnit/RunnerTest.php +++ b/test/Unit/Runners/PHPUnit/RunnerTest.php @@ -4,133 +4,28 @@ namespace ParaTest\Tests\Unit\Runners\PHPUnit; -use ParaTest\Logging\LogInterpreter; -use ParaTest\Runners\PHPUnit\ResultPrinter; -use ParaTest\Runners\PHPUnit\Runner; -use ParaTest\Tests\TestBase; -use PHPUnit\TextUI\XmlConfiguration\Loader; -use Symfony\Component\Console\Output\BufferedOutput; - -use function getcwd; -use function uniqid; - -final class RunnerTest extends TestBase +use function preg_match; + +/** + * @covers \ParaTest\Runners\PHPUnit\BaseRunner + * @covers \ParaTest\Runners\PHPUnit\Runner + * @covers \ParaTest\Runners\PHPUnit\Worker\RunnerWorker + */ +final class RunnerTest extends RunnerTestCase { - /** @var Runner */ - private $runner; - /** @var BufferedOutput */ - private $output; - - public function setUp(): void + public function testStopOnFailureEndsRunBeforeWholeTestSuite(): void { - $this->output = new BufferedOutput(); - $this->runner = new Runner($this->createOptionsFromArgv([]), $this->output); - } + $this->bareOptions['--path'] = $this->fixture('failing-tests'); + $runnerResult = $this->runRunner(); - public function testConstructor(): void - { - $opts = [ - '--processes' => 4, - '--path' => FIXTURES . DS . 'tests', - '--bootstrap' => 'hello', - '--functional' => true, - ]; - $runner = new Runner($this->createOptionsFromArgv($opts), $this->output); - $options = $this->getObjectValue($runner, 'options'); + $regexp = '/Tests: \d+, Assertions: \d+, Failures: \d+, Errors: \d+\./'; + static::assertSame(1, preg_match($regexp, $runnerResult->getOutput(), $matchesOnFullRun)); - static::assertEquals(4, $options->processes()); - static::assertEquals(FIXTURES . DS . 'tests', $options->path()); - static::assertEquals([], $this->getObjectValue($runner, 'pending')); - static::assertEquals([], $this->getObjectValue($runner, 'running')); - static::assertEquals(-1, $this->getObjectValue($runner, 'exitcode')); - static::assertTrue($options->functional()); - //filter out processes and path and phpunit - $config = (new Loader())->load(getcwd() . DS . 'phpunit.xml.dist'); - static::assertEquals(['bootstrap' => 'hello', 'configuration' => $config->filename()], $options->filtered()); - static::assertInstanceOf(LogInterpreter::class, $this->getObjectValue($runner, 'interpreter')); - static::assertInstanceOf(ResultPrinter::class, $this->getObjectValue($runner, 'printer')); - } + $this->bareOptions['--stop-on-failure'] = true; + $runnerResult = $this->runRunner(); - public function testGetExitCode(): void - { - static::assertEquals(-1, $this->runner->getExitCode()); - } - - public function testConstructorAssignsTokens(): void - { - $opts = [ - '--processes' => 4, - '--path' => FIXTURES . DS . 'tests', - '--bootstrap' => 'hello', - '--functional' => true, - ]; - $runner = new Runner($this->createOptionsFromArgv($opts), $this->output); - $tokens = $this->getObjectValue($runner, 'tokens'); - static::assertCount(4, $tokens); - } - - public function testGetsNextAvailableTokenReturnsTokenIdentifier(): void - { - $tokens = [ - 0 => ['token' => 0, 'unique' => uniqid(), 'available' => false], - 1 => ['token' => 1, 'unique' => uniqid(), 'available' => false], - 2 => ['token' => 2, 'unique' => uniqid(), 'available' => true], - 3 => ['token' => 3, 'unique' => uniqid(), 'available' => false], - ]; - $opts = [ - '--processes' => 4, - '--path' => FIXTURES . DS . 'tests', - '--bootstrap' => 'hello', - '--functional' => true, - ]; - $runner = new Runner($this->createOptionsFromArgv($opts), $this->output); - $this->setObjectValue($runner, 'tokens', $tokens); - - $tokenData = $this->call($runner, 'getNextAvailableToken'); - static::assertEquals(2, $tokenData['token']); - } - - public function testGetNextAvailableTokenReturnsFalseWhenNoTokensAreAvailable(): void - { - $tokens = [ - 0 => ['token' => 0, 'unique' => uniqid(), 'available' => false], - 1 => ['token' => 1, 'unique' => uniqid(), 'available' => false], - 2 => ['token' => 2, 'unique' => uniqid(), 'available' => false], - 3 => ['token' => 3, 'unique' => uniqid(), 'available' => false], - ]; - $opts = [ - '--processes' => 4, - '--path' => FIXTURES . DS . 'tests', - '--bootstrap' => 'hello', - '--functional' => true, - ]; - $runner = new Runner($this->createOptionsFromArgv($opts), $this->output); - $this->setObjectValue($runner, 'tokens', $tokens); - - $tokenData = $this->call($runner, 'getNextAvailableToken'); - static::assertFalse($tokenData); - } - - public function testReleaseTokenMakesTokenAvailable(): void - { - $tokens = [ - 0 => ['token' => 0, 'unique' => uniqid(), 'available' => false], - 1 => ['token' => 1, 'unique' => uniqid(), 'available' => false], - 2 => ['token' => 2, 'unique' => uniqid(), 'available' => false], - 3 => ['token' => 3, 'unique' => uniqid(), 'available' => false], - ]; - $opts = [ - '--processes' => 4, - '--path' => FIXTURES . DS . 'tests', - '--bootstrap' => 'hello', - '--functional' => true, - ]; - $runner = new Runner($this->createOptionsFromArgv($opts), $this->output); - $this->setObjectValue($runner, 'tokens', $tokens); + static::assertSame(1, preg_match($regexp, $runnerResult->getOutput(), $matchesOnPartialRun)); - static::assertFalse($tokens[1]['available']); - $this->call($runner, 'releaseToken', 1); - $tokens = $this->getObjectValue($runner, 'tokens'); - static::assertTrue($tokens[1]['available']); + static::assertNotEquals($matchesOnFullRun[0], $matchesOnPartialRun[0]); } } diff --git a/test/Unit/Runners/PHPUnit/RunnerTestCase.php b/test/Unit/Runners/PHPUnit/RunnerTestCase.php new file mode 100644 index 00000000..36381811 --- /dev/null +++ b/test/Unit/Runners/PHPUnit/RunnerTestCase.php @@ -0,0 +1,127 @@ +bareOptions['--path'] = $this->fixture('passing-tests' . DS . 'GroupsTest.php'); + $this->bareOptions['--coverage-php'] = TMP_DIR . DS . uniqid('result_'); + + $this->assertTestsPassed($this->runRunner()); + + $coveragePhp = include $this->bareOptions['--coverage-php']; + static::assertInstanceOf(CodeCoverage::class, $coveragePhp); + } + + final public function testRunningFewerTestsThanTheWorkersIsPossible(): void + { + $this->bareOptions['--path'] = $this->fixture('passing-tests' . DS . 'GroupsTest.php'); + $this->bareOptions['--processes'] = 2; + + $this->assertTestsPassed($this->runRunner()); + } + + final public function testExitCodes(): void + { + $this->bareOptions['--path'] = $this->fixture('wrapper-runner-exit-code-tests' . DS . 'ErrorTest.php'); + $runnerResult = $this->runRunner(); + + static::assertStringContainsString('Tests: 1', $runnerResult->getOutput()); + static::assertStringContainsString('Failures: 0', $runnerResult->getOutput()); + static::assertStringContainsString('Errors: 1', $runnerResult->getOutput()); + static::assertEquals(TestRunner::EXCEPTION_EXIT, $runnerResult->getExitCode()); + + $this->bareOptions['--path'] = $this->fixture('wrapper-runner-exit-code-tests' . DS . 'FailureTest.php'); + $runnerResult = $this->runRunner(); + + static::assertStringContainsString('Tests: 1', $runnerResult->getOutput()); + static::assertStringContainsString('Failures: 1', $runnerResult->getOutput()); + static::assertStringContainsString('Errors: 0', $runnerResult->getOutput()); + static::assertEquals(TestRunner::FAILURE_EXIT, $runnerResult->getExitCode()); + + $this->bareOptions['--path'] = $this->fixture('wrapper-runner-exit-code-tests' . DS . 'SuccessTest.php'); + $runnerResult = $this->runRunner(); + + static::assertStringContainsString('OK (1 test, 1 assertion)', $runnerResult->getOutput()); + static::assertEquals(TestRunner::SUCCESS_EXIT, $runnerResult->getExitCode()); + + $this->bareOptions['--path'] = $this->fixture('wrapper-runner-exit-code-tests'); + $runnerResult = $this->runRunner(); + + static::assertStringContainsString('Tests: 3', $runnerResult->getOutput()); + static::assertStringContainsString('Failures: 1', $runnerResult->getOutput()); + static::assertStringContainsString('Errors: 1', $runnerResult->getOutput()); + static::assertEquals(TestRunner::EXCEPTION_EXIT, $runnerResult->getExitCode()); + } + + final public function testParallelSuiteOption(): void + { + $this->bareOptions = array_merge($this->bareOptions, [ + '--configuration' => $this->fixture('phpunit-parallel-suite.xml'), + '--parallel-suite' => true, + '--processes' => 2, + '--verbose' => 1, + '--whitelist' => $this->fixture('parallel-suite'), + ]); + + $this->assertTestsPassed($this->runRunner()); + } + + final public function testRaiseExceptionWhenATestCallsExitSilently(): void + { + $this->bareOptions['--path'] = $this->fixture('exit-tests' . DS . 'UnitTestThatExitsSilentlyTest.php'); + $this->bareOptions['--coverage-php'] = TMP_DIR . DS . uniqid('result_'); + + $this->expectException(WorkerCrashedException::class); + $this->expectExceptionMessageMatches('/UnitTestThatExitsSilentlyTest/'); + + $this->runRunner(); + } + + final public function testRaiseExceptionWhenATestCallsExitLoudly(): void + { + $this->bareOptions['--path'] = $this->fixture('exit-tests' . DS . 'UnitTestThatExitsLoudlyTest.php'); + $this->bareOptions['--coverage-php'] = TMP_DIR . DS . uniqid('result_'); + + $this->expectException(WorkerCrashedException::class); + $this->expectExceptionMessageMatches('/UnitTestThatExitsLoudlyTest/'); + + $this->runRunner(); + } + + final public function testPassthrus(): void + { + $this->bareOptions['--path'] = $this->fixture('passthru-tests' . DS . 'PassthruTest.php'); + + $runnerResult = $this->runRunner(); + static::assertSame(TestRunner::FAILURE_EXIT, $runnerResult->getExitCode()); + + $this->bareOptions['--passthru-php'] = sprintf("'-d' 'highlight.comment=%s'", self::PASSTHRU_PHP_CUSTOM); + $this->bareOptions['--passthru'] = sprintf("'-d' 'highlight.string=%s'", self::PASSTHRU_PHPUNIT_CUSTOM); + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + $this->bareOptions['--passthru'] = str_replace('\'', '"', (string) $this->bareOptions['--passthru']); + $this->bareOptions['--passthru-php'] = str_replace('\'', '"', (string) $this->bareOptions['--passthru-php']); + } + + $runnerResult = $this->runRunner(); + $this->assertTestsPassed($runnerResult); + } +} diff --git a/test/Unit/Runners/PHPUnit/SqliteRunnerTest.php b/test/Unit/Runners/PHPUnit/SqliteRunnerTest.php new file mode 100644 index 00000000..3d7fe92b --- /dev/null +++ b/test/Unit/Runners/PHPUnit/SqliteRunnerTest.php @@ -0,0 +1,20 @@ +getObjectValue($loader, 'options')); } - public function testOptionsCanBeNull(): void - { - $loader = new SuiteLoader(); - static::assertNull($this->getObjectValue($loader, 'options')); - } - public function testLoadThrowsExceptionWithInvalidPath(): void { + $loader = new SuiteLoader($this->createOptionsFromArgv(['--path' => '/path/to/nowhere'])); + $this->expectException(RuntimeException::class); - $loader = new SuiteLoader(); - $loader->load('/path/to/nowhere'); + $loader->load(); } public function testLoadBarePathWithNoPathAndNoConfiguration(): void { + $loader = new SuiteLoader($this->createOptionsFromArgv([], __DIR__)); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('No path or configuration provided (tests must end with Test.php)'); - $loader = new SuiteLoader(); $loader->load(); } @@ -52,7 +52,6 @@ public function testLoadTestsuiteFileFromConfig(): void { $options = $this->createOptionsFromArgv([ '--configuration' => $this->fixture('phpunit-file.xml'), - '--testsuite' => 'ParaTest Fixtures', ]); $loader = new SuiteLoader($options); $loader->load(); @@ -66,7 +65,6 @@ public function testLoadTestsuiteFilesFromConfigWhileIgnoringExcludeTag(): void { $options = $this->createOptionsFromArgv([ '--configuration' => $this->fixture('phpunit-excluded-including-file.xml'), - '--testsuite' => 'ParaTest Fixtures', ]); $loader = new SuiteLoader($options); $loader->load(); @@ -80,7 +78,6 @@ public function testLoadTestsuiteFilesFromDirFromConfigWhileRespectingExcludeTag { $options = $this->createOptionsFromArgv([ '--configuration' => $this->fixture('phpunit-excluded-including-dir.xml'), - '--testsuite' => 'ParaTest Fixtures', ]); $loader = new SuiteLoader($options); $loader->load(); @@ -94,21 +91,19 @@ public function testLoadTestsuiteFilesFromConfig(): void { $options = $this->createOptionsFromArgv([ '--configuration' => $this->fixture('phpunit-multifile.xml'), - '--testsuite' => 'ParaTest Fixtures', + '--group' => 'fixtures,group4', ]); $loader = new SuiteLoader($options); $loader->load(); $files = $this->getObjectValue($loader, 'files'); - $expected = 2; - static::assertCount($expected, $files); + static::assertCount(3, $files); } public function testLoadTestsuiteWithDirectory(): void { $options = $this->createOptionsFromArgv([ '--configuration' => $this->fixture('phpunit-passing.xml'), - '--testsuite' => 'ParaTest Fixtures', ]); $loader = new SuiteLoader($options); $loader->load(); @@ -122,7 +117,6 @@ public function testLoadTestsuiteWithDirectories(): void { $options = $this->createOptionsFromArgv([ '--configuration' => $this->fixture('phpunit-multidir.xml'), - '--testsuite' => 'ParaTest Fixtures', ]); $loader = new SuiteLoader($options); $loader->load(); @@ -137,7 +131,6 @@ public function testLoadTestsuiteWithFilesDirsMixed(): void { $options = $this->createOptionsFromArgv([ '--configuration' => $this->fixture('phpunit-files-dirs-mix.xml'), - '--testsuite' => 'ParaTest Fixtures', ]); $loader = new SuiteLoader($options); $loader->load(); @@ -151,7 +144,6 @@ public function testLoadTestsuiteWithDuplicateFilesDirMixed(): void { $options = $this->createOptionsFromArgv([ '--configuration' => $this->fixture('phpunit-files-dirs-mix-duplicates.xml'), - '--testsuite' => 'ParaTest Fixtures', ]); $loader = new SuiteLoader($options); $loader->load(); @@ -161,6 +153,20 @@ public function testLoadTestsuiteWithDuplicateFilesDirMixed(): void static::assertCount($expected, $files); } + public function testLoadSomeTestsuite(): void + { + $options = $this->createOptionsFromArgv([ + '--configuration' => $this->fixture('phpunit-parallel-suite.xml'), + '--testsuite' => 'Suite 1', + ]); + $loader = new SuiteLoader($options); + $loader->load(); + $files = $this->getObjectValue($loader, 'files'); + + $expected = count($this->findTests(FIXTURES . DS . 'parallel-suite' . DS . 'One')); + static::assertCount($expected, $files); + } + public function testLoadSuiteFromConfig(): void { $options = $this->createOptionsFromArgv([ @@ -214,8 +220,8 @@ public function testLoadFileGetsPathOfFile(): void */ private function getLoadedPaths(string $path, ?SuiteLoader $loader = null): array { - $loader = $loader ?? new SuiteLoader(); - $loader->load($path); + $loader = $loader ?? new SuiteLoader($this->createOptionsFromArgv(['--path' => $path])); + $loader->load(); $loaded = $this->getObjectValue($loader, 'loadedSuites'); return array_keys($loaded); @@ -236,8 +242,8 @@ public function testLoadDirGetsPathOfAllTestsWithKeys(): array $fixturePath = $this->fixture('passing-tests'); $files = $this->findTests($fixturePath); - $loader = new SuiteLoader(); - $loader->load($fixturePath); + $loader = new SuiteLoader($this->createOptionsFromArgv(['--path' => $fixturePath])); + $loader->load(); $loaded = $this->getObjectValue($loader, 'loadedSuites'); foreach ($loaded as $path => $test) { static::assertContains($path, $files); @@ -276,7 +282,7 @@ private function suiteByPath(string $path, array $paraSuites): Suite } } - throw new RuntimeException("Suite $path not found."); + throw new RuntimeException("Suite {$path} not found."); } /** @@ -293,10 +299,12 @@ public function testSecondParallelSuiteHasCorrectFunctions(array $paraSuites): v public function testGetTestMethodsOnlyReturnsMethodsOfGroupIfOptionIsSpecified(): void { - $options = $this->createOptionsFromArgv(['--group' => 'group1']); - $loader = new SuiteLoader($options); - $groupsTest = $this->fixture('passing-tests/GroupsTest.php'); - $loader->load($groupsTest); + $options = $this->createOptionsFromArgv([ + '--group' => 'group1', + '--path' => $this->fixture('passing-tests/GroupsTest.php'), + ]); + $loader = new SuiteLoader($options); + $loader->load(); $methods = $loader->getTestMethods(); static::assertCount(2, $methods); static::assertEquals('testTruth', $methods[0]->getName()); @@ -305,10 +313,12 @@ public function testGetTestMethodsOnlyReturnsMethodsOfGroupIfOptionIsSpecified() public function testGetTestMethodsOnlyReturnsMethodsOfClassGroup(): void { - $options = $this->createOptionsFromArgv(['--group' => 'group4']); - $loader = new SuiteLoader($options); - $groupsTest = $this->fixture('passing-tests/GroupsTest.php'); - $loader->load($groupsTest); + $options = $this->createOptionsFromArgv([ + '--group' => 'group4', + '--path' => $this->fixture('passing-tests/GroupsTest.php'), + ]); + $loader = new SuiteLoader($options); + $loader->load(); $methods = $loader->getTestMethods(); static::assertCount(1, $loader->getSuites()); static::assertCount(5, $methods); @@ -316,27 +326,33 @@ public function testGetTestMethodsOnlyReturnsMethodsOfClassGroup(): void public function testGetSuitesForNonMatchingGroups(): void { - $options = $this->createOptionsFromArgv(['--group' => 'non-existent']); - $loader = new SuiteLoader($options); - $groupsTest = $this->fixture('passing-tests/GroupsTest.php'); - $loader->load($groupsTest); + $options = $this->createOptionsFromArgv([ + '--group' => 'non-existent', + '--path' => $this->fixture('passing-tests/GroupsTest.php'), + ]); + $loader = new SuiteLoader($options); + $loader->load(); static::assertCount(0, $loader->getSuites()); static::assertCount(0, $loader->getTestMethods()); } public function testLoadIgnoresFilesWithoutClasses(): void { - $loader = new SuiteLoader(); - $fileWithoutClass = $this->fixture('special-classes/FileWithoutClass.php'); - $loader->load($fileWithoutClass); + $options = $this->createOptionsFromArgv([ + '--group' => 'non-existent', + '--path' => $this->fixture('special-classes/FileWithoutClass.php'), + ]); + $loader = new SuiteLoader($options); + $loader->load(); static::assertCount(0, $loader->getTestMethods()); } public function testExecutableTestsForFunctionalModeUse(): void { - $path = $this->fixture('passing-tests/DependsOnChain.php'); - $loader = new SuiteLoader(); - $loader->load($path); + $loader = new SuiteLoader($this->createOptionsFromArgv([ + '--path' => $this->fixture('passing-tests/DependsOnChain.php'), + ])); + $loader->load(); $tests = $loader->getTestMethods(); static::assertCount(2, $tests); $testMethod = $tests[0]; @@ -344,4 +360,43 @@ public function testExecutableTestsForFunctionalModeUse(): void $testMethod = $tests[1]; static::assertEquals('testTwoA|testTwoBDependsOnA', $testMethod->getName()); } + + public function testParallelSuite(): void + { + $loader = new SuiteLoader($this->createOptionsFromArgv([ + '--configuration' => $this->fixture('phpunit-parallel-suite.xml'), + '--parallel-suite' => true, + '--processes' => 2, + ])); + $loader->load(); + + $suites = $loader->getSuites(); + + static::assertCount(2, $suites); + foreach ($suites as $suite) { + static::assertInstanceOf(FullSuite::class, $suite); + } + } + + public function testBatches(): void + { + $options = $this->createOptionsFromArgv([ + '--bootstrap' => BOOTSTRAP, + '--path' => $this->fixture('dataprovider-tests/DataProviderTest.php'), + '--filter' => 'testNumericDataProvider1000', + '--functional' => true, + '--max-batch-size' => 50, + ], __DIR__); + $loader = new SuiteLoader($options); + $loader->load(); + + $suites = $loader->getSuites(); + + static::assertCount(1, $suites); + + $suite = array_shift($suites); + + static::assertInstanceOf(Suite::class, $suite); + static::assertCount(20, $suite->getFunctions()); + } } diff --git a/test/Unit/Runners/PHPUnit/SuiteTest.php b/test/Unit/Runners/PHPUnit/SuiteTest.php new file mode 100644 index 00000000..cfbfcfc2 --- /dev/null +++ b/test/Unit/Runners/PHPUnit/SuiteTest.php @@ -0,0 +1,33 @@ +commandArguments(uniqid(), [], null); + + static::assertNotContains('--filter', $commandArguments); + static::assertContains($file, $commandArguments); + static::assertSame($testMethods, $suite->getFunctions()); + static::assertSame(2, $suite->getTestCount()); + } +} diff --git a/test/Unit/Runners/PHPUnit/TestMethodTest.php b/test/Unit/Runners/PHPUnit/TestMethodTest.php index 3e1e5fb6..2a80023a 100644 --- a/test/Unit/Runners/PHPUnit/TestMethodTest.php +++ b/test/Unit/Runners/PHPUnit/TestMethodTest.php @@ -7,11 +7,24 @@ use ParaTest\Runners\PHPUnit\TestMethod; use ParaTest\Tests\TestBase; +use function uniqid; + +/** + * @covers \ParaTest\Runners\PHPUnit\TestMethod + */ final class TestMethodTest extends TestBase { public function testConstructor(): void { - $testMethod = new TestMethod('pathToFile', ['methodName'], false); - static::assertEquals('pathToFile', $this->getObjectValue($testMethod, 'path')); + $file = uniqid('pathToFile_'); + $testMethod = new TestMethod($file, ['method1', 'method2'], false, TMP_DIR); + + $commandArguments = $testMethod->commandArguments(uniqid(), [], null); + + static::assertContains('--filter', $commandArguments); + static::assertContains($file, $commandArguments); + static::assertStringContainsString('method1', $testMethod->getName()); + static::assertStringContainsString('method2', $testMethod->getName()); + static::assertSame(2, $testMethod->getTestCount()); } } diff --git a/test/Unit/Runners/PHPUnit/WrapperRunnerOnWindowsTest.php b/test/Unit/Runners/PHPUnit/WrapperRunnerOnWindowsTest.php new file mode 100644 index 00000000..fbcd6bc8 --- /dev/null +++ b/test/Unit/Runners/PHPUnit/WrapperRunnerOnWindowsTest.php @@ -0,0 +1,27 @@ +createOptionsFromArgv([]); + $output = new BufferedOutput(); + + $this->expectException(RuntimeException::class); + + new WrapperRunner($options, $output); + } +} diff --git a/test/Unit/Runners/PHPUnit/WrapperRunnerTest.php b/test/Unit/Runners/PHPUnit/WrapperRunnerTest.php index cdd35951..01d3ec9d 100644 --- a/test/Unit/Runners/PHPUnit/WrapperRunnerTest.php +++ b/test/Unit/Runners/PHPUnit/WrapperRunnerTest.php @@ -4,23 +4,28 @@ namespace ParaTest\Tests\Unit\Runners\PHPUnit; +use InvalidArgumentException; use ParaTest\Runners\PHPUnit\WrapperRunner; -use ParaTest\Tests\TestBase; -use RuntimeException; -use Symfony\Component\Console\Output\BufferedOutput; -final class WrapperRunnerTest extends TestBase +/** + * @requires OSFAMILY Linux + * @covers \ParaTest\Runners\PHPUnit\BaseWrapperRunner + * @covers \ParaTest\Runners\PHPUnit\WrapperRunner + * @covers \ParaTest\Runners\PHPUnit\Worker\BaseWorker + * @covers \ParaTest\Runners\PHPUnit\Worker\WrapperWorker + */ +final class WrapperRunnerTest extends RunnerTestCase { - /** - * @requires OSFAMILY Windows - */ - public function testWrapperRunnerCannotBeUsedOnWindows(): void + /** {@inheritdoc } */ + protected $runnerClass = WrapperRunner::class; + + public function testWrapperRunnerNotAvailableInFunctionalMode(): void { - $options = $this->createOptionsFromArgv([]); - $output = new BufferedOutput(); + $this->bareOptions['--path'] = $this->fixture('passing-tests' . DS . 'GroupsTest.php'); + $this->bareOptions['--functional'] = true; - $this->expectException(RuntimeException::class); + $this->expectException(InvalidArgumentException::class); - new WrapperRunner($options, $output); + $this->runRunner(); } } diff --git a/test/Unit/Util/StrTest.php b/test/Unit/Util/StrTest.php index 2957b4c2..658510df 100644 --- a/test/Unit/Util/StrTest.php +++ b/test/Unit/Util/StrTest.php @@ -9,6 +9,9 @@ use function array_values; +/** + * @covers \ParaTest\Util\Str + */ final class StrTest extends TestCase { /** diff --git a/test/bootstrap.php b/test/bootstrap.php index 1ee133d2..2b2295ea 100644 --- a/test/bootstrap.php +++ b/test/bootstrap.php @@ -4,4 +4,3 @@ require __DIR__ . DIRECTORY_SEPARATOR . 'constants.php'; require PARATEST_ROOT . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php'; -require __DIR__ . DIRECTORY_SEPARATOR . 'TestBase.php'; diff --git a/test/constants.php b/test/constants.php index 917bfd2f..c654ab59 100644 --- a/test/constants.php +++ b/test/constants.php @@ -8,6 +8,7 @@ //TEST CONSTANTS define('FIXTURES', __DIR__ . DS . 'fixtures'); +define('TMP_DIR', __DIR__ . DS . 'tmp'); define('PARATEST_ROOT', dirname(__DIR__)); define('PARA_BINARY', PARATEST_ROOT . DS . 'bin' . DS . 'paratest'); define('PHPUNIT', PARATEST_ROOT . DS . 'vendor' . DS . 'phpunit' . DS . 'phpunit' . DS . 'phpunit'); diff --git a/test/fixtures/exit-tests/UnitTestThatExitsLoudlyTest.php b/test/fixtures/exit-tests/UnitTestThatExitsLoudlyTest.php new file mode 100644 index 00000000..3b46f4dd --- /dev/null +++ b/test/fixtures/exit-tests/UnitTestThatExitsLoudlyTest.php @@ -0,0 +1,11 @@ +assertEquals(5, sizeof($elems)); } + + /** + * @group fixtures + */ + public function testWarning(): void + { + $this->addWarning(uniqid()); + } + + /** + * @group fixtures + */ + public function testSkipped(): void + { + $this->markTestSkipped(); + } + + /** + * @group fixtures + */ + public function testIncomplete(): void + { + $this->markTestIncomplete(); + } + + /** + * @group fixtures + */ + public function testRisky(): void + { + $this->markAsRisky(); + } } diff --git a/test/fixtures/parallel-suite/ParallelBase.php b/test/fixtures/parallel-suite/ParallelBase.php index fce110b9..7bdbb683 100644 --- a/test/fixtures/parallel-suite/ParallelBase.php +++ b/test/fixtures/parallel-suite/ParallelBase.php @@ -14,7 +14,7 @@ abstract class ParallelBase extends TestCase final public function testToken(): void { $refClass = new ReflectionClass(static::class); - $file = sys_get_temp_dir() . DS . 'parallel-suite' . DS . 'token_' . str_replace(['\\', '/'], '_', $refClass->getNamespaceName()); + $file = TMP_DIR . DS . 'token_' . str_replace(['\\', '/'], '_', $refClass->getNamespaceName()); $token = getenv('TEST_TOKEN'); static::assertIsString($token); diff --git a/test/fixtures/passthru-tests/PassthruTest.php b/test/fixtures/passthru-tests/PassthruTest.php new file mode 100644 index 00000000..12d9e6cf --- /dev/null +++ b/test/fixtures/passthru-tests/PassthruTest.php @@ -0,0 +1,14 @@ + + + + + ./fatal-tests/ + + + diff --git a/test/fixtures/phpunit-multifile.xml b/test/fixtures/phpunit-multifile.xml index 5f21791a..75c7518f 100644 --- a/test/fixtures/phpunit-multifile.xml +++ b/test/fixtures/phpunit-multifile.xml @@ -4,6 +4,7 @@ ./passing-tests/TestOfUnits.php ./passing-tests/GroupsTest.php + ./failing-tests/FailingTest.php diff --git a/test/fixtures/results/data-provider-with-special-chars.xml b/test/fixtures/results/data-provider-with-special-chars.xml index c8cf6178..97e0e82c 100644 --- a/test/fixtures/results/data-provider-with-special-chars.xml +++ b/test/fixtures/results/data-provider-with-special-chars.xml @@ -1,7 +1,7 @@ - - + + UnitTestWithDataProviderSpecialCharsTest::testIsItFalse with data set #0 ('—') Failed asserting that '—' is false. diff --git a/test/fixtures/results/mixed-results.xml b/test/fixtures/results/mixed-results.xml index b70af33c..a1c368eb 100644 --- a/test/fixtures/results/mixed-results.xml +++ b/test/fixtures/results/mixed-results.xml @@ -1,36 +1,76 @@ - - - - - UnitTestWithClassAnnotationTest::testFalsehood + + + + + Fixtures\Tests\UnitTestWithClassAnnotationTest::testFalsehood Failed asserting that true is false. -/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithClassAnnotationTest.php:20 +/home/brian/Projects/parallel-phpunit/test/fixtures/failing-tests/UnitTestWithClassAnnotationTest.php:32 - + + - - + + UnitTestWithErrorTest::testTruth Exception: Error!!! -/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithErrorTest.php:12 +/home/brian/Projects/parallel-phpunit/test/fixtures/failing-tests/UnitTestWithErrorTest.php:17 + + + + + UnitTestWithErrorTest::testFalsehood +Failed asserting that true is false. + +/home/brian/Projects/parallel-phpunit/test/fixtures/failing-tests/UnitTestWithMethodAnnotationsTest.php:20 + + + + + UnitTestWithErrorTest::testWarning +Function 1 deprecated + + + + + + + + + + Risky Test - - - + + + UnitTestWithMethodAnnotationsTest::testFalsehood Failed asserting that true is false. -/home/brian/Projects/parallel-phpunit/test/fixtures/tests/UnitTestWithMethodAnnotationsTest.php:18 +/home/brian/Projects/parallel-phpunit/test/fixtures/failing-tests/UnitTestWithMethodAnnotationsTest.php:20 - + + + UnitTestWithMethodAnnotationsTest::testWarning +Function 2 deprecated + + + + + + + + + + Risky Test + + diff --git a/test/fixtures/results/single-passing.xml b/test/fixtures/results/single-passing.xml index a1faf7ad..8c6446be 100644 --- a/test/fixtures/results/single-passing.xml +++ b/test/fixtures/results/single-passing.xml @@ -1,8 +1,8 @@ - - - - + + + + diff --git a/test/fixtures/results/single-skipped.xml b/test/fixtures/results/single-skipped.xml new file mode 100644 index 00000000..4f6969a5 --- /dev/null +++ b/test/fixtures/results/single-skipped.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/test/fixtures/special-classes/SomeNamespace/ParserTestClass.php b/test/fixtures/special-classes/SomeNamespace/ParserTestClass.php index 7533bc93..6f39c4bc 100644 --- a/test/fixtures/special-classes/SomeNamespace/ParserTestClass.php +++ b/test/fixtures/special-classes/SomeNamespace/ParserTestClass.php @@ -7,14 +7,18 @@ use PHPUnit\Framework\TestCase; // Test that it gives the class matching the file name priority. -final class SomeOtherClass extends TestCase +final class NonTestClass { } -final class ParserTestClass extends TestCase +final class SomeOtherClass extends TestCase { } final class AnotherClass extends TestCase { } + +final class ParserTestClass extends TestCase +{ +} diff --git a/test/tmp/.gitignore b/test/tmp/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/test/tmp/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file