• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2021 The Chromium Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * @fileoverview Takes raw v8 coverage files and converts to IstanbulJS
7 * compliant coverage files.
8 */
9
10// Relative path to the node modules.
11const NODE_MODULES = [
12  '..', '..', '..', 'third_party', 'js_code_coverage', 'node_modules'];
13
14const {createHash} = require('crypto');
15const {join, dirname, normalize} = require('path');
16const {readdir, readFile, writeFile, mkdir, access} = require('fs').promises;
17const V8ToIstanbul = require(join(...NODE_MODULES, 'v8-to-istanbul'));
18const {ArgumentParser} = require(join(...NODE_MODULES, 'argparse'));
19const convertSourceMap = require(join(...NODE_MODULES, 'convert-source-map'));
20const sourceMap = require(join(...NODE_MODULES, 'source-map'));
21
22/**
23 * Validate that the mapping in the sourcemaps is valid.
24 * @param {Object} mapping Individual mapping to validate.
25 * @param {Map} sourcesMap Map of the sources in the mappings to it's content.
26 * @param {string} instrumentedFilePath Path to the instrumented file.
27 * @returns {boolean} true if mapping is valid, false otherwise.
28 */
29function validateMapping(mapping, sourcesMap, instrumentedFilePath) {
30  if (!mapping.generatedLine || !mapping.originalLine || !mapping.source) {
31    console.log(`Invalid mapping found for ${instrumentedFilePath}`);
32    return false;
33  }
34
35  // Verify that we have file contents.
36  if (!sourcesMap[mapping.source]) {
37    return false;
38  }
39
40  // Verify that the mapping line numbers refers to actual lines in source.
41  const origLine = sourcesMap[mapping.source][mapping.originalLine-1];
42  const genLine = sourcesMap[instrumentedFilePath][mapping.generatedLine-1];
43  if (origLine === undefined || genLine === undefined) {
44    return false;
45  }
46
47  // Verify that the mapping columns refers to actual column bounds in source.
48  if (mapping.generatedColumn > genLine.length ||
49      mapping.originalColumn > origLine.length) {
50    return false;
51  }
52
53  return true;
54}
55
56/**
57 * Validate the sourcemap by looking at:
58 * 1. Existence of the source files in sourcemap
59 * 2. Verify original and generated lines are within bounds.
60 * @param {string} instrumentedFilePath Path to the file with source map.
61 * @returns {boolean} true if sourcemap is valid, false otherwise
62 */
63async function validateSourceMaps(instrumentedFilePath) {
64  const rawSource = await readFile(instrumentedFilePath, 'utf8');
65  const rawSourceMap = convertSourceMap.fromSource(rawSource) ||
66      convertSourceMap.fromMapFileSource(
67        rawSource, dirname(instrumentedFilePath));
68
69  if (!rawSourceMap || rawSourceMap.sourcemap.sources.length < 1) {
70    console.log(`No valid source map found for ${instrumentedFilePath}`);
71    return false;
72  }
73
74  let sourcesMap = {};
75  sourcesMap[instrumentedFilePath] = rawSource.toString().split('\n');
76  for (let i = 0; i < rawSourceMap.sourcemap.sources.length; i++) {
77    const sourcePath = normalize(join(
78        rawSourceMap.sourcemap.sourceRoot, rawSourceMap.sourcemap.sources[i]));
79    try {
80      const content = await readFile(sourcePath, 'utf-8');
81      sourcesMap[sourcePath] = content.toString().split('\n');
82    } catch (error) {
83      if (error.code === 'ENOENT') {
84        console.error(`Original missing for ${sourcePath}`);
85        return false;
86      } else {
87        throw error;
88      }
89    }
90  }
91
92  let validMap = true;
93  const consumer =
94      await new sourceMap.SourceMapConsumer(rawSourceMap.sourcemap);
95  consumer.eachMapping(function(mapping) {
96    if (!validMap ||
97      !validateMapping(mapping, sourcesMap, instrumentedFilePath)) {
98      validMap = false;
99    }
100  });
101
102  // Destroy consumer as we dont need it anymore.
103  consumer.destroy();
104  return validMap;
105}
106
107/**
108 * Extracts the raw coverage data from the v8 coverage reports and converts
109 * them into IstanbulJS compliant reports.
110 * @param {string} coverageDirectory Directory containing the raw v8 output.
111 * @param {string} instrumentedDirectoryRoot Directory containing the source
112 *    files where the coverage was instrumented from.
113 * @param {string} outputDir Directory to store the istanbul coverage reports.
114 * @param {!Map<string, string>} urlToPathMap A mapping of URL observed during
115 *    test execution to the on-disk location created in previous steps.
116 */
117async function extractCoverage(
118    coverageDirectory, instrumentedDirectoryRoot, outputDir, urlToPathMap) {
119  const coverages = await readdir(coverageDirectory);
120
121  for (const fileName of coverages) {
122    if (!fileName.endsWith('.cov.json'))
123      continue;
124
125    const filePath = join(coverageDirectory, fileName);
126    const contents = await readFile(filePath, 'utf-8');
127
128    const {result: scriptCoverages} = JSON.parse(contents);
129    if (!scriptCoverages)
130      throw new Error(`result key missing for file: ${filePath}`);
131
132    for (const coverage of scriptCoverages) {
133      if (!urlToPathMap[coverage.url])
134        continue;
135
136      const instrumentedFilePath =
137          join(instrumentedDirectoryRoot, urlToPathMap[coverage.url]);
138      const validSourceMap = await validateSourceMaps(instrumentedFilePath)
139      if (!validSourceMap) {
140        continue;
141      }
142
143      const converter = V8ToIstanbul(instrumentedFilePath);
144      await converter.load();
145      converter.applyCoverage(coverage.functions);
146      const convertedCoverage = converter.toIstanbul();
147
148      const jsonString = JSON.stringify(convertedCoverage);
149      await writeFile(
150          join(outputDir, createSHA1HashFromFileContents(jsonString) + '.json'),
151          jsonString);
152    }
153  }
154}
155
156/**
157 * Helper function to provide a unique file name for resultant istanbul reports.
158 * @param {string} str File contents
159 * @return {string} A sha1 hash to be used as a file name.
160 */
161function createSHA1HashFromFileContents(contents) {
162  return createHash('sha1').update(contents).digest('hex');
163}
164
165/**
166 * The entry point to the function to enable the async functionality throughout.
167 * @param {Object} args The parsed CLI arguments.
168 * @return {!Promise<string>} Directory containing istanbul reports.
169 */
170async function main(args) {
171  const urlToPathMapFile =
172      await readFile(join(args.source_dir, 'parsed_scripts.json'));
173  const urlToPathMap = JSON.parse(urlToPathMapFile.toString());
174
175  const outputDir = join(args.output_dir, 'istanbul')
176  await mkdir(outputDir, {recursive: true});
177  for (const coverageDir of args.raw_coverage_dirs)
178    await extractCoverage(
179        coverageDir, args.source_dir, outputDir, urlToPathMap);
180
181  return outputDir;
182}
183
184const parser = new ArgumentParser({
185  description: 'Converts raw v8 coverage into IstanbulJS compliant files.',
186});
187
188parser.add_argument('-s', '--source-dir', {
189  required: true,
190  help: 'Root directory where source files live. The corresponding ' +
191      'coverage files must refer to these files. Currently source ' +
192      'maps are not supported.',
193});
194parser.add_argument('-o', '--output-dir', {
195  required: true,
196  help: 'Root directory to output all the converted istanbul coverage ' +
197      'reports.',
198});
199parser.add_argument('-c', '--raw-coverage-dirs', {
200  required: true,
201  nargs: '*',
202  help: 'Directory that contains the raw v8 coverage files (files ' +
203      'ending in .cov.json)',
204});
205
206const args = parser.parse_args();
207main(args)
208    .then(outputDir => {
209      console.log(`Successfully converted from v8 to IstanbulJS: ${outputDir}`);
210    })
211    .catch(error => {
212      console.error(error);
213      process.exit(1);
214    });
215