Instrument code with Istanbul for interactions tests
All interaction tests are now lazily instrumented with Istanbul
to obtain code coverage. The interactions tests can be started
with `COVERAGE=1` to obtain coverage. For that, the Mocha hooks
perform the eventual reporting and gathering of data. The instrumentation
is performed in the components server itself.
To make sure that we perform the minimal amount of work required
(since code coverage instrumentation is computationally expensive),
we preload pages to populate the instrumentation cache. Every
interactions tests should preload an example (most likely basic.html)
to populate the cache. Every subsequent test will then use the
already-instrumented code, rather than computing the code over
and over again.
The eventual code coverage is written to /interactions-coverage.
The results will eventually be merged with /karma-coverage
to obtain the union of both unit and interaction tests coverage.
R=aerotwist@chromium.org,jacktfranklin@chromium.org
Bug: 1206705
Change-Id: I5e19b1ecef23d21107210699cb29800556e0415e
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2879986
Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org>
Reviewed-by: Jack Franklin <jacktfranklin@chromium.org>
diff --git a/scripts/component_server/server.js b/scripts/component_server/server.js
index 91b5b86..d252bff 100644
--- a/scripts/component_server/server.js
+++ b/scripts/component_server/server.js
@@ -7,6 +7,11 @@
const path = require('path');
const parseURL = require('url').parse;
const {argv} = require('yargs');
+
+const {createInstrumenter} = require('istanbul-lib-instrument');
+const convertSourceMap = require('convert-source-map');
+const defaultIstanbulSchema = require('@istanbuljs/schema');
+
const {getTestRunnerConfigSetting} = require('../test/test_config_helpers.js');
@@ -202,6 +207,38 @@
}
}
+const EXCLUDED_COVERAGE_FOLDERS = new Set(['third_party', 'ui/components/docs', 'Images']);
+
+/**
+ * @param {string} filePath
+ * @returns {boolean}
+ */
+function isIncludedForCoverageComputation(filePath) {
+ for (const excludedFolder of EXCLUDED_COVERAGE_FOLDERS) {
+ if (filePath.startsWith(`/front_end/${excludedFolder}/`)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+const COVERAGE_INSTRUMENTER = createInstrumenter({
+ esModules: true,
+ parserPlugins: [
+ ...defaultIstanbulSchema.instrumenter.properties.parserPlugins.default,
+ 'topLevelAwait',
+ ],
+});
+
+const instrumentedSourceCacheForFilePaths = new Map();
+
+const SHOULD_GATHER_COVERAGE_INFORMATION = process.env.COVERAGE === '1';
+
+/**
+ * @param {http.IncomingMessage} request
+ * @param {http.ServerResponse} response
+ */
async function requestHandler(request, response) {
const filePath = parseURL(request.url).pathname;
if (filePath === '/favicon.ico') {
@@ -297,7 +334,47 @@
encoding = 'binary';
}
- const fileContents = await fs.promises.readFile(fullPath, encoding);
+ let fileContents = await fs.promises.readFile(fullPath, encoding);
+ const isComputingCoverageRequest = request.headers['devtools-compute-coverage'] === '1';
+
+ if (SHOULD_GATHER_COVERAGE_INFORMATION && fullPath.endsWith('.js') && filePath.startsWith('/front_end/') &&
+ isIncludedForCoverageComputation(filePath)) {
+ const previouslyGeneratedInstrumentedSource = instrumentedSourceCacheForFilePaths.get(fullPath);
+
+ if (previouslyGeneratedInstrumentedSource) {
+ fileContents = previouslyGeneratedInstrumentedSource;
+ } else {
+ if (!isComputingCoverageRequest) {
+ response.writeHead(400);
+ response.write(`Invalid coverage request. Attempted to load ${request.url}.`, 'utf8');
+ response.end();
+
+ console.error(
+ `Invalid coverage request. Attempted to load ${request.url} which was not available in the ` +
+ 'code coverage instrumentation cache. Make sure that you call `preloadForCodeCoverage` in the describe block ' +
+ 'of your interactions test, before declaring any tests.\n');
+ return;
+ }
+
+ fileContents = await new Promise(async (resolve, reject) => {
+ let sourceMap = convertSourceMap.fromSource(fileContents);
+ if (!sourceMap) {
+ sourceMap = convertSourceMap.fromMapFileSource(fileContents, path.dirname(fullPath));
+ }
+
+ COVERAGE_INSTRUMENTER.instrument(fileContents, fullPath, (error, instrumentedSource) => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(instrumentedSource);
+ }
+ }, sourceMap ? sourceMap.sourcemap : undefined);
+ });
+
+ instrumentedSourceCacheForFilePaths.set(fullPath, fileContents);
+ }
+ }
+
response.writeHead(200);
response.write(fileContents, encoding);
response.end();