* @fileoverview Takes raw v8 coverage files and converts to IstanbulJS
* compliant coverage files.
*/
const NODE_MODULES = [
'..', '..', '..', 'third_party', 'js_code_coverage', 'node_modules'];
const {createHash} = require('crypto');
const {join, dirname} = require('path');
const {readdir, readFile, writeFile, mkdir, access} = require('fs').promises;
const V8ToIstanbul = require(join(...NODE_MODULES, 'v8-to-istanbul'));
const {ArgumentParser} = require(join(...NODE_MODULES, 'argparse'));
const convertSourceMap = require(join(...NODE_MODULES, 'convert-source-map'));
const sourceMap = require(join(...NODE_MODULES, 'source-map'));
* Validate the sourcemap by looking at the start and end positions
* of the source file if present.
* @param {string} instrumentedFilePath Path to the file with source map.
* @returns {boolean} true if sourcemap is valid, false otherwise
*/
async function validateSourceMaps(instrumentedFilePath) {
const rawSource = await readFile(instrumentedFilePath, 'utf8');
const rawSourceMap = convertSourceMap.fromSource(rawSource) ||
convertSourceMap.fromMapFileSource(
rawSource, dirname(instrumentedFilePath));
if (!rawSourceMap || rawSourceMap.sourcemap.sources.length < 1) {
return false
}
let exists = false
const consumer =
await new sourceMap.SourceMapConsumer(rawSourceMap.sourcemap);
let result = consumer.originalPositionFor({line: 1, column: 0});
let file = null
try {
file = await readFile(result.source, 'utf8');
} catch(error) {
if (error.code === 'ENOENT') {
exists = false
} else {
throw error;
}
}
if (!file) {
consumer.destroy();
return exists;
}
const contents = file.toString();
const lines = contents.split("\n");
result = consumer.originalPositionFor({line: lines.length, column: 0});
if (checkSource(result.source)) {
exists = true
} else {
throw new Error(`Original source missing for ${instrumentedFilePath}`)
}
consumer.destroy();
return exists;
}
* Check if the source exists on disk.
* @param {string} url Path to the source file
* @returns {boolean} True if exists, false otherwise
*/
async function checkSource(url) {
try {
await access(url)
return true
} catch(error) {
return false
}
}
* Extracts the raw coverage data from the v8 coverage reports and converts
* them into IstanbulJS compliant reports.
* @param {string} coverageDirectory Directory containing the raw v8 output.
* @param {string} instrumentedDirectoryRoot Directory containing the source
* files where the coverage was instrumented from.
* @param {string} outputDir Directory to store the istanbul coverage reports.
* @param {!Map<string, string>} urlToPathMap A mapping of URL observed during
* test execution to the on-disk location created in previous steps.
*/
async function extractCoverage(
coverageDirectory, instrumentedDirectoryRoot, outputDir, urlToPathMap) {
const coverages = await readdir(coverageDirectory);
for (const fileName of coverages) {
if (!fileName.endsWith('.cov.json'))
continue;
const filePath = join(coverageDirectory, fileName);
const contents = await readFile(filePath, 'utf-8');
const {result: scriptCoverages} = JSON.parse(contents);
if (!scriptCoverages)
throw new Error(`result key missing for file: ${filePath}`);
for (const coverage of scriptCoverages) {
if (!urlToPathMap[coverage.url])
continue;
const instrumentedFilePath =
join(instrumentedDirectoryRoot, urlToPathMap[coverage.url]);
const validSourceMap = await validateSourceMaps(instrumentedFilePath)
if (!validSourceMap) {
console.log(`Original source missing for ${instrumentedFilePath}.`);
continue;
}
const converter = V8ToIstanbul(instrumentedFilePath);
await converter.load();
converter.applyCoverage(coverage.functions);
const convertedCoverage = converter.toIstanbul();
const jsonString = JSON.stringify(convertedCoverage);
await writeFile(
join(outputDir, createSHA1HashFromFileContents(jsonString) + '.json'),
jsonString);
}
}
}
* Helper function to provide a unique file name for resultant istanbul reports.
* @param {string} str File contents
* @return {string} A sha1 hash to be used as a file name.
*/
function createSHA1HashFromFileContents(contents) {
return createHash('sha1').update(contents).digest('hex');
}
* The entry point to the function to enable the async functionality throughout.
* @param {Object} args The parsed CLI arguments.
* @return {!Promise<string>} Directory containing istanbul reports.
*/
async function main(args) {
const urlToPathMapFile =
await readFile(join(args.source_dir, 'parsed_scripts.json'));
const urlToPathMap = JSON.parse(urlToPathMapFile.toString());
const outputDir = join(args.output_dir, 'istanbul')
await mkdir(outputDir, {recursive: true});
for (const coverageDir of args.raw_coverage_dirs)
await extractCoverage(
coverageDir, args.source_dir, outputDir, urlToPathMap);
return outputDir;
}
const parser = new ArgumentParser({
description: 'Converts raw v8 coverage into IstanbulJS compliant files.',
});
parser.add_argument('-s', '--source-dir', {
required: true,
help: 'Root directory where source files live. The corresponding ' +
'coverage files must refer to these files. Currently source ' +
'maps are not supported.',
});
parser.add_argument('-o', '--output-dir', {
required: true,
help: 'Root directory to output all the converted istanbul coverage ' +
'reports.',
});
parser.add_argument('-c', '--raw-coverage-dirs', {
required: true,
nargs: '*',
help: 'Directory that contains the raw v8 coverage files (files ' +
'ending in .cov.json)',
});
const args = parser.parse_args();
main(args)
.then(outputDir => {
console.log(`Successfully converted from v8 to IstanbulJS: ${outputDir}`);
})
.catch(error => {
console.error(error);
process.exit(1);
});