From 2191132c9d5265ec3b6ba4864c188d0d65330e05 Mon Sep 17 00:00:00 2001 From: lizhonghan Date: Sun, 25 May 2025 16:40:59 +0800 Subject: [PATCH] Add test coverage Issue: Change-Id: Ib2dac1c5397a1108b42a484f7a50ef89fe453718 Signed-off-by: lizhonghan --- ets2panda/linter/.gitignore | 1 + ets2panda/linter/package.json | 12 +- .../scripts/testRunner/coverage_collect.js | 121 ++++++++++++++++++ .../scripts/testRunner/coverage_prepare.js | 64 +++++++++ .../scripts/testRunner/coverage_report.js | 56 ++++++++ ets2panda/linter/src/testRunner/TestRunner.ts | 15 +++ ets2panda/linter/webpack.config.js | 2 +- 7 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 ets2panda/linter/scripts/testRunner/coverage_collect.js create mode 100644 ets2panda/linter/scripts/testRunner/coverage_prepare.js create mode 100644 ets2panda/linter/scripts/testRunner/coverage_report.js diff --git a/ets2panda/linter/.gitignore b/ets2panda/linter/.gitignore index 4343cbbd9d..a84c3073e0 100644 --- a/ets2panda/linter/.gitignore +++ b/ets2panda/linter/.gitignore @@ -5,3 +5,4 @@ dist node_modules package-lock.json panda-tslinter-1.0.0.tgz +coverage/ \ No newline at end of file diff --git a/ets2panda/linter/package.json b/ets2panda/linter/package.json index 73dd2a4180..d763f9a5b4 100644 --- a/ets2panda/linter/package.json +++ b/ets2panda/linter/package.json @@ -39,7 +39,13 @@ "eslint-check": "npx eslint .", "eslint-fix": "npm run eslint-check -- --fix", "prettier-fix": "npx prettier --write .", - "fix": "npm run prettier-fix && npm run eslint-fix" + "fix": "npm run prettier-fix && npm run eslint-fix", + "coverage": "npm run coverage-prepare && npm run coverage-instrument && npm run coverage-test && npm run coverage-collect && npm run coverage-report", + "coverage-prepare": "npm run build && node scripts/testRunner/coverage_prepare.js", + "coverage-instrument": "nyc --compact false instrument build coverage/build_instrument", + "coverage-test": "node coverage/build_instrument/testRunner/TestRunner.js -d test/main,test/rules,test/regression,test/extended_features,test/migration,test/ohmurl,test/interop,test/sdkwhite,test/concurrent,test/builtin", + "coverage-collect": "node scripts/testRunner/coverage_collect.js", + "coverage-report": "node scripts/testRunner/coverage_report.js" }, "dependencies": { "commander": "^9.4.0", @@ -66,7 +72,9 @@ "shelljs": "^0.8.5", "typescript-eslint": "latest", "webpack": "^5.75.0", - "webpack-cli": "^5.0.1" + "webpack-cli": "^5.0.1", + "nyc": "^15.1.0", + "source-map": "^0.7.4" }, "bundleDependencies": [ "log4js", diff --git a/ets2panda/linter/scripts/testRunner/coverage_collect.js b/ets2panda/linter/scripts/testRunner/coverage_collect.js new file mode 100644 index 0000000000..9d1a8a55f9 --- /dev/null +++ b/ets2panda/linter/scripts/testRunner/coverage_collect.js @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const fs = require('fs'); +const sourceMap = require('source-map'); +const path = require('path'); + +const projectRoot = path.join(__dirname, '..', '..'); +const coverageDir = path.join(projectRoot, 'coverage'); +const coverageFile = path.join(coverageDir, 'coverage.json'); +const buildDir = path.join(projectRoot, 'build'); +const instrumentDir = path.join(projectRoot, 'coverage', 'build_instrument'); + +async function collectCoverage() { + if (!fs.existsSync(coverageFile)) { + console.error(`Coverage file not found: ${coverageFile}`); + process.exit(1); + } + + const coverageData = JSON.parse(fs.readFileSync(coverageFile, 'utf8')); + let newCoverageData = {}; + + for (let file in coverageData) { + const mapFile = file + '.map'; + + if (!fs.existsSync(mapFile)) { + console.warn(`Source map not found for ${file}, expected file: ${mapFile}, relative path: ${relativePath}, original file: ${originalFile}`); + continue; + } + + const sourceMapData = JSON.parse(fs.readFileSync(mapFile, 'utf8')); + + await sourceMap.SourceMapConsumer.with(sourceMapData, null, (consumer) => { + const statementMap = coverageData[file].statementMap; + const functionMap = coverageData[file].functionMap; + const branchMap = coverageData[file].branchMap; + const sources = sourceMapData.sources; + const sourcesRelativePath = sources[0]; + const newFile = path.join(path.dirname(mapFile), sourcesRelativePath); + + newCoverageData[newFile] = coverageData[file]; + newCoverageData[newFile].path = newFile; + // statementMap + for (let i in statementMap) { + const statementStart = consumer.originalPositionFor(statementMap[i].start); + statementMap[i].start.line = statementStart.line; + statementMap[i].start.column = statementStart.column; + const statementEnd = consumer.originalPositionFor(statementMap[i].end); + statementMap[i].end.line = statementEnd.line; + statementMap[i].end.column = statementEnd.column; + } + newCoverageData[newFile].statementMap = statementMap; + // functionMap + for (let i in functionMap) { + // decl + const functionStart = consumer.originalPositionFor(functionMap[i].decl.start); + functionMap[i].decl.start.line = functionStart.line; + functionMap[i].decl.start.column = functionStart.column; + const functionEnd = consumer.originalPositionFor(functionMap[i].decl.end); + functionMap[i].decl.end.line = functionEnd.line; + functionMap[i].decl.end.column = functionEnd.column; + // loc + const functionLocStart = consumer.originalPositionFor(functionMap[i].loc.start); + functionMap[i].loc.start.line = functionLocStart.line; + functionMap[i].loc.start.column = functionLocStart.column; + const functionLocEnd = consumer.originalPositionFor(functionMap[i].loc.end); + functionMap[i].loc.end.line = functionLocEnd.line; + functionMap[i].loc.end.column = functionLocEnd.column; + // line + functionMap[i].line = functionStart.line; + } + newCoverageData[newFile].functionMap = functionMap; + // branchMap + for (let i in branchMap) { + // locations + const locations = branchMap[i].locations; + for (let j in locations) { + const locationStart = consumer.originalPositionFor(locations[j].start); + locations[j].start.line = locationStart.line; + locations[j].start.column = locationStart.column; + const locationEnd = consumer.originalPositionFor(locations[j].end); + locations[j].end.line = locationEnd.line; + locations[j].end.column = locationEnd.column; + } + branchMap[i].locations = locations; + // loc + const locStart = consumer.originalPositionFor(branchMap[i].loc.start); + branchMap[i].loc.start.line = locStart.line; + branchMap[i].loc.start.column = locStart.column; + const locEnd = consumer.originalPositionFor(branchMap[i].loc.end); + branchMap[i].loc.end.line = locEnd.line; + branchMap[i].loc.end.column = locEnd.column; + // line + branchMap[i].line = locStart.line; + } + newCoverageData[newFile].branchMap = branchMap; + }); + } + + fs.writeFileSync( + path.join(projectRoot, 'coverage', 'newCoverage.json'), + JSON.stringify(newCoverageData, null, 4) + ); +} + +collectCoverage().catch(error => { + console.error('Error collecting coverage:', error); + process.exit(1); +}); \ No newline at end of file diff --git a/ets2panda/linter/scripts/testRunner/coverage_prepare.js b/ets2panda/linter/scripts/testRunner/coverage_prepare.js new file mode 100644 index 0000000000..2a746b4b09 --- /dev/null +++ b/ets2panda/linter/scripts/testRunner/coverage_prepare.js @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const fs = require('fs'); +const path = require('path'); + +const projectRoot = path.join(__dirname, '..', '..'); +const buildDir = path.join(projectRoot, 'build'); +const coverageDir = path.join(projectRoot, 'coverage'); +const buildInstrumentDir = path.join(coverageDir, 'build_instrument'); + +function copyDirectory(src, dest) { + fs.mkdirSync(dest, { recursive: true }); + + const entries = fs.readdirSync(src, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + + if (entry.isDirectory()) { + copyDirectory(srcPath, destPath); + } else { + fs.copyFileSync(srcPath, destPath); + } + } +} + +function prepareCoverage() { + try { + if (fs.existsSync(coverageDir)) { + fs.rmSync(coverageDir, { recursive: true, force: true }); + } + + fs.mkdirSync(coverageDir, { recursive: true }); + fs.mkdirSync(buildInstrumentDir, { recursive: true }); + + copyDirectory(buildDir, buildInstrumentDir); + + const dataDir = path.join(projectRoot, 'src', 'data'); + const instrumentDataDir = path.join(buildInstrumentDir, 'data'); + + if (fs.existsSync(dataDir)) { + copyDirectory(dataDir, instrumentDataDir); + } + } catch (error) { + console.error('Error during coverage preparation:', error); + process.exit(1); + } +} + +prepareCoverage(); \ No newline at end of file diff --git a/ets2panda/linter/scripts/testRunner/coverage_report.js b/ets2panda/linter/scripts/testRunner/coverage_report.js new file mode 100644 index 0000000000..84fa3219d0 --- /dev/null +++ b/ets2panda/linter/scripts/testRunner/coverage_report.js @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const fs = require('fs'); +const path = require('path'); +const libCoverage = require('istanbul-lib-coverage'); +const libReport = require('istanbul-lib-report'); +const reports = require('istanbul-reports'); + +const projectRoot = path.join(__dirname, '..', '..'); +const coverageDir = path.join(projectRoot, 'coverage'); + +const coverageFile = fs.readFileSync(path.join(coverageDir, 'newCoverage.json'), 'utf8'); +const coverageData = JSON.parse(coverageFile); +const coverageMap = libCoverage.createCoverageMap(coverageData); + +// create summary report +const summary = libCoverage.createCoverageSummary(); +coverageMap.files().forEach(file => { + const fc = coverageMap.fileCoverageFor(file); + const s = fc.toSummary(); + summary.merge(s); +}); +console.log(summary); + +// Watermarks for the report +const configWatermarks = { + statements: [50, 80], + branches: [50, 80], + functions: [50, 80], + lines: [50, 80], +}; +const context = libReport.createContext({ + dir: path.join(coverageDir, 'report-html'), + defaultSummarizer: 'nested', + watermarks: configWatermarks, + coverageMap, +}); + +const report = reports.create('html', {}); +report.execute(context); + +const report_text = reports.create('text', {}); +report_text.execute(context); \ No newline at end of file diff --git a/ets2panda/linter/src/testRunner/TestRunner.ts b/ets2panda/linter/src/testRunner/TestRunner.ts index 4d3639fb32..1f068e2f51 100755 --- a/ets2panda/linter/src/testRunner/TestRunner.ts +++ b/ets2panda/linter/src/testRunner/TestRunner.ts @@ -143,6 +143,21 @@ function runTests(): boolean { const { passed, failed } = testStats; Logger.info(`\nSUMMARY: ${passed + failed} total, ${passed} passed, ${failed} failed.`); Logger.info(failed > 0 ? '\nTEST FAILED' : '\nTEST SUCCESSFUL'); + + const saveCoverageData = (): void => { + const coverageData = globalThis.__coverage__; + if (coverageData) { + const projectRoot = path.resolve(__dirname, '../../..'); + const coverageDir = path.join(projectRoot, 'coverage'); + fs.mkdirSync(coverageDir, { recursive: true }); + + const coverageFile = path.join(coverageDir, 'coverage.json'); + fs.writeFileSync(coverageFile, JSON.stringify(coverageData, null, 4)); + } else { + console.log('no coverage data found'); + } + }; + saveCoverageData(); process.exit(failed > 0 ? -1 : 0); } diff --git a/ets2panda/linter/webpack.config.js b/ets2panda/linter/webpack.config.js index 15ee37c29b..99b46e1479 100644 --- a/ets2panda/linter/webpack.config.js +++ b/ets2panda/linter/webpack.config.js @@ -17,7 +17,7 @@ let path = require('path'); module.exports = { mode: 'development', - target: 'node', + target: 'node', entry: './build/cli/main.js', externalsType: 'commonjs', externals: { -- Gitee