diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6efb163 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +.DEFAULT_GOAL := help + +mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST))) +current_dir := $(dir $(mkfile_path)) + +help: + @echo "Use this makefile to execute your tests in correct php version" + @echo "\tr.php-7.4\t\trun Tests with PHP 7.4" + @echo "\tr.php-8.0\t\trun Tests with PHP 8.0" + @echo "\tr.php-8.1\t\trun Tests with PHP 8.1" + @echo "\tr.php-8.2\t\trun Tests with PHP 8.2" + +r.php-7.4: + docker build -t robo:php-7.4 --target PHP74 --build-arg PHP_VERSION=7.4 docker + docker run --rm -v $(current_dir):/app -w /app robo:php-7.4 composer install + docker run --rm -v $(current_dir):/app -w /app robo:php-7.4 composer test + +r.php-8.0: + docker build -t robo:php-8.0 --target PHP8 --build-arg PHP_VERSION=8.0 docker + docker run --rm -v $(current_dir):/app -w /app robo:php-8.0 composer install + docker run --rm -v $(current_dir):/app -w /app robo:php-8.0 composer test + +r.php-8.1: + docker build -t robo:php-8.1 --target PHP8 --build-arg PHP_VERSION=8.1 docker + docker run --rm -v $(current_dir):/app -w /app robo:php-8.1 composer install + docker run --rm -v $(current_dir):/app -w /app robo:php-8.1 composer test + +r.php-8.2: + docker build -t robo:php-8.2 --target PHP8 --build-arg PHP_VERSION=8.2 docker + docker run --rm -v $(current_dir):/app -w /app robo:php-8.2 composer install + docker run --rm -v $(current_dir):/app -w /app robo:php-8.2 composer test \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..073ad7b --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,13 @@ +ARG PHP_VERSION=7.4 +FROM php:${PHP_VERSION}-cli as base +COPY --from=composer /usr/bin/composer /usr/bin/composer + +RUN apt update \ + && apt upgrade -y \ + && apt install -y apt-utils libxml2-dev libzip-dev + +FROM base as PHP74 +RUN docker-php-ext-install dom json xml zip + +FROM base as PHP8 +RUN docker-php-ext-install dom xml zip \ No newline at end of file diff --git a/src/Merger/HtmlReportMerger.php b/src/Merger/HtmlReportMerger.php index 88f198d..a2c0066 100644 --- a/src/Merger/HtmlReportMerger.php +++ b/src/Merger/HtmlReportMerger.php @@ -50,7 +50,20 @@ class HtmlReportMerger extends AbstractMerger protected bool $previousLibXmlUseErrors = false; - private float $executionTimeSum = 0; + protected bool $maxTime = false; + + protected array $executionTime = []; + + /** + * @var string|float + */ + private $executionTimeSum = 0; + + + public function maxTime(): void + { + $this->maxTime = true; + } /** * HtmlReportMerger constructor. @@ -198,9 +211,10 @@ private function countExecutionTime(DOMDocument $dstFile): void if (!$nodeList) { throw XPathExpressionException::malformedXPath($xpathHeadline); } - + $hoursMinutesSeconds = '(([0-1]?\d|2[0-3])(?::([0-5]?\d))?(?::([0-5]?\d))\.\d+)'; + $seconds = '\d+\.\d+s'; $pregResult = preg_match( - '#^Codeception Results .* \((?\d+\.\d+)s\)$#', + "#^Codeception Results .* \((?$hoursMinutesSeconds|$seconds)\)$#", $nodeList[0]->nodeValue, $matches ); @@ -213,7 +227,18 @@ private function countExecutionTime(DOMDocument $dstFile): void return; } - $this->executionTimeSum += (float)$matches['timesum']; + if (str_contains($matches['timesum'], 's')) { + $matches['timesum'] = str_replace('s', '', $matches['timesum']); + } + if (!$this->maxTime) { + if (str_contains($matches['timesum'], ':')) { + $this->executionTimeSum = $this->sumTime(strval($this->executionTimeSum), (string)$matches['timesum']); + } else { + $this->executionTimeSum += (float)$matches['timesum']; + } + } else { + $this->executionTime[] = (string)$matches['timesum']; + } } /** @@ -238,8 +263,20 @@ private function updateHeaderLine(DOMDocument $dstFile): void $statusNode->nodeValue = 'FAILED'; $statusAttr->value = 'color: red'; } - - $executionTimeNode->nodeValue = sprintf(' (%ss)', $this->executionTimeSum); + if (!$this->maxTime) { + $executionTime = (string)$this->executionTimeSum; + } else { + usort($this->executionTime, function ($a, $b) { + return strcmp($a, $b); + }); + $executionTime = max($this->executionTime); + } + $executionTimeNode->nodeValue = sprintf( + (preg_match('#([0-1]?\d|2[0-3])(?::([0-5]?\d))?(?::([0-5]?\d))\.\d+#', $executionTime)) + ? ' (%s)' + : ' (%ss)', + $executionTime + ); } /** @@ -342,4 +379,38 @@ private function updateButtons(DOMDocument $dstFile): void $table->setAttribute('id', "stepContainer" . $n); } } + + private function sumTime(string $time1, string $time2): string + { + $times = [$time1, $time2]; + $seconds = 0; + $milliseconds = 0; + $isHour = false; + foreach ($times as $time) { + if ($time !== '0') { + $output = explode(':', $time); + if (count($output) > 2) { + $isHour = true; + [$hour, $minute, $second] = $output; + $seconds += $hour * 3600; + } else { + [$minute, $second] = $output; + } + $seconds += $minute * 60; + [$second, $millisecond] = explode('.', $second); + $seconds += $second; + $milliseconds += $millisecond; + } + } + if ($isHour) { + $hours = floor($seconds / 3600); + $seconds -= $hours * 3600; + } + $minutes = floor($seconds / 60); + $seconds -= $minutes * 60; + + return $isHour + ? sprintf('%02d:%02d:%02d.%02d', $hours, $minutes, $seconds, $milliseconds) + : sprintf('%02d:%02d.%02d', $minutes, $seconds, $milliseconds); + } } diff --git a/src/Merger/XmlReportMergerTask.php b/src/Merger/XmlReportMergerTask.php index d8d88e6..fcbc5b1 100644 --- a/src/Merger/XmlReportMergerTask.php +++ b/src/Merger/XmlReportMergerTask.php @@ -18,8 +18,10 @@ class XmlReportMergerTask extends AbstractMerger protected array $src = []; protected string $dst = ''; + protected array $suiteDuration = []; protected bool $summarizeTime = true; + protected bool $maxSuiteTime = false; protected bool $mergeRewrite = false; @@ -36,6 +38,12 @@ public function maxTime(): void $this->summarizeTime = false; } + public function maxSuiteTime(): void + { + $this->summarizeTime = false; + $this->maxSuiteTime = true; + } + public function mergeRewrite(): self { $this->mergeRewrite = true; @@ -111,6 +119,7 @@ public function run(): void protected function loadSuites(DOMElement $current): void { + $this->suiteDuration[$current->getAttribute('name')][] = (float) $current->getAttribute('time'); /** @var DOMNode $node */ foreach ($current->childNodes as $node) { if ($node instanceof DOMElement) { @@ -136,13 +145,20 @@ protected function mergeSuites(DOMDocument $dstXml): void 'errors' => 0, 'time' => 0, ]; + foreach ($tests as $test) { $resultNode->appendChild($test); $data['assertions'] += (int)$test->getAttribute('assertions'); - $data['time'] = $this->summarizeTime - ? ((float)$test->getAttribute('time') + $data['time']) - : max($test->getAttribute('time'), $data['time']); + if ($this->summarizeTime) { + $data['time'] = ((float)$test->getAttribute('time') + $data['time']); + } else { + if ($this->maxSuiteTime) { + $data['time'] = max($this->suiteDuration[$suiteName]); + } else { + $data['time'] = max($test->getAttribute('time'), $data['time']); + } + } $data['failures'] += $test->getElementsByTagName('failure')->length; $data['errors'] += $test->getElementsByTagName('error')->length; diff --git a/src/Splitter/TestFileSplitterTask.php b/src/Splitter/TestFileSplitterTask.php index d1d4b0e..c0b318b 100644 --- a/src/Splitter/TestFileSplitterTask.php +++ b/src/Splitter/TestFileSplitterTask.php @@ -20,8 +20,6 @@ * ->testsFrom('tests/unit/Acme') * ->codeceptionRoot('projects/tested') * ->groupsTo('tests/_log/paratest_') - * ->addFilter(new Filter1()) - * ->addFilter(new Filter2()) * ->run(); * ``` * diff --git a/tests/Merger/HtmlReportMergerTest.php b/tests/Merger/HtmlReportMergerTest.php index d3df126..f5be1f5 100644 --- a/tests/Merger/HtmlReportMergerTest.php +++ b/tests/Merger/HtmlReportMergerTest.php @@ -62,4 +62,156 @@ public function testRun(): void $this->assertSame($expectedTimeInSeconds, (float)$matches['timesum']); } + + /** + * @covers ::run + */ + public function testRunWithCodeception5Reports(): void + { + $expectedTimeInSeconds = '03:34.98'; + $expectedSuccess= 3; + + $reportPath = TEST_PATH . '/fixtures/reports/html/'; + $task = new HtmlReportMerger(); + $task->setLogger(new Logger(new NullOutput())); + + $resultReport = TEST_PATH . '/result/report_codeception5.html'; + $task + ->from( + [ + $reportPath . 'report_0_codeception5.html', // this file did not exists and it should not fail + $reportPath . 'report_1_codeception5.html', + $reportPath . 'report_2_codeception5.html', + $reportPath . 'report_3_codeception5.html', + ] + ) + ->into($resultReport) + ->run(); + + $this->assertFileExists($resultReport); + + //read first source file as main + $dstHTML = new DOMDocument(); + $dstHTML->loadHTMLFile($resultReport, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + /** @var DOMNodeList $values */ + $values = (new DOMXPath($dstHTML)) + ->query("//*[contains(@class,'scenarioSuccessValue')]"); + + $this->assertCount(1, $values); + $this->assertSame($expectedSuccess, (int)$values[0]->nodeValue); + + $values = (new DOMXPath($dstHTML)) + ->query("//h1[text() = 'Codeception Results ']"); + preg_match( + '#^Codeception Results .* \((?(([0-1]?\d|2[0-3])(?::([0-5]?\d))?(?::([0-5]?\d))\.\d+))\)$#', + $values[0]->nodeValue, + $matches + ); + + $this->assertSame($expectedTimeInSeconds, (string)$matches['timesum']); + } + + /** + * @covers ::run + */ + public function testRunMaxTimeReports(): void + { + $expectedTime = '129.25'; + $expectedSuccess= 3; + + $reportPath = TEST_PATH . '/fixtures/reports/html/'; + $task = new HtmlReportMerger(); + $task->setLogger(new Logger(new NullOutput())); + + $resultReport = TEST_PATH . '/result/report_max_time.html'; + $task->maxTime(); + $task + ->from( + [ + $reportPath . 'report_0.html', // this file did not exists and it should not fail + $reportPath . 'report_1.html', + $reportPath . 'report_2.html', + $reportPath . 'report_3.html', + ] + ) + ->into($resultReport) + ->run(); + + $this->assertFileExists($resultReport); + + //read first source file as main + $dstHTML = new DOMDocument(); + $dstHTML->loadHTMLFile($resultReport, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + /** @var DOMNodeList $values */ + $values = (new DOMXPath($dstHTML)) + ->query("//*[contains(@class,'scenarioSuccessValue')]"); + + $this->assertCount(1, $values); + $this->assertSame($expectedSuccess, (int)$values[0]->nodeValue); + + $values = (new DOMXPath($dstHTML)) + ->query("//h1[text() = 'Codeception Results ']"); + preg_match( + '#^Codeception Results .* \((?\d+\.\d+)s\)$#', + $values[0]->nodeValue, + $matches + ); + $executionTime[] = (string)$matches['timesum']; + usort($executionTime, function ($a, $b) { + return strcmp($a, $b); + }); + $this->assertSame($expectedTime, max($executionTime)); + } + + /** + * @covers ::run + */ + public function testRunMaxTimeWithCodeception5Reports(): void + { + $expectedTime = '02:09.25'; + $expectedSuccess= 3; + + $reportPath = TEST_PATH . '/fixtures/reports/html/'; + $task = new HtmlReportMerger(); + $task->setLogger(new Logger(new NullOutput())); + + $resultReport = TEST_PATH . '/result/report_codeception5_max_time.html'; + $task->maxTime(); + $task + ->from( + [ + $reportPath . 'report_0_codeception5.html', // this file did not exists and it should not fail + $reportPath . 'report_1_codeception5.html', + $reportPath . 'report_2_codeception5.html', + $reportPath . 'report_3_codeception5.html', + ] + ) + ->into($resultReport) + ->run(); + + $this->assertFileExists($resultReport); + + //read first source file as main + $dstHTML = new DOMDocument(); + $dstHTML->loadHTMLFile($resultReport, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + /** @var DOMNodeList $values */ + $values = (new DOMXPath($dstHTML)) + ->query("//*[contains(@class,'scenarioSuccessValue')]"); + + $this->assertCount(1, $values); + $this->assertSame($expectedSuccess, (int)$values[0]->nodeValue); + + $values = (new DOMXPath($dstHTML)) + ->query("//h1[text() = 'Codeception Results ']"); + preg_match( + '#^Codeception Results .* \((?(([0-1]?\d|2[0-3])(?::([0-5]?\d))?(?::([0-5]?\d))\.\d+))\)$#', + $values[0]->nodeValue, + $matches + ); + $executionTime[] = (string)$matches['timesum']; + usort($executionTime, function ($a, $b) { + return strcmp($a, $b); + }); + $this->assertSame($expectedTime, max($executionTime)); + } } diff --git a/tests/Merger/XmlReportMergerTaskTest.php b/tests/Merger/XmlReportMergerTaskTest.php index 9cf40cd..22416bc 100644 --- a/tests/Merger/XmlReportMergerTaskTest.php +++ b/tests/Merger/XmlReportMergerTaskTest.php @@ -26,11 +26,11 @@ public function testMergeReports(): void $this->assertFileExists(TEST_PATH . '/result/merged.xml'); $xml = file_get_contents(TEST_PATH . '/result/merged.xml'); $this->assertStringContainsString( - '', $xml ); $this->assertStringContainsString( - '', $xml ); $this->assertStringContainsString( @@ -45,6 +45,28 @@ public function testMergeReports(): void ); } + public function testMergeReportsMaxSuiteTime(): void + { + $task = new XmlReportMergerTask(); + $task->setLogger(new Logger(new NullOutput())); + $task->maxSuiteTime(); + $task->from(TEST_PATH . '/fixtures/result1.xml') + ->from(TEST_PATH . '/fixtures/result2.xml') + ->into(TEST_PATH . '/result/merged.xml') + ->run(); + + $this->assertFileExists(TEST_PATH . '/result/merged.xml'); + $xml = file_get_contents(TEST_PATH . '/result/merged.xml'); + $this->assertStringContainsString( + '', + $xml + ); + $this->assertStringContainsString( + '', + $xml + ); + } + public function testMergeRewriteReports(): void { $task = new XmlReportMergerTask(); diff --git a/tests/fixtures/reports/html/expected_report_codeception5.html b/tests/fixtures/reports/html/expected_report_codeception5.html new file mode 100644 index 0000000..0c496f3 --- /dev/null +++ b/tests/fixtures/reports/html/expected_report_codeception5.html @@ -0,0 +1,528 @@ + + + Test results + + + + + + + + + + +
+

Codeception Results OK (234.98s)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Acceptance (cloud-stage) Tests

+
+

+ + ExpleogroupCest » Student apply expleogroup and + choose interest 103.29s

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ I acting as "Vincenzo" +

+ +

+ I set locale "fr"

+ +

+ I visit "Page\Applications\ApplicationFormPage","","appsource=expleogroup" +

+ +

+ Page\Applications\ApplicationFormPage: start + application {"title":"La première étape de votre candidature","button":"Commencer"} +

+ +

+ Page\Applications\ApplicationFormPage: fill + personal details {"title":"Votre profil","birthdate":"Votre date de naissance","address":"Votre adresse","gender":"Votre genre"},{"gender":"Masculin","address":"7 cité paradis PARIS"} +

+ +

+ Page\Applications\ApplicationFormPage: fill + education {"title":"Vos études","education":"","diploma":"Quel est votre niveau de diplôme le plus élevé ?","frenchLevel":"","englishLevel":"Quel est votre niveau d’anglais ?"},{"diploma":"Bac+2","englishLevel":"Intermédiaire (B1-B2)"} +

+ +

+ Page\Applications\ApplicationFormPage: fill + learning resources {"title":"La formation en ligne et vous","followedCourses":"Avez-vous déjà suivi des cours en ligne ?","followedCoursesSubject":"Sur quels sujets et sur quels sites ?","resources":"Pour cette formation, vous aurez accès à :"},{"followedCourses":"Oui","followedCoursesSubject":"Coursera","resources":["Un endroit calme","Un ordinateur","Un micro"]} +

+ +

+ Page\Applications\ApplicationFormPage: fill + motivation {"title":"Votre nouvelle carrière","professionalProject":"Détaillez votre projet professionnel (à court, moyen et long-terme)","cv":"Ajoutez votre CV","linkedin":"","foundEmployer":"","desiredStartDate":"","nextButton":"Envoyer la candidature"},{"cv":"cv.pdf"} +

+ +

+ Page\Applications\ApplicationFormPage: see + application end {"title":"Merci de votre intérêt pour nos formations !","button":"RETOUR À L’ACCUEIL"} +

+ +
+
+

Api (cloud-stage) Tests

+
+

+ + AnalyticsCest » Get analytics with paths code + 2.44s

+
+ + + + + + + + + + + + + +

+ I acting as "Business\Users\BotAdmin" +

+ +

+ I get analytics "PATHS",[],"0-1000" +

+ +

+ I see analytics "PATHS" +

+ +
+ + +
+

Bdd (cloud-stage) Tests

+
+

+ + Course » Member can follow a course and pass a + quiz 129.25s

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    As a member +
    I want to follow a + course and pass a quiz
    In order to make + progress on the platform

+ Given i am "member"

+ +

+ When i choose a course

+ +

+ And i follow a course

+ +

+ And i pass a quiz

+ +

+ Then i see that i have followed the course

+ +

+ And i see that i have passed the quiz

+ +
+ + +
+

Summary

+
+ + + + + + + + + + + + + + + + + +
Successful scenarios:3
Failed scenarios:0
Skipped scenarios:0
Incomplete scenarios:0
+
+
+
+ + diff --git a/tests/fixtures/reports/html/report_1_codeception5.html b/tests/fixtures/reports/html/report_1_codeception5.html new file mode 100644 index 0000000..3073123 --- /dev/null +++ b/tests/fixtures/reports/html/report_1_codeception5.html @@ -0,0 +1,317 @@ + + + Test results + + + + + + + + + + +
+

Codeception Results OK (01:23.29)

+ + + + + + + + + + + + + + + +
+

Acceptance (cloud-stage) Tests

+
+

+ + ExpleogroupCest » Student apply expleogroup and choose interest 103.29s

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ I acting as "Vincenzo"

+ +

+ I set locale "fr"

+ +

+ I visit "Page\Applications\ApplicationFormPage","","appsource=expleogroup"

+ +

+ Page\Applications\ApplicationFormPage: start application {"title":"La première étape de votre candidature","button":"Commencer"}

+ +

+ Page\Applications\ApplicationFormPage: fill personal details {"title":"Votre profil","birthdate":"Votre date de naissance","address":"Votre adresse","gender":"Votre genre"},{"gender":"Masculin","address":"7 cité paradis PARIS"}

+ +

+ Page\Applications\ApplicationFormPage: fill education {"title":"Vos études","education":"","diploma":"Quel est votre niveau de diplôme le plus élevé ?","frenchLevel":"","englishLevel":"Quel est votre niveau d’anglais ?"},{"diploma":"Bac+2","englishLevel":"Intermédiaire (B1-B2)"}

+ +

+ Page\Applications\ApplicationFormPage: fill learning resources {"title":"La formation en ligne et vous","followedCourses":"Avez-vous déjà suivi des cours en ligne ?","followedCoursesSubject":"Sur quels sujets et sur quels sites ?","resources":"Pour cette formation, vous aurez accès à :"},{"followedCourses":"Oui","followedCoursesSubject":"Coursera","resources":["Un endroit calme","Un ordinateur","Un micro"]}

+ +

+ Page\Applications\ApplicationFormPage: fill motivation {"title":"Votre nouvelle carrière","professionalProject":"Détaillez votre projet professionnel (à court, moyen et long-terme)","cv":"Ajoutez votre CV","linkedin":"","foundEmployer":"","desiredStartDate":"","nextButton":"Envoyer la candidature"},{"cv":"cv.pdf"}

+ +

+ Page\Applications\ApplicationFormPage: see application end {"title":"Merci de votre intérêt pour nos formations !","button":"RETOUR À L’ACCUEIL"}

+ +
+
+

Summary

+
+ + + + + + + + + + + + + + + + + +
Successful scenarios:1
Failed scenarios:0
Skipped scenarios:0
Incomplete scenarios:0
+
+
+
+ + diff --git a/tests/fixtures/reports/html/report_2_codeception5.html b/tests/fixtures/reports/html/report_2_codeception5.html new file mode 100644 index 0000000..8bb50e2 --- /dev/null +++ b/tests/fixtures/reports/html/report_2_codeception5.html @@ -0,0 +1,292 @@ + + + Test results + + + + + + + + + + +
+

Codeception Results OK (00:02.44)

+ + + + + + + + + + + + + + + + + + + + +
+

Api (cloud-stage) Tests

+
+

+ + AnalyticsCest » Get analytics with paths code 2.44s

+
+ + + + + + + + + + + + + +

+ I acting as "Business\Users\BotAdmin"

+ +

+ I get analytics "PATHS",[],"0-1000"

+ +

+ I see analytics "PATHS"

+ +
+ + +
+

Summary

+
+ + + + + + + + + + + + + + + + + +
Successful scenarios:1
Failed scenarios:0
Skipped scenarios:0
Incomplete scenarios:0
+
+
+
+ + diff --git a/tests/fixtures/reports/html/report_3_codeception5.html b/tests/fixtures/reports/html/report_3_codeception5.html new file mode 100644 index 0000000..bffa76b --- /dev/null +++ b/tests/fixtures/reports/html/report_3_codeception5.html @@ -0,0 +1,319 @@ + + + Test results + + + + + + + + + + +
+

Codeception Results OK (02:09.25)

+ + + + + + + + + + + + + + + + + + + + + +
+

Bdd (cloud-stage) Tests

+
+

+ + Course » Member can follow a course and pass a quiz 129.25s

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    As a member
    I want to follow a course and pass a quiz
    In order to make progress on the platform

+ Given i am "member"

+ +

+ When i choose a course

+ +

+ And i follow a course

+ +

+ And i pass a quiz

+ +

+ Then i see that i have followed the course

+ +

+ And i see that i have passed the quiz

+ +
+ + +
+

Summary

+
+ + + + + + + + + + + + + + + + + +
Successful scenarios:1
Failed scenarios:0
Skipped scenarios:0
Incomplete scenarios:0
+
+
+
+ +