diff --git a/src/executeTestPlan.js b/src/executeTestPlan.js index 45092ecc87..62c232ab59 100644 --- a/src/executeTestPlan.js +++ b/src/executeTestPlan.js @@ -59,6 +59,8 @@ export const executeTestPlan = async ({ coverageConfig = jsenvCoverageConfig, coverageIncludeMissing = true, coverageAndExecutionAllowed = false, + coverageForceIstanbul = false, + coverageTextLog = true, coverageJsonFile = Boolean(process.env.CI), coverageJsonFileLog = true, @@ -167,6 +169,7 @@ export const executeTestPlan = async ({ coverage, coverageConfig, coverageIncludeMissing, + coverageForceIstanbul, ...rest, }) diff --git a/src/internal/browser-launcher/executeHtmlFile.js b/src/internal/browser-launcher/executeHtmlFile.js index 9de241a29d..4ffa0477a6 100644 --- a/src/internal/browser-launcher/executeHtmlFile.js +++ b/src/internal/browser-launcher/executeHtmlFile.js @@ -1,11 +1,9 @@ import { extname } from "path" import { resolveUrl, assertFilePresence } from "@jsenv/util" -import { normalizeIstanbulCoverage } from "@jsenv/core/src/internal/executing/coverage/normalizeIstanbulCoverage.js" import { composeIstanbulCoverages } from "@jsenv/core/src/internal/executing/coverage/composeIstanbulCoverages.js" import { evalSource } from "../runtime/createNodeRuntime/evalSource.js" import { escapeRegexpSpecialCharacters } from "../escapeRegexpSpecialCharacters.js" -import { projectDirectoryUrl } from "@jsenv/core/jsenv.config.js" export const executeHtmlFile = async ( fileRelativeUrl, @@ -82,14 +80,14 @@ export const executeHtmlFile = async ( status: "errored", error: evalException(exceptionSource, { projectDirectoryUrl, compileServerOrigin }), namespace: fileExecutionResultMap, - readCoverage: () => generateCoverageForPage(fileExecutionResultMap), + coverageMap: generateCoverageForPage(fileExecutionResultMap), } } return { status: "completed", namespace: fileExecutionResultMap, - readCoverage: () => generateCoverageForPage(fileExecutionResultMap), + coverageMap: generateCoverageForPage(fileExecutionResultMap), } } @@ -98,11 +96,7 @@ const generateCoverageForPage = (fileExecutionResultMap) => { Object.keys(fileExecutionResultMap).forEach((fileRelativeUrl) => { const istanbulCoverage = fileExecutionResultMap[fileRelativeUrl].coverageMap if (istanbulCoverage) { - const istanbulCoverageNormalized = normalizeIstanbulCoverage( - istanbulCoverage, - projectDirectoryUrl, - ) - istanbulCoverages.push(istanbulCoverageNormalized) + istanbulCoverages.push(istanbulCoverage) } }) const istanbulCoverage = composeIstanbulCoverages(...istanbulCoverages) diff --git a/src/internal/executing/coverage/istanbulCoverageFromV8Coverage.js b/src/internal/executing/coverage/istanbulCoverageFromV8Coverage.js index 048fc4d018..a306373c67 100644 --- a/src/internal/executing/coverage/istanbulCoverageFromV8Coverage.js +++ b/src/internal/executing/coverage/istanbulCoverageFromV8Coverage.js @@ -9,7 +9,6 @@ import { } from "@jsenv/util" import { require } from "@jsenv/core/src/internal/require.js" import { composeIstanbulCoverages } from "./composeIstanbulCoverages.js" -import { normalizeIstanbulCoverage } from "./normalizeIstanbulCoverage.js" const { mergeProcessCovs } = require("@c88/v8-coverage") @@ -38,7 +37,6 @@ export const istanbulCoverageFromV8Coverage = async ({ const coverageReport = mergeCoverageReports(coverageReportsFiltered) const istanbulCoverage = await convertV8CoverageToIstanbul(coverageReport, { - projectDirectoryUrl, sourceMapCache, }) return istanbulCoverage @@ -91,10 +89,7 @@ const mergeCoverageReports = (coverageReports) => { return coverageReport } -const convertV8CoverageToIstanbul = async ( - coverageReport, - { projectDirectoryUrl, sourceMapCache }, -) => { +const convertV8CoverageToIstanbul = async (coverageReport, { sourceMapCache }) => { const istanbulCoverages = await Promise.all( coverageReport.result.map(async (fileV8Coverage) => { const sources = sourcesFromSourceMapCache(fileV8Coverage.url, sourceMapCache) @@ -110,11 +105,7 @@ const convertV8CoverageToIstanbul = async ( converter.applyCoverage(fileV8Coverage.functions) const istanbulCoverage = converter.toIstanbul() - const istanbulCoverageNormalized = normalizeIstanbulCoverage( - istanbulCoverage, - projectDirectoryUrl, - ) - return istanbulCoverageNormalized + return istanbulCoverage }), ) diff --git a/src/internal/executing/coverage/reportToCoverageMap.js b/src/internal/executing/coverage/reportToCoverageMap.js index f5439a3d69..1e7cb9360d 100644 --- a/src/internal/executing/coverage/reportToCoverageMap.js +++ b/src/internal/executing/coverage/reportToCoverageMap.js @@ -6,6 +6,7 @@ import { normalizeIstanbulCoverage } from "./normalizeIstanbulCoverage.js" export const reportToCoverageMap = async ( report, { + logger, cancellationToken, projectDirectoryUrl, babelPluginMap, @@ -13,10 +14,14 @@ export const reportToCoverageMap = async ( coverageIncludeMissing, }, ) => { - const coverageMapForReport = executionReportToCoverageMap(report) + const istanbulCoverageFromExecutionRaw = executionReportToCoverageMap(report, { logger }) + const istanbulCoverageFromExecution = normalizeIstanbulCoverage( + istanbulCoverageFromExecutionRaw, + projectDirectoryUrl, + ) if (!coverageIncludeMissing) { - return coverageMapForReport + return istanbulCoverageFromExecution } const relativeFileUrlToCoverArray = await listRelativeFileUrlToCover({ @@ -27,12 +32,12 @@ export const reportToCoverageMap = async ( const relativeFileUrlMissingCoverageArray = relativeFileUrlToCoverArray.filter( (relativeFileUrlToCover) => - Object.keys(coverageMapForReport).every((key) => { + Object.keys(istanbulCoverageFromExecution).every((key) => { return key !== `./${relativeFileUrlToCover}` }), ) - const coverageMapForMissedFiles = {} + const istanbulCoverageFromMissedFiles = {} await Promise.all( relativeFileUrlMissingCoverageArray.map(async (relativeFileUrlMissingCoverage) => { const emptyCoverage = await relativeUrlToEmptyCoverage(relativeFileUrlMissingCoverage, { @@ -40,14 +45,14 @@ export const reportToCoverageMap = async ( projectDirectoryUrl, babelPluginMap, }) - coverageMapForMissedFiles[relativeFileUrlMissingCoverage] = emptyCoverage + istanbulCoverageFromMissedFiles[relativeFileUrlMissingCoverage] = emptyCoverage return emptyCoverage }), ) return { - ...coverageMapForReport, // already normalized - ...normalizeIstanbulCoverage(coverageMapForMissedFiles, projectDirectoryUrl), + ...istanbulCoverageFromExecution, // already normalized + ...normalizeIstanbulCoverage(istanbulCoverageFromMissedFiles, projectDirectoryUrl), } } @@ -70,15 +75,15 @@ const listRelativeFileUrlToCover = async ({ return matchingFileResultArray.map(({ relativeUrl }) => relativeUrl) } -const executionReportToCoverageMap = (report) => { - const coverageMapArray = [] +const executionReportToCoverageMap = (report, { logger }) => { + const istanbulCoverages = [] Object.keys(report).forEach((file) => { const executionResultForFile = report[file] Object.keys(executionResultForFile).forEach((executionName) => { const executionResultForFileOnRuntime = executionResultForFile[executionName] - const { coverageMap } = executionResultForFileOnRuntime + const { status, coverageMap } = executionResultForFileOnRuntime if (!coverageMap) { // several reasons not to have coverageMap here: // 1. the file we executed did not import an instrumented file. @@ -97,15 +102,18 @@ const executionReportToCoverageMap = (report) => { // in any scenario we are fine because // coverDescription will generate empty coverage for files // that were suppose to be coverage but were not. + + if (status === "completed") { + logger.warn(`No execution.coverageMap from execution named "${executionName}" of ${file}`) + } return } - coverageMapArray.push(coverageMap) + istanbulCoverages.push(coverageMap) }) }) - debugger - const executionCoverageMap = composeIstanbulCoverages(...coverageMapArray) + const istanbulCoverage = composeIstanbulCoverages(...istanbulCoverages) - return executionCoverageMap + return istanbulCoverage } diff --git a/src/internal/executing/executeConcurrently.js b/src/internal/executing/executeConcurrently.js index 0f44db43e4..8e6ed10216 100644 --- a/src/internal/executing/executeConcurrently.js +++ b/src/internal/executing/executeConcurrently.js @@ -41,6 +41,7 @@ export const executeConcurrently = async ( coverage, coverageConfig, coverageIncludeMissing, + coverageForceIstanbul, ...rest }, @@ -160,6 +161,7 @@ export const executeConcurrently = async ( fileRelativeUrl, collectCoverage, coverageConfig, + coverageForceIstanbul, ...rest, }) @@ -239,6 +241,7 @@ export const executeConcurrently = async ( ...(coverage ? { coverageMap: await reportToCoverageMap(report, { + logger, cancellationToken, projectDirectoryUrl, babelPluginMap, diff --git a/src/internal/executing/executePlan.js b/src/internal/executing/executePlan.js index cecca6b260..4e379f3620 100644 --- a/src/internal/executing/executePlan.js +++ b/src/internal/executing/executePlan.js @@ -35,10 +35,10 @@ export const executePlan = async ( logSummary, measureGlobalDuration, - // coverage parameters coverage, coverageConfig, coverageIncludeMissing, + coverageForceIstanbul, ...rest } = {}, @@ -109,6 +109,7 @@ export const executePlan = async ( coverage, coverageConfig, coverageIncludeMissing, + coverageForceIstanbul, ...rest, }) diff --git a/src/internal/executing/launchAndExecute.js b/src/internal/executing/launchAndExecute.js index da742e022b..8bf79c1e0a 100644 --- a/src/internal/executing/launchAndExecute.js +++ b/src/internal/executing/launchAndExecute.js @@ -43,6 +43,7 @@ export const launchAndExecute = async ({ inheritCoverage = false, collectCoverage = false, coverageConfig, + coverageForceIstanbul, ...rest } = {}) => { const logger = createLogger({ logLevel: executionLogLevel }) @@ -156,6 +157,7 @@ export const launchAndExecute = async ({ runtimeStoppedCallback, collectCoverage, coverageConfig, + coverageForceIstanbul, ...rest, }) @@ -236,6 +238,7 @@ const computeExecutionResult = async ({ runtimeDisconnectCallback, collectCoverage, + coverageForceIstanbul, ...rest }) => { @@ -248,6 +251,7 @@ const computeExecutionResult = async ({ cancellationToken, logger, collectCoverage, + coverageForceIstanbul, ...rest, }) runtimeStartedCallback({ name: value.name, version: value.version }) @@ -315,6 +319,7 @@ const computeExecutionResult = async ({ registerErrorCallback, registerConsoleCallback, disconnected, + finalizeExecutionResult = (executionResult) => executionResult, } = await launchOperation const runtime = `${runtimeName}/${runtimeVersion}` @@ -359,7 +364,7 @@ const computeExecutionResult = async ({ timing = TIMING_AFTER_EXECUTION if (raceResult.winner === disconnected) { - return createDisconnectedExecutionResult({}) + return createDisconnectedExecutionResult() } if (stopAfterExecute) { @@ -381,17 +386,11 @@ const computeExecutionResult = async ({ ["runtime"]: runtime, }), ) - return { - ...createErroredExecutionResult(executionResult.error), - ...(collectCoverage ? { coverageMap: await executionResult.readCoverage() } : {}), - } + return finalizeExecutionResult(createErroredExecutionResult(executionResult)) } logger.debug(`${fileRelativeUrl} ${runtime}: execution completed.`) - return { - ...createCompletedExecutionResult(executionResult.namespace), - ...(collectCoverage ? { coverageMap: await executionResult.readCoverage() } : {}), - } + return finalizeExecutionResult(createCompletedExecutionResult(executionResult)) }, }) @@ -412,17 +411,18 @@ const createDisconnectedExecutionResult = () => { } } -const createErroredExecutionResult = (error) => { +const createErroredExecutionResult = (executionResult) => { return { + ...executionResult, status: "errored", - error, } } -const createCompletedExecutionResult = (namespace) => { +const createCompletedExecutionResult = (executionResult) => { return { + ...executionResult, status: "completed", - namespace: normalizeNamespace(namespace), + namespace: normalizeNamespace(executionResult.namespace), } } diff --git a/src/launchNode.js b/src/launchNode.js index 4225f1d42a..ed311a8bb3 100644 --- a/src/launchNode.js +++ b/src/launchNode.js @@ -1,14 +1,17 @@ +/* eslint-disable import/max-dependencies */ import { Script } from "vm" +import cuid from "cuid" + import { loggerToLogLevel } from "@jsenv/logger" import { createCancellationToken } from "@jsenv/cancellation" -import { jsenvNodeSystemUrl } from "@jsenv/core/src/internal/jsenvInternalFiles.js" import { writeDirectory, resolveUrl, urlToFileSystemPath, removeFileSystemNode } from "@jsenv/util" -import { jsenvCoreDirectoryUrl } from "./internal/jsenvCoreDirectoryUrl.js" + +import { jsenvNodeSystemUrl } from "@jsenv/core/src/internal/jsenvInternalFiles.js" +import { jsenvCoreDirectoryUrl } from "@jsenv/core/src/internal/jsenvCoreDirectoryUrl.js" import { escapeRegexpSpecialCharacters } from "./internal/escapeRegexpSpecialCharacters.js" import { createControllableNodeProcess } from "./internal/node-launcher/createControllableNodeProcess.js" import { istanbulCoverageFromV8Coverage } from "./internal/executing/coverage/istanbulCoverageFromV8Coverage.js" -import cuid from "cuid" export const launchNode = async ({ cancellationToken = createCancellationToken(), @@ -30,6 +33,7 @@ export const launchNode = async ({ remap = true, collectCoverage = false, coverageConfig, + coverageForceIstanbul = false, }) => { if (typeof projectDirectoryUrl !== "string") { throw new TypeError(`projectDirectoryUrl must be a string, got ${projectDirectoryUrl}`) @@ -47,7 +51,7 @@ export const launchNode = async ({ JSENV: true, } - if (collectCoverage) { + if (collectCoverage && !coverageForceIstanbul) { env.NODE_V8_COVERAGE = await getNodeV8CoverageDir({ projectDirectoryUrl }) } @@ -94,32 +98,6 @@ export default execute(${JSON.stringify(executeParams, null, " ")}) projectDirectoryUrl, }) - if (collectCoverage) { - const { NODE_V8_COVERAGE } = env - if (executionResult.coverageMap === undefined) { - executionResult.readCoverage = async () => { - try { - await controllableNodeProcess.stop() - const coverageMap = await istanbulCoverageFromV8Coverage({ - projectDirectoryUrl, - NODE_V8_COVERAGE, - coverageConfig, - }) - return coverageMap - } finally { - removeFileSystemNode(NODE_V8_COVERAGE, { - recursive: true, - }) - } - } - } else { - executionResult.readCoverage = () => executionResult.coverageMap - removeFileSystemNode(NODE_V8_COVERAGE, { - recursive: true, - }) - } - } - return executionResult } @@ -138,6 +116,38 @@ export default execute(${JSON.stringify(executeParams, null, " ")}) registerErrorCallback: controllableNodeProcess.registerErrorCallback, registerConsoleCallback: controllableNodeProcess.registerConsoleCallback, executeFile, + finalizeExecutionResult: coverageForceIstanbul + ? (executionResult) => executionResult + : // the v8 coverage directory is available once the child process is disconnected + async (executionResult) => { + const { NODE_V8_COVERAGE } = env + const coverageMap = await ensureV8CoverageDirRemoval(async () => { + // prefer istanbul if available + if (executionResult.coverageMap) { + return executionResult.coverageMap + } + + await controllableNodeProcess.disconnected + const istanbulCoverage = await istanbulCoverageFromV8Coverage({ + projectDirectoryUrl, + NODE_V8_COVERAGE, + coverageConfig, + }) + return istanbulCoverage + }, NODE_V8_COVERAGE) + executionResult.coverageMap = coverageMap + return executionResult + }, + } +} + +const ensureV8CoverageDirRemoval = async (fn, NODE_V8_COVERAGE) => { + try { + return await fn() + } finally { + removeFileSystemNode(NODE_V8_COVERAGE, { + recursive: true, + }) } } diff --git a/test/coverage/coverage-node/coverage-node.test.js b/test/coverage/coverage-node/coverage-node.test.js index cbd7993a56..4d997a8cac 100644 --- a/test/coverage/coverage-node/coverage-node.test.js +++ b/test/coverage/coverage-node/coverage-node.test.js @@ -18,33 +18,62 @@ const testPlan = { }, } -const result = await executeTestPlan({ - ...EXECUTE_TEST_PLAN_TEST_PARAMS, - jsenvDirectoryRelativeUrl, - testPlan, - coverage: true, - coverageConfig: { - [`./${testDirectoryRelativeUrl}file.js`]: true, - }, - // coverageTextLog: true, - // coverageJsonFile: true, - // coverageHtmlDirectory: true, -}) -const actual = result.coverageMap -const expected = { - [`${testDirectoryRelativeUrl}file.js`]: { - ...actual[`${testDirectoryRelativeUrl}file.js`], - path: `./${testDirectoryRelativeUrl}file.js`, - s: { - 0: 1, - 1: 1, - 2: 0, - 3: 1, - 4: 1, - 5: 1, - 6: 0, - 7: 0, +const getCoverage = async ({ coverageForceIstanbul = false } = {}) => { + const result = await executeTestPlan({ + ...EXECUTE_TEST_PLAN_TEST_PARAMS, + jsenvDirectoryRelativeUrl, + testPlan, + coverage: true, + coverageConfig: { + [`./${testDirectoryRelativeUrl}file.js`]: true, }, - }, + coverageForceIstanbul, + // coverageTextLog: true, + // coverageJsonFile: true, + // coverageHtmlDirectory: true, + }) + return result.coverageMap +} + +// v8 +{ + const actual = await getCoverage() + const expected = { + [`./${testDirectoryRelativeUrl}file.js`]: { + ...actual[`./${testDirectoryRelativeUrl}file.js`], + path: `./${testDirectoryRelativeUrl}file.js`, + s: { + 0: 1, + 1: 1, + 2: 0, + 3: 1, + 4: 1, + 5: 1, + 6: 0, + 7: 0, + }, + }, + } + assert({ actual, expected }) +} + +// istanbul +{ + const actual = await getCoverage({ + coverageForceIstanbul: true, + }) + const expected = { + [`./${testDirectoryRelativeUrl}file.js`]: { + ...actual[`./${testDirectoryRelativeUrl}file.js`], + path: `./${testDirectoryRelativeUrl}file.js`, + s: { + 0: 1, + 1: 0, + 2: 1, + 3: 1, + 4: 0, + }, + }, + } + assert({ actual, expected }) } -assert({ actual, expected })