From b93e892184f927cd4a1c2b0b37e7411a28366b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 7 Oct 2022 19:46:28 +0200 Subject: [PATCH] Collect js coverage --- .github/workflows/test-unit.yml | 29 ++++++++- src/Behat/Context.php | 6 +- src/Behat/JsCoverageContextTrait.php | 93 ++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 src/Behat/JsCoverageContextTrait.php diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index fd206d9b04..c4210efdf5 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -336,6 +336,13 @@ jobs: diff -ru template.orig template rm -r public.orig template.orig + - name: Instrument & recompile JS files (only for coverage) + if: env.LOG_COVERAGE + run: | + rm public/*.js + (cd js && npm install --package-lock-only --save-dev nyc && npm ci --loglevel=error) + (cd js && npx nyc instrument --in-place . && npx webpack --env production) + - name: Install PHP dependencies run: | composer remove --no-interaction --no-update phpunit/phpunit johnkary/phpunit-speedtrap --dev @@ -351,7 +358,7 @@ jobs: php -r '(new PDO("mysql:host=mariadb", "root", "atk4_pass_root"))->exec("ALTER USER '"'"'atk4_test_user'"'"'@'"'"'%'"'"' WITH MAX_USER_CONNECTIONS 5");' php -r '(new PDO("pgsql:host=postgres;dbname=atk4_test", "atk4_test_user", "atk4_pass"))->exec("ALTER ROLE atk4_test_user CONNECTION LIMIT 1");' /usr/lib/oracle/setup.sh - if [ -n "$LOG_COVERAGE" ]; then mkdir coverage; fi + if [ -n "$LOG_COVERAGE" ]; then mkdir coverage coverage/js; fi ci_wait_until () { timeout 30 sh -c "until { $1 2> /dev/null; }; do sleep 0.02; done" || timeout 15 sh -c "$1" || { echo "health timeout: $1"; exit 1; }; } php -d opcache.enable_cli=1 -S 127.0.0.1:8888 > /dev/null 2>&1 & ci_wait_until 'nc -w 1 127.0.0.1 8888' @@ -424,15 +431,31 @@ jobs: php demos/_demo-data/create-db.php vendor/bin/behat -vv --config behat.yml.dist - - name: Upload coverage logs 1/2 (only for latest Chrome) + - name: Upload PHP coverage logs 1/2 (only for coverage) if: env.LOG_COVERAGE run: | ls -l coverage | wc -l php -d memory_limit=2G vendor/bin/phpcov merge coverage/ --clover coverage/merged.xml - - name: Upload coverage logs 2/2 (only for latest Chrome) + - name: Upload PHP coverage logs 2/2 (only for coverage) if: env.LOG_COVERAGE uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} file: coverage/merged.xml + + - name: Upload JS coverage logs 1/2 (only for coverage) + if: env.LOG_COVERAGE + run: | + ls -l coverage/js | wc -l + (cd js && npx nyc report --temp-dir ../coverage/js --report-dir ../coverage/js --reporter=clover) + # fix never reached condition is rendered to clover with falsecount = 2 (or even higher if condition has multiple statements) + # https://github.com/istanbuljs/istanbuljs/issues/695 + sed -i -E 's~count="0" type="cond" truecount="0" falsecount="[1-9]+"~count="0" type="cond" truecount="0" falsecount="0"~' coverage/js/clover.xml + + - name: Upload JS coverage logs 2/2 (only for coverage) + if: env.LOG_COVERAGE + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: coverage/js/clover.xml diff --git a/src/Behat/Context.php b/src/Behat/Context.php index 301d743b74..426545f4ef 100644 --- a/src/Behat/Context.php +++ b/src/Behat/Context.php @@ -17,6 +17,7 @@ class Context extends RawMinkContext implements BehatContext { + use JsCoverageContextTrait; use WarnDynamicPropertyTrait; public function getSession($name = null): MinkSession @@ -73,10 +74,13 @@ public function waitUntilLoadingAndAnimationFinished(AfterStepScope $event): voi { $this->jqueryWait(); $this->disableAnimations(); + if (!str_contains($this->getScenario($event)->getTitle() ?? '', 'exception is displayed')) { $this->assertNoException(); } $this->assertNoDuplicateId(); + + $this->saveJsCoverage(); } protected function getFinishedScript(): string @@ -139,7 +143,7 @@ protected function disableAnimations(): void ]); $this->getSession()->executeScript( - 'if (Array.prototype.filter.call(document.getElementsByTagName("style"), e => e.getAttribute("about") === "atk-test-behat").length === 0) {' + 'if (Array.prototype.filter.call(document.getElementsByTagName("style"), (e) => e.getAttribute("about") === "atk-test-behat").length === 0) {' . ' $(\'\').appendTo(\'head\');' . ' }' . 'jQuery.fx.off = true;' diff --git a/src/Behat/JsCoverageContextTrait.php b/src/Behat/JsCoverageContextTrait.php new file mode 100644 index 0000000000..21bb75fa5f --- /dev/null +++ b/src/Behat/JsCoverageContextTrait.php @@ -0,0 +1,93 @@ +> */ + private array $jsCoverage = []; + + protected function isJsCoverageEnabled(): bool + { + return is_dir(__DIR__ . '/../../coverage/js'); + } + + public function __destruct() + { + if (!$this->isJsCoverageEnabled()) { + return; + } + + $outputFile = __DIR__ . '/../../coverage/js/' . hash('sha256', microtime(true) . random_bytes(64)) . '.json'; + file_put_contents($outputFile, json_encode($this->jsCoverage, \JSON_THROW_ON_ERROR)); + } + + protected function saveJsCoverage(): void + { + if (!$this->isJsCoverageEnabled()) { + return; + } + + $seenPaths = array_keys($this->jsCoverage); + $coverageAll = $this->getSession()->evaluateScript(<<<'EOF' + return (function (seenPaths) { + seenPaths = new Set(seenPaths); + const istanbulCoverage = window.__coverage__; + if (typeof istanbulCoverage !== 'object') { + throw '"window.__coverage__" is not defined'; + } + + const resAll = {}; + Object.entries(istanbulCoverage).forEach(([path, data]) => { + const res = {}; + Object.entries(data).forEach(([k, v]) => { + if (['statementMap', 'fnMap', 'branchMap'].includes(k) && seenPaths.has(path)) { + return; + } + if (typeof v === 'object') { + const vKeys = Object.keys(v); + if (JSON.stringify(vKeys) === JSON.stringify(vKeys.map((v, k) => k.toString()))) { + v = [...Object.values(v)]; + } + } + res[k] = v; + }); + resAll[path] = res; + }); + + return resAll; + })(arguments[0]); + EOF, [$seenPaths]); + + foreach ($coverageAll as $path => $data) { + if (!isset($this->jsCoverage[$path])) { + $this->jsCoverage[$path] = $data; + } else { + if ($this->jsCoverage[$path]['hash'] !== $data['hash'] + || $this->jsCoverage[$path]['_coverageSchema'] !== $data['_coverageSchema'] + || count($this->jsCoverage[$path]['s']) !== count($data['s']) + || count($this->jsCoverage[$path]['f']) !== count($data['f']) + || count($this->jsCoverage[$path]['b']) !== count($data['b']) + ) { + throw new Exception('Unexpected JS coverage hash change'); + } + + foreach (['s', 'f', 'b'] as $k) { + foreach ($data[$k] as $i => $n) { + if ($k === 'b') { + foreach ($n as $nI => $nN) { + $this->jsCoverage[$path][$k][$i][$nI] += $nN; + } + } else { + $this->jsCoverage[$path][$k][$i] += $n; + } + } + } + } + } + } +}