From cabe4a83a07a24585958491e2f501b0dc2b89750 Mon Sep 17 00:00:00 2001 From: Isaac Leinweber Date: Wed, 19 Oct 2016 09:49:33 -0700 Subject: [PATCH 1/4] First pass at generating a jUnit report. --- src/Assert.php | 15 +++ src/Contract/Assert.php | 7 +- src/Report/Format/JUnit.php | 77 ++++++++++++ test/Report/JUnit.php | 232 ++++++++++++++++++++++++++++++++++++ 4 files changed, 328 insertions(+), 3 deletions(-) create mode 100644 src/Report/Format/JUnit.php create mode 100644 test/Report/JUnit.php diff --git a/src/Assert.php b/src/Assert.php index d75c4ae..b7eda30 100644 --- a/src/Assert.php +++ b/src/Assert.php @@ -107,4 +107,19 @@ public function skip( $l($skip); } } + + public function fail(string $message): void { + $stack = Util\Trace::generate(); + $traceItem = shape( + 'file' => $stack[0]['file'], + 'line' => $stack[0]['line'], + 'function' => $stack[1]['function'], + 'class' => $stack[1]['class'], + ); + $fail = + new Event\Failure($message, $traceItem, Util\Trace::findTestMethod()); + foreach ($this->failureListeners as $l) { + $l($fail); + } + } } diff --git a/src/Contract/Assert.php b/src/Contract/Assert.php index b0853ee..c04044a 100644 --- a/src/Contract/Assert.php +++ b/src/Contract/Assert.php @@ -16,8 +16,9 @@ public function mixed(mixed $context): Assertion\MixedAssertion; public function container( Container $context, ): Assertion\ContainerAssertion; - public function keyedContainer( - KeyedContainer $context, - ): Assertion\KeyedContainerAssertion; + public function keyedContainer( + KeyedContainer $context, + ): Assertion\KeyedContainerAssertion; public function skip(string $reason, ?TraceItem $traceItem = null): void; + public function fail(string $reason): void; } diff --git a/src/Report/Format/JUnit.php b/src/Report/Format/JUnit.php new file mode 100644 index 0000000..9de02fc --- /dev/null +++ b/src/Report/Format/JUnit.php @@ -0,0 +1,77 @@ +report = new SimpleXMLElement(''); + } + public function writeReport(Summary $summary): void { + foreach ($summary['suite summaries'] as $name => $suiteSummary) { + $this->report->addChild('testsuite') + |> $this->populateSuiteReport($name, $suiteSummary, $$); + } + fwrite($this->out, $this->report->asXML()); + } + + private function populateSuiteReport( + string $name, + SuiteSummary $summary, + SimpleXMLElement $report, + ): void { + $report->addAttribute('name', $name); + $this->addSuiteCounts($summary, $report); + foreach ($summary['test summaries'] as $testName => $testSummary) { + $report->addChild('testcase') + |> $this->populateTestReport($name, $testName, $testSummary, $$); + } + } + + private function addSuiteCounts( + SuiteSummary $summary, + SimpleXMLElement $report, + ): void { + $report->addAttribute('failures', $summary['fail count']); + $report->addAttribute('tests', $summary['test count']); + } + + private function populateTestReport( + string $suiteName, + string $name, + TestSummary $summary, + SimpleXMLElement $report, + ): void { + $report->addAttribute('name', $name); + $report->addAttribute('classname', $suiteName); + + switch ($summary['result']) { + case TestResult::Pass: + case TestResult::Error: + // Error is not used yet + // Nothing more to add when test passes + break; + case TestResult::Fail: + $event = $summary['fail event']; + $message = $event === null ? 'Unknown failure' : $event->getMessage(); + $failElement = $report->addChild('failure'); + $failElement->addAttribute('message', $message); + break; + case TestResult::Skip: + $report->addChild('skip'); + break; + } + } +} diff --git a/test/Report/JUnit.php b/test/Report/JUnit.php new file mode 100644 index 0000000..56e2c57 --- /dev/null +++ b/test/Report/JUnit.php @@ -0,0 +1,232 @@ +stream = fopen('php://memory', 'r+'); + $this->formatter = new JUnit($this->stream); + $this->summary = SummaryBuilder::emptySummary(); + } + + <> + public function rootNodeIsCreated(Assert $assert): void { + $this->formatter->writeReport($this->summary); + $report = $this->getActualReport($assert); + $assert->string($report->getName())->is('testsuites'); + $attributeCount = 0; + foreach ($report->attributes() as $name => $value) { + $attributeCount++; + } + $assert->int($attributeCount)->eq(0); + } + + <> + public function suiteReportsAreGenerated(Assert $assert): void { + $this->summary['suite summaries'] = Map { + 'suite 1' => SummaryBuilder::emptySuiteSummary(), + 'suite 2' => SummaryBuilder::emptySuiteSummary(), + }; + + $this->formatter->writeReport($this->summary); + $report = $this->getActualReport($assert); + $assert->int(count($report->xpath('testsuite')))->eq(2); + } + + <> + public function executionTimeIsIncludedInSuiteReport(Assert $assert): void { + $assert->skip('Need to time individual suites'); + } + + <> + public function suiteNameIsIncluded(Assert $assert): void { + $suiteSummary = SummaryBuilder::emptySuiteSummary(); + $this->summary['suite summaries'] = Map {'suite name' => $suiteSummary}; + $this->formatter->writeReport($this->summary); + $report = $this->getActualReport($assert)->xpath('testsuite')[0]; + $assert->string((string) $report['name'])->is('suite name'); + } + + <> + public function suiteCountsAreIncluded(Assert $assert): void { + $suiteSummary = SummaryBuilder::emptySuiteSummary(); + $suiteSummary['fail count'] = 2; + $suiteSummary['test count'] = 5; + $this->summary['suite summaries'] = Map {'suite name' => $suiteSummary}; + + $this->formatter->writeReport($this->summary); + $report = $this->getActualReport($assert)->xpath('testsuite')[0]; + + $assert->string((string) $report->offsetGet('failures'))->is('2'); + $assert->string((string) $report->offsetGet('tests'))->is('5'); + } + + <> + public function testsAreIncluded(Assert $assert): void { + $suiteSummary = SummaryBuilder::emptySuiteSummary(); + $suiteSummary['test summaries'] = Map { + 'test1' => SummaryBuilder::emptyTestSummary(), + 'test2' => SummaryBuilder::emptyTestSummary(), + }; + $this->summary['suite summaries'] = Map {'suite name' => $suiteSummary}; + $this->formatter->writeReport($this->summary); + $report = $this->getActualReport($assert)->xpath('testsuite')[0]; + + $testCases = $report->xpath('testcase'); + $assert->int(count($testCases))->eq(2); + } + + <> + public function timingIsIncludedInTestReports(Assert $assert): void { + $assert->skip('Need to time individual tests.'); + } + + <> + public function testNameIsIncluded(Assert $assert): void { + $suiteSummary = SummaryBuilder::emptySuiteSummary(); + $suiteSummary['test summaries'] = Map { + 'test name' => SummaryBuilder::emptyTestSummary(), + }; + + $summary = SummaryBuilder::emptySummary(); + $summary['suite summaries'] = Map {'suite name' => $suiteSummary}; + + $this->formatter->writeReport($summary); + $report = $this->getActualReport($assert); + $testReport = $report->xpath('/testsuites/testsuite/testcase')[0]; + + $assert->string((string) $testReport->offsetGet('name'))->is('test name'); + } + + <> + public function testFailureIsIncluded(Assert $assert): void { + + $failEvent = Failure::fromCallStack('For jUnit report.'); + $testSummary = SummaryBuilder::emptyTestSummary(); + $testSummary['result'] = TestResult::Fail; + $testSummary['fail event'] = $failEvent; + + $suiteSummary = SummaryBuilder::emptySuiteSummary(); + $suiteSummary['test summaries'] = Map {'test name' => $testSummary}; + + $summary = SummaryBuilder::emptySummary(); + $summary['suite summaries'] = Map {'suite name' => $suiteSummary}; + + $this->formatter->writeReport($summary); + $report = $this->getActualReport($assert); + $failElements = $report->xpath('/testsuites/testsuite/testcase/failure'); + $assert->int(count($failElements))->eq(1); + + $failure = $failElements[0]; + $assert->string((string) $failure->offsetGet('message')) + ->is($failEvent->getMessage()); + } + + <> + public function testSkipIsIncluded(Assert $assert): void { + $testSummary = SummaryBuilder::emptyTestSummary(); + $testSummary['result'] = TestResult::Skip; + + $suiteSummary = SummaryBuilder::emptySuiteSummary(); + $suiteSummary['test summaries'] = Map {'test name' => $testSummary}; + + $summary = SummaryBuilder::emptySummary(); + $summary['suite summaries'] = Map {'suite name' => $suiteSummary}; + + $this->formatter->writeReport($summary); + $report = $this->getActualReport($assert); + $failElements = $report->xpath('/testsuites/testsuite/testcase/skip'); + $assert->int(count($failElements))->eq(1); + } + + private function getActualReport(Assert $assert): SimpleXMLElement { + rewind($this->stream); + $rawXml = stream_get_contents($this->stream); + try { + $report = new SimpleXMLElement($rawXml); + } catch (\Exception $e) { + $assert->fail( + sprintf( + "XML error: %s\nOriginal XML:\n%s", + $e->getMessage(), + $rawXml, + ), + ); + throw new \RuntimeException('This should not be reached'); + } + return $report; + } +} From d5cd80c82f23df0bd455f17df9ce676ab9af54c6 Mon Sep 17 00:00:00 2001 From: Isaac Leinweber Date: Wed, 19 Oct 2016 23:21:27 -0700 Subject: [PATCH 2/4] Add stdout indication of location of jUnit report. --- test/self-test.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/self-test.php b/test/self-test.php index b69635d..6a6092c 100644 --- a/test/self-test.php +++ b/test/self-test.php @@ -18,10 +18,13 @@ $reportFormatters = Vector {new CliFormat(STDOUT)}; // CircleCI provides this env -// $circleReportDir = getenv('CIRCLE_TEST_REPORTS'); -// if (is_string($circleReportDir)) { -// $reportFormatters->add(JUnitFormat::build($circleReportDir.'/report.xml')); -// } +$circleReportDir = getenv('CIRCLE_TEST_REPORTS'); +if (is_string($circleReportDir)) { + echo + PHP_EOL.'jUnit report will be saved to '.$circleReportDir.'/report.xml' + ; + $reportFormatters->add(JUnitFormat::build($circleReportDir.'/report.xml')); +} $status = new Status(STDOUT); $suiteBuilder = new SuiteBuilder( From 4915f8cdd6ae74463e09286334a9f8b5e81b24f3 Mon Sep 17 00:00:00 2001 From: Isaac Leinweber Date: Wed, 19 Oct 2016 23:29:10 -0700 Subject: [PATCH 3/4] Mount the circleCI reports dir as a volume. --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 5bfd28d..c2d21bf 100644 --- a/circle.yml +++ b/circle.yml @@ -11,4 +11,4 @@ dependencies: test: override: - - docker run -v "$(pwd)":"$(pwd)" --workdir="$(pwd)" -e "CIRCLE_TEST_REPORTS=$CIRCLE_TEST_REPORTS" hhvm/hhvm:3.15-lts-latest hhvm -c test/self-test.ini test/self-test.php + - docker run -v "$(pwd)":"$(pwd)" -v "$CIRCLE_TEST_REPORTS":"$CIRCLE_TEST_REPORTS" --workdir="$(pwd)" -e "CIRCLE_TEST_REPORTS=$CIRCLE_TEST_REPORTS" hhvm/hhvm:3.15-lts-latest hhvm -c test/self-test.ini test/self-test.php From 3a0a349aec3d11eecf2c41542a5f100303e7d416 Mon Sep 17 00:00:00 2001 From: Isaac Riceweber Date: Fri, 3 Mar 2017 17:17:01 -0800 Subject: [PATCH 4/4] Unknown working changes --- src/Contract/Test/Suite.php | 2 + src/Event/Listeners.php | 3 +- src/Event/SuiteEnd.php | 11 +++++ src/Event/TestEnd.php | 28 +++++++++++ src/Event/TestStart.php | 10 ++++ src/HackUnit.php | 11 ++++- src/Report/Format/JUnit.php | 1 + src/Report/SummaryBuilder.php | 80 ++++++++++++++++++++++++++----- src/Test/Runner.php | 22 +++++---- src/Test/SkippedSuite.php | 2 + src/Test/Suite.php | 23 +++++++-- test/Doubles/AsyncSuite.php | 2 + test/Doubles/SpySuite.php | 2 + test/Report/SummaryBuilder.php | 87 ++++++++++++++++++++++++++++++++-- test/Test/Suite.php | 11 ++++- 15 files changed, 263 insertions(+), 32 deletions(-) create mode 100644 src/Event/SuiteEnd.php create mode 100644 src/Event/TestEnd.php diff --git a/src/Contract/Test/Suite.php b/src/Contract/Test/Suite.php index 13ba882..448e9a4 100644 --- a/src/Contract/Test/Suite.php +++ b/src/Contract/Test/Suite.php @@ -4,6 +4,7 @@ use HackPack\HackUnit\Contract\Assert; use HackPack\HackUnit\Event\TestStartListener; +use HackPack\HackUnit\Event\TestEndListener; interface Suite { public function up(): Awaitable; @@ -11,6 +12,7 @@ public function run( Assert $assert, (function(): void) $testPassed, \ConstVector $testStartListeners, + \ConstVector $testEndListeners, ): Awaitable; public function down(): Awaitable; public function name(): string; diff --git a/src/Event/Listeners.php b/src/Event/Listeners.php index 5c5c44b..02369be 100644 --- a/src/Event/Listeners.php +++ b/src/Event/Listeners.php @@ -11,6 +11,7 @@ type RunStartListener = (function(): void); type SkipListener = (function(Skip): void); type SuccessListener = (function(Success): void); -type SuiteEndListener = (function(): void); +type SuiteEndListener = (function(SuiteEnd): void); type SuiteStartListener = (function(SuiteStart): void); +type TestEndListener = (function(TestEnd): void); type TestStartListener = (function(TestStart): void); diff --git a/src/Event/SuiteEnd.php b/src/Event/SuiteEnd.php new file mode 100644 index 0000000..25008c6 --- /dev/null +++ b/src/Event/SuiteEnd.php @@ -0,0 +1,11 @@ +suiteName; + } +} diff --git a/src/Event/TestEnd.php b/src/Event/TestEnd.php new file mode 100644 index 0000000..339de0a --- /dev/null +++ b/src/Event/TestEnd.php @@ -0,0 +1,28 @@ +suiteName; + } + + public function testName(): string { + return $this->testName; + } + + public function file(): string { + return $this->file === null ? '??' : $this->file; + } + + public function line(): int { + return $this->line === null ? -1 : $this->line; + } +} diff --git a/src/Event/TestStart.php b/src/Event/TestStart.php index fdc424a..6155be2 100644 --- a/src/Event/TestStart.php +++ b/src/Event/TestStart.php @@ -6,6 +6,8 @@ class TestStart { public function __construct( private string $suiteName, private string $testName, + private ?string $file, + private ?int $line, ) {} public function suiteName(): string { @@ -15,4 +17,12 @@ public function suiteName(): string { public function testName(): string { return $this->testName; } + + public function file(): string { + return $this->file === null ? '??' : $this->file; + } + + public function line(): int { + return $this->line === null ? -1 : $this->line; + } } diff --git a/src/HackUnit.php b/src/HackUnit.php index 2ba1a58..14d4732 100644 --- a/src/HackUnit.php +++ b/src/HackUnit.php @@ -31,9 +31,16 @@ public function run(): void { $this->runner->onRunStart( () ==> { $this->status->handleRunStart(); - $this->summaryBuilder->startTiming(); + $this->summaryBuilder->handleRunStart(); }, ); + + $this->runner->onTestStart( + ($e) ==> { + $this->summaryBuilder->handleTestStart($e); + }, + ); + $this->runner->onFailure( ($e) ==> { // Allow us to set the exit code @@ -70,7 +77,7 @@ public function run(): void { ); $this->runner->onRunEnd( () ==> { - $this->summaryBuilder->stopTiming(); + $this->summaryBuilder->handleRunEnd(); $summary = $this->summaryBuilder->getSummary(); foreach ($this->reportFormatters as $formatter) { $formatter->writeReport($summary); diff --git a/src/Report/Format/JUnit.php b/src/Report/Format/JUnit.php index 9de02fc..9be50f4 100644 --- a/src/Report/Format/JUnit.php +++ b/src/Report/Format/JUnit.php @@ -19,6 +19,7 @@ public static function build(string $reportPath): this { public function __construct(private resource $out) { $this->report = new SimpleXMLElement(''); } + public function writeReport(Summary $summary): void { foreach ($summary['suite summaries'] as $name => $suiteSummary) { $this->report->addChild('testsuite') diff --git a/src/Report/SummaryBuilder.php b/src/Report/SummaryBuilder.php index f9ba525..c569a8d 100644 --- a/src/Report/SummaryBuilder.php +++ b/src/Report/SummaryBuilder.php @@ -7,6 +7,10 @@ use HackPack\HackUnit\Event\Pass; use HackPack\HackUnit\Event\Skip; use HackPack\HackUnit\Event\Success; +use HackPack\HackUnit\Event\SuiteEnd; +use HackPack\HackUnit\Event\SuiteStart; +use HackPack\HackUnit\Event\TestStart; +use HackPack\HackUnit\Event\TestEnd; use HackPack\HackUnit\Util\Options; use HackPack\HackUnit\Util\TraceItem; @@ -42,6 +46,8 @@ ); type MutableSuiteSummary = shape( + 'start time' => float, + 'end time' => float, 'assert count' => int, 'success count' => int, 'pass count' => int, @@ -52,6 +58,8 @@ ); type SuiteSummary = shape( + 'start time' => float, + 'end time' => float, 'assert count' => int, 'success count' => int, 'pass count' => int, @@ -62,6 +70,8 @@ ); type TestSummary = shape( + 'start time' => float, + 'end time' => float, 'assert count' => int, 'success count' => int, 'result' => TestResult, @@ -72,6 +82,8 @@ type TestInfo = shape( 'test name' => string, 'suite name' => string, + 'file' => string, + 'line' => int, ); enum TestResult : string { @@ -89,26 +101,51 @@ public function __construct() { $this->summary = self::emptyMutableSummary(); } - public function startTiming(): void { + public function handleRunStart(): void { $this->summary['start time'] = microtime(true); } - public function stopTiming(): void { + public function handleRunEnd(): void { $this->summary['end time'] = microtime(true); } + public function handleSuiteStart(SuiteStart $event): void {} + + public function handleTestStart(TestStart $event): void { + $testInfo = shape( + 'test name' => $event->testName(), + 'suite name' => $event->suiteName(), + 'file' => $event->file(), + 'line' => $event->line(), + ); + $this->ensureTestExists($testInfo); + + $this->summary['test count']++; + + $suiteSummary = + $this->summary['suite summaries']->at($testInfo['suite name']); + $suiteSummary['test count']++; + + $testSummary = + $suiteSummary['test summaries']->at($testInfo['test name']); + $testSummary['start time'] = microtime(true); + + $suiteSummary['test summaries'] + ->set($testInfo['test name'], $testSummary); + $this->summary['suite summaries'] + ->set($testInfo['suite name'], $suiteSummary); + } + public function handleFailure(Failure $event): void { $testInfo = $this->determineTestInfo($event->testMethodTraceItem()); $this->ensureTestExists($testInfo); - $this->summary['test count']++; $this->summary['fail count']++; $this->summary['assert count']++; $this->summary['fail events']->add($event); $suiteSummary = $this->summary['suite summaries']->at($testInfo['suite name']); - $suiteSummary['test count']++; $suiteSummary['fail count']++; $suiteSummary['assert count']++; @@ -129,13 +166,11 @@ public function handleSkip(Skip $event): void { $testInfo = $this->determineTestInfo($event->testMethodTraceItem()); $this->ensureTestExists($testInfo); - $this->summary['test count']++; $this->summary['skip count']++; $this->summary['skip events']->add($event); $suiteSummary = $this->summary['suite summaries']->at($testInfo['suite name']); - $suiteSummary['test count']++; $suiteSummary['skip count']++; $testSummary = @@ -176,13 +211,14 @@ public function handlePass(Pass $event): void { $testInfo = $this->determineTestInfo($event->testMethodTraceItem()); $this->ensureTestExists($testInfo); - $this->summary['test count']++; $this->summary['pass count']++; + $suiteSummary = + $this->summary['suite summaries']->at($testInfo['suite name']); + $suiteSummary['pass count']++; + $this->summary['suite summaries'] - ->at($testInfo['suite name'])['test count']++; - $this->summary['suite summaries'] - ->at($testInfo['suite name'])['pass count']++; + ->set($testInfo['suite name'], $suiteSummary); } public function handleUntestedException(\Exception $e): void { @@ -193,6 +229,9 @@ public function handleMalformedSuite(MalformedSuite $event): void { $this->summary['malformed events']->add($event); } + public function handleSuiteEnd(SuiteEnd $event): void {} + public function handleTestEnd(TestEnd $event): void {} + public function getSummary(): Summary { return $this->summary; } @@ -226,6 +265,8 @@ public static function emptySuiteSummary(): SuiteSummary { private static function emptyMutableSuiteSummary(): MutableSuiteSummary { return shape( + 'start time' => 0.0, + 'end time' => 0.0, 'assert count' => 0, 'success count' => 0, 'pass count' => 0, @@ -238,6 +279,8 @@ private static function emptyMutableSuiteSummary(): MutableSuiteSummary { public static function emptyTestSummary(): TestSummary { return shape( + 'start time' => 0.0, + 'end time' => 0.0, 'assert count' => 0, 'success count' => 0, 'message' => '', @@ -256,7 +299,22 @@ private function determineTestInfo(TraceItem $trace): TestInfo { $suiteName = '??'; } - return shape('test name' => $testName, 'suite name' => $suiteName); + $fileName = Shapes::idx($trace, 'file', '??'); + if ($fileName === null) { + $fileName = '??'; + } + + $line = Shapes::idx($trace, 'line', -1); + if ($line === null) { + $line = -1; + } + + return shape( + 'test name' => $testName, + 'suite name' => $suiteName, + 'file' => $fileName, + 'line' => $line, + ); } private function ensureTestExists(TestInfo $testInfo): void { diff --git a/src/Test/Runner.php b/src/Test/Runner.php index f3d4eef..c698ab7 100644 --- a/src/Test/Runner.php +++ b/src/Test/Runner.php @@ -15,8 +15,11 @@ use HackPack\HackUnit\Event\SkipListener; use HackPack\HackUnit\Event\SuccessListener; use HackPack\HackUnit\Event\SuiteEndListener; +use HackPack\HackUnit\Event\SuiteEnd; use HackPack\HackUnit\Event\SuiteStartListener; use HackPack\HackUnit\Event\SuiteStart; +use HackPack\HackUnit\Event\TestEndListener; +use HackPack\HackUnit\Event\TestEnd; use HackPack\HackUnit\Event\TestStartListener; use HackPack\HackUnit\Event\TestStart; use HH\Asio; @@ -32,6 +35,7 @@ class Runner implements \HackPack\HackUnit\Contract\Test\Runner { private Vector $suiteEndListeners = Vector {}; private Vector $suiteStartListeners = Vector {}; private Vector $testStartListeners = Vector {}; + private Vector $testEndListeners = Vector {}; public function __construct( private (function(Vector, @@ -85,6 +89,11 @@ public function onTestStart(TestStartListener $l): this { return $this; } + public function onTestEnd(TestEndListener $l): this { + $this->testEndListeners->add($l); + return $this; + } + public function onPass(PassListener $l): this { $this->passListeners->add($l); return $this; @@ -124,10 +133,11 @@ public function run(Vector $suites): void { $this->emitPass(); }, $this->testStartListeners, + $this->testEndListeners, ) |> Asio\wrap($$); await $s->down(); - $this->emitSuiteEnd(); + $this->emitSuiteEnd(new SuiteEnd($s->name())); if ($testResult->isFailed()) { throw $testResult->getException(); @@ -145,9 +155,9 @@ public function run(Vector $suites): void { $this->emitRunEnd(); } - private function emitSuiteEnd(): void { + private function emitSuiteEnd(SuiteEnd $e): void { foreach ($this->suiteEndListeners as $l) { - $l(); + $l($e); } } @@ -157,12 +167,6 @@ private function emitSuiteStart(SuiteStart $e): void { } } - private function emitTestStart(TestStart $e): void { - foreach ($this->testStartListeners as $l) { - $l($e); - } - } - private function emitRunEnd(): void { foreach ($this->runEndListeners as $l) { $l(); diff --git a/src/Test/SkippedSuite.php b/src/Test/SkippedSuite.php index 5c0dc0f..95c3db6 100644 --- a/src/Test/SkippedSuite.php +++ b/src/Test/SkippedSuite.php @@ -3,6 +3,7 @@ namespace HackPack\HackUnit\Test; use HackPack\HackUnit\Contract\Assert; +use HackPack\HackUnit\Event\TestEndListener; use HackPack\HackUnit\Event\TestStartListener; use HackPack\HackUnit\Util\TraceItem; @@ -17,6 +18,7 @@ public function __construct(private string $name, private TraceItem $trace) {} Assert $assert, (function(): void) $testPassed, \ConstVector $testStartListeners, + \ConstVector $testEndListeners, ): Awaitable { $assert->skip('Class '.$this->name.' marked "Skipped"', $this->trace); } diff --git a/src/Test/Suite.php b/src/Test/Suite.php index e12dcce..b291ead 100644 --- a/src/Test/Suite.php +++ b/src/Test/Suite.php @@ -5,6 +5,8 @@ use HackPack\HackUnit\Contract\Assert; use HackPack\HackUnit\Contract\Test\TestCase; use HackPack\HackUnit\Event\Interruption; +use HackPack\HackUnit\Event\TestEndListener; +use HackPack\HackUnit\Event\TestEnd; use HackPack\HackUnit\Event\TestStartListener; use HackPack\HackUnit\Event\TestStart; use HackPack\HackUnit\Util\Trace; @@ -40,11 +42,17 @@ public function name(): string { Assert $assert, (function(): void) $testPassed, \ConstVector $testStartListeners, + \ConstVector $testEndListeners, ): Awaitable { await (async (Test $test) ==> { - - $testStartEvent = - new TestStart($test['suite name'], $test['name']); + $testTrace = $test['trace item']; + + $testStartEvent = new TestStart( + $test['suite name'], + $test['name'], + $testTrace['file'], + $testTrace['line'], + ); foreach ($testStartListeners as $testStartListener) { $testStartListener($testStartEvent); } @@ -63,6 +71,15 @@ public function name(): string { await ($this->testup->map($pretest ==> $pretest($instance, [])) |> Asio\v($$)); + $testEndEvent = new TestEnd( + $test['suite name'], + $test['name'], + $testTrace['file'], + $testTrace['line'], + ); + foreach ($testEndListeners as $testEndListener) { + $testEndListener($testEndEvent); + } $results = Vector {}; foreach ($test['data provider']() await as $data) { array_unshift($data, $assert); diff --git a/test/Doubles/AsyncSuite.php b/test/Doubles/AsyncSuite.php index 8c6f6e9..e1df253 100644 --- a/test/Doubles/AsyncSuite.php +++ b/test/Doubles/AsyncSuite.php @@ -4,6 +4,7 @@ use HackPack\HackUnit\Contract\Assert; use HackPack\HackUnit\Contract\Test\Suite; +use HackPack\HackUnit\Event\TestEndListener; use HackPack\HackUnit\Event\TestStartListener; class AsyncSuite implements Suite { @@ -16,6 +17,7 @@ public function __construct(private int $sleepTime) {} Assert $assert, (function(): void) $testPassed, \ConstVector $testStartListeners, + \ConstVector $testEndListeners, ): Awaitable { await \HH\Asio\usleep($this->sleepTime); } diff --git a/test/Doubles/SpySuite.php b/test/Doubles/SpySuite.php index a7499e6..31eb96c 100644 --- a/test/Doubles/SpySuite.php +++ b/test/Doubles/SpySuite.php @@ -4,6 +4,7 @@ use HackPack\HackUnit\Contract\Assert; use HackPack\HackUnit\Contract\Test\Suite; +use HackPack\HackUnit\Event\TestEndListener; use HackPack\HackUnit\Event\TestStartListener; type RunCounts = shape( @@ -44,6 +45,7 @@ public function name(): string { Assert $assert, (function(): void) $testPassed, \ConstVector $testStartListeners, + \ConstVector $testEndListeners, ): Awaitable { $this->asserts->add($assert); $this->passCallbacks->add($testPassed); diff --git a/test/Report/SummaryBuilder.php b/test/Report/SummaryBuilder.php index ed4db82..0fcbe5d 100644 --- a/test/Report/SummaryBuilder.php +++ b/test/Report/SummaryBuilder.php @@ -6,6 +6,10 @@ use HackPack\HackUnit\Event\Failure; use HackPack\HackUnit\Event\Skip; use HackPack\HackUnit\Event\Success; +use HackPack\HackUnit\Event\SuiteEnd; +use HackPack\HackUnit\Event\SuiteStart; +use HackPack\HackUnit\Event\TestEnd; +use HackPack\HackUnit\Event\TestStart; use HackPack\HackUnit\Report\TestResult; use HackPack\HackUnit\Report\SuiteSummary; use HackPack\HackUnit\Report\Summary; @@ -14,6 +18,8 @@ final class SummaryBuilderTest { + const TestLineNumber = 42; + private SummaryBuilder $builder; public function __construct() { $this->builder = new SummaryBuilder(); @@ -49,6 +55,32 @@ private function buildSkipEvent(): Skip { return new Skip('skip message', ...$this->buildStackTraces()); } + private function buildSuiteStartEvent(): SuiteStart { + return new SuiteStart('suite name'); + } + + private function buildSuiteEndEvent(): SuiteEnd { + return new SuiteEnd('suite name'); + } + + private function buildTestStartEvent(): TestStart { + return new TestStart( + 'suite name', + 'test name', + 'test file', + self::TestLineNumber, + ); + } + + private function buildTestEndEvent(): TestEnd { + return new TestEnd( + 'suite name', + 'test name', + 'file name', + self::TestLineNumber, + ); + } + <> public function successIncrementsAppropriateCounts(Assert $assert): void { $this->builder->handleSuccess($this->buildSuccessEvent()); @@ -88,7 +120,6 @@ public function failIncrementsAppropriateCounts(Assert $assert): void { $expectedSuiteSummary = SummaryBuilder::emptySuiteSummary(); $expectedSuiteSummary['assert count'] = 1; - $expectedSuiteSummary['test count'] = 1; $expectedSuiteSummary['fail count'] = 1; $expectedSuiteSummary['test summaries'] = Map { 'testFunction' => $expectedTestSummary, @@ -96,7 +127,6 @@ public function failIncrementsAppropriateCounts(Assert $assert): void { $expectedSummary = SummaryBuilder::emptySummary(); $expectedSummary['assert count'] = 1; - $expectedSummary['test count'] = 1; $expectedSummary['fail count'] = 1; $expectedSummary['fail events'] = Vector {$event}; $expectedSummary['suite summaries'] = Map { @@ -113,19 +143,16 @@ public function skipIncrementsAppropriateCounts(Assert $assert): void { $actualSummary = $this->builder->getSummary(); $expectedTestSummary = SummaryBuilder::emptyTestSummary(); - $expectedTestSummary['test count'] = 1; $expectedTestSummary['result'] = TestResult::Skip; $expectedTestSummary['skip event'] = $event; $expectedSuiteSummary = SummaryBuilder::emptySuiteSummary(); $expectedSuiteSummary['skip count'] = 1; - $expectedSuiteSummary['test count'] = 1; $expectedSuiteSummary['test summaries'] = Map { 'testFunction' => $expectedTestSummary, }; $expectedSummary = SummaryBuilder::emptySummary(); - $expectedSummary['test count'] = 1; $expectedSummary['skip count'] = 1; $expectedSummary['skip events'] = Vector {$event}; $expectedSummary['suite summaries'] = Map { @@ -135,6 +162,56 @@ public function skipIncrementsAppropriateCounts(Assert $assert): void { $this->compareSummaries($assert, $actualSummary, $expectedSummary); } + <> + public function suiteStartSetsAppropriateFields(Assert $assert): void { + $event = $this->buildSuiteStartEvent(); + $this->builder->handleSuiteStart($event); + $summary = $this->builder->getSummary(); + + $suiteSummaries = $summary['suite summaries']; + + $assert->int(count($suiteSummaries))->eq(1); + $assert->float($suiteSummaries->at('suite name')['start time'])->gt(0.0); + } + + <> + public function testStartSetsAppropriateFields(Assert $assert): void { + $event = $this->buildTestStartEvent(); + $this->builder->handleTestStart($event); + $summary = $this->builder->getSummary(); + + $testSummaries = + $summary['suite summaries']->at('suite name')['test summaries']; + + $assert->int(count($testSummaries))->eq(1); + $assert->float($testSummaries->at('test name')['start time'])->gt(0.0); + } + + <> + public function suiteEndSetsAppropriateFields(Assert $assert): void { + $event = $this->buildSuiteEndEvent(); + $this->builder->handleSuiteEnd($event); + $summary = $this->builder->getSummary(); + + $suiteSummaries = $summary['suite summaries']; + + $assert->int(count($suiteSummaries))->eq(1); + $assert->float($suiteSummaries->at('suite name')['end time'])->gt(0.0); + } + + <> + public function testEndSetsAppropriateFields(Assert $assert): void { + $event = $this->buildTestEndEvent(); + $this->builder->handleTestEnd($event); + $summary = $this->builder->getSummary(); + + $testSummaries = + $summary['suite summaries']->at('suite name')['test summaries']; + + $assert->int(count($testSummaries))->eq(1); + $assert->float($testSummaries->at('test name')['end time'])->gt(0.0); + } + private function compareSummaries( Assert $assert, Summary $actualSummary, diff --git a/test/Test/Suite.php b/test/Test/Suite.php index c57949c..55fce8e 100644 --- a/test/Test/Suite.php +++ b/test/Test/Suite.php @@ -5,7 +5,7 @@ use HackPack\HackUnit\Contract\Assert; use HackPack\HackUnit\Event\Interruption; use HackPack\HackUnit\Event\Skip; -use HackPack\HackUnit\Event\TestStartListener; +use HackPack\HackUnit\Event\TestEnd; use HackPack\HackUnit\Event\TestStart; use HackPack\HackUnit\Test\Suite; use HackPack\HackUnit\Test\Test as TestShape; @@ -24,6 +24,7 @@ class SuiteTest { private int $passedEvents = 0; private Vector $skipEvents = Vector {}; private Vector $testStartEvents = Vector {}; + private Vector $testEndEvents = Vector {}; private (function(): Awaitable) $factory; private TraceItem $traceItem; @@ -126,6 +127,9 @@ private function runTests(Assert $assert, Vector $tests): void { // Test start events triggered $assert->int($this->testStartEvents->count())->eq($tests->count()); + // Test end events triggered + $assert->int($this->testEndEvents->count())->eq($tests->count()); + // Skipped tests shouldn't run the factory $assert->int($this->factoryRuns)->eq(2 * $thirdTestCount); @@ -302,6 +306,11 @@ private function runSuite(Suite $suite): void { $this->testStartEvents->add($e); }, }, + Vector { + (TestEnd $e) ==> { + $this->testEndEvents->add($e); + }, + }, ), ); }