testplane
Version:
Tests framework based on mocha and wdio
243 lines • 13.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.JSSelectivity = void 0;
const lodash_1 = require("lodash");
const node_url_1 = require("node:url");
const constants_1 = require("../../../error-snippets/constants");
const utils_1 = require("./utils");
const fs_cache_1 = require("./fs-cache");
const debug_1 = require("./debug");
const SOURCE_CODE_EXTENSIONS = [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts"];
const isSourceCodeFile = (sourceFileName) => {
return SOURCE_CODE_EXTENSIONS.some(ext => sourceFileName.endsWith(ext));
};
class JSSelectivity {
constructor(cdp, sessionId, sourceRoot, mapSourceMapUrl) {
this._debuggerOnScriptParsedFn = null;
this._scriptsSource = {};
this._scriptsSourceMap = {};
this._scriptIdToSourceUrl = {};
this._scriptIdToSourceMapUrl = {};
this._coverageResult = [];
this._cdp = cdp;
this._sessionId = sessionId;
this._sourceRoot = sourceRoot;
this._mapSourceMapUrl = mapSourceMapUrl;
}
_processScript({ scriptId, url, sourceMapURL }, cdpSessionId) {
if (!this._sessionId || cdpSessionId !== this._sessionId) {
return;
}
this._scriptIdToSourceUrl[scriptId] ||= url;
if (!url || !sourceMapURL || url.startsWith("chrome-error://")) {
this._scriptsSource[scriptId] ||= null;
this._scriptsSourceMap[scriptId] ||= null;
return;
}
if (this._scriptsSource[scriptId] && this._scriptsSourceMap[scriptId]) {
return;
}
let sourceMapResolvedUrl = (0, node_url_1.resolve)(url, sourceMapURL);
const mapResult = this._mapSourceMapUrl
? this._mapSourceMapUrl({ type: "js", sourceUrl: url, sourceMapUrl: sourceMapResolvedUrl })
: true;
if (!mapResult) {
this._scriptsSource[scriptId] ||= null;
this._scriptsSourceMap[scriptId] ||= null;
return;
}
if (mapResult !== true) {
this._scriptIdToSourceMapUrl[scriptId] = sourceMapResolvedUrl = mapResult;
}
this._scriptsSource[scriptId] ||= (0, fs_cache_1.hasCachedSelectivityFile)(fs_cache_1.CacheType.Asset, url).then(isCached => {
return isCached
? true
: this._cdp.debugger
.getScriptSource(this._sessionId, scriptId)
.then(res => res.scriptSource)
.then(data => (0, fs_cache_1.setCachedSelectivityFile)(fs_cache_1.CacheType.Asset, url, data)
.then(() => true)
.catch(err => {
(0, debug_1.debugSelectivity)(`Couldn't offload asset from "${url}" to fs-cache: %O`, err);
return data;
}))
.catch((err) => err);
});
// Embedded source maps are not cached on file system because of their large cache key
if ((0, utils_1.isDataProtocol)(sourceMapResolvedUrl)) {
this._scriptIdToSourceMapUrl[scriptId] = null;
this._scriptsSourceMap[scriptId] ||= (0, utils_1.fetchTextWithBrowserFallback)(sourceMapResolvedUrl, this._cdp.runtime, this._sessionId).catch((err) => err);
}
else {
this._scriptIdToSourceMapUrl[scriptId] = sourceMapResolvedUrl;
this._scriptsSourceMap[scriptId] ||= (0, fs_cache_1.hasCachedSelectivityFile)(fs_cache_1.CacheType.Asset, sourceMapResolvedUrl).then(isCached => {
return isCached
? true
: (0, utils_1.fetchTextWithBrowserFallback)(sourceMapResolvedUrl, this._cdp.runtime, this._sessionId)
.then(data => (0, fs_cache_1.setCachedSelectivityFile)(fs_cache_1.CacheType.Asset, sourceMapResolvedUrl, data)
.then(() => true)
.catch(err => {
(0, debug_1.debugSelectivity)(`Couldn't offload asset from "${sourceMapResolvedUrl}" to fs-cache: %O`, err);
return data;
}))
.catch((err) => err);
});
}
}
// If we haven't got "scriptParsed" event for the script, pull up source code + source map manually
_ensureScriptsAreLoading(coverage) {
coverage.forEach(({ scriptId, url }) => {
const fixedUrl = url || this._scriptIdToSourceUrl[scriptId];
const wasProcessed = Object.hasOwn(this._scriptsSource, scriptId) && Object.hasOwn(this._scriptsSourceMap, scriptId);
const isAnonymous = !fixedUrl;
const urlWasCorrected = url && url !== this._scriptIdToSourceUrl[scriptId];
const shouldRecalculateSource = Boolean(urlWasCorrected && this._mapSourceMapUrl);
if ((wasProcessed && !shouldRecalculateSource) || isAnonymous) {
return;
}
// Not dropping sources to fs the end of test (when "stop" is called) because we use it immediately
const scriptSourcePromise = (async () => {
const currentValue = await this._scriptsSource[scriptId];
const cacheResolvedValue = (0, utils_1.isCachedOnFs)(currentValue)
? await (0, fs_cache_1.getCachedSelectivityFile)(fs_cache_1.CacheType.Asset, fixedUrl)
: currentValue;
if (cacheResolvedValue && typeof cacheResolvedValue === "string") {
return cacheResolvedValue;
}
return this._cdp.debugger
.getScriptSource(this._sessionId, scriptId)
.then(({ scriptSource }) => {
(0, fs_cache_1.setCachedSelectivityFile)(fs_cache_1.CacheType.Asset, fixedUrl, scriptSource).catch(() => { });
return scriptSource;
})
.catch((err) => err);
})();
this._scriptIdToSourceUrl[scriptId] ||= url;
this._scriptsSource[scriptId] ||= scriptSourcePromise;
if (!this._scriptsSourceMap[scriptId] || shouldRecalculateSource) {
this._scriptsSourceMap[scriptId] = scriptSourcePromise.then(async (sourceCode) => {
if (sourceCode instanceof Error) {
return sourceCode;
}
const sourceMapsStartIndex = sourceCode.lastIndexOf(constants_1.JS_SOURCE_MAP_URL_COMMENT);
const sourceMapsEndIndex = sourceCode.indexOf("\n", sourceMapsStartIndex);
// Source maps are not generated for this source file
if (sourceMapsStartIndex === -1) {
return null;
}
const sourceMapURL = sourceMapsEndIndex === -1
? sourceCode.slice(sourceMapsStartIndex + constants_1.JS_SOURCE_MAP_URL_COMMENT.length)
: sourceCode.slice(sourceMapsStartIndex + constants_1.JS_SOURCE_MAP_URL_COMMENT.length, sourceMapsEndIndex);
let resolvedSourceMapUrl = (0, node_url_1.resolve)(fixedUrl, sourceMapURL);
const mappedResult = this._mapSourceMapUrl
? this._mapSourceMapUrl({ type: "js", sourceUrl: fixedUrl, sourceMapUrl: resolvedSourceMapUrl })
: true;
if (!mappedResult) {
this._scriptsSource[scriptId] = null;
return null;
}
if (mappedResult !== true) {
this._scriptIdToSourceMapUrl[scriptId] = resolvedSourceMapUrl = mappedResult;
}
else {
this._scriptIdToSourceMapUrl[scriptId] ||= resolvedSourceMapUrl;
}
if ((0, utils_1.isDataProtocol)(resolvedSourceMapUrl)) {
return (0, utils_1.fetchTextWithBrowserFallback)(resolvedSourceMapUrl, this._cdp.runtime, this._sessionId).catch((err) => err);
}
try {
const cachedSourceMaps = await (0, fs_cache_1.getCachedSelectivityFile)(fs_cache_1.CacheType.Asset, resolvedSourceMapUrl);
if (cachedSourceMaps) {
return cachedSourceMaps;
}
const sourceMap = await (0, utils_1.fetchTextWithBrowserFallback)(resolvedSourceMapUrl, this._cdp.runtime, this._sessionId);
(0, fs_cache_1.setCachedSelectivityFile)(fs_cache_1.CacheType.Asset, resolvedSourceMapUrl, sourceMap).catch(() => { });
return sourceMap;
}
catch (err) {
return err;
}
});
}
});
}
async _waitForLoadingScripts() {
await Promise.all([
Promise.allSettled(Object.values(this._scriptsSource)),
Promise.allSettled(Object.values(this._scriptsSourceMap)),
]);
}
async start() {
const debuggerOnScriptParsedFn = (this._debuggerOnScriptParsedFn = this._processScript.bind(this));
const sessionId = this._sessionId;
this._cdp.debugger.on("scriptParsed", debuggerOnScriptParsedFn);
await this._cdp.profiler.startPreciseCoverage(sessionId, {
callCount: false,
detailed: false,
allowTriggeredUpdates: false,
});
}
async takeCoverageSnapshot() {
const coveragePart = await this._cdp.profiler.takePreciseCoverage(this._sessionId);
this._ensureScriptsAreLoading(coveragePart.result);
await this._waitForLoadingScripts();
this._coverageResult.push(...coveragePart.result);
}
/** @param drop only performs cleanup without providing actual deps. Should be "true" if test is failed */
async stop(drop) {
try {
if (drop) {
return null;
}
const coverageLastPart = await this._cdp.profiler.takePreciseCoverage(this._sessionId);
const coverageScripts = [...this._coverageResult, ...coverageLastPart.result];
this._ensureScriptsAreLoading(coverageLastPart.result);
const totalDependingSourceFiles = new Set();
const grouppedByScriptCoverage = (0, lodash_1.groupBy)(coverageScripts, "scriptId");
const scriptIds = Object.keys(grouppedByScriptCoverage);
await Promise.all(scriptIds.map(async (scriptId) => {
const [source, sourceMaps] = await Promise.all([
this._scriptsSource[scriptId],
this._scriptsSourceMap[scriptId],
]);
const sourceMapUrl = this._scriptIdToSourceMapUrl[scriptId];
// Every "scriptId" has only one uniq "url"
const sourceUrl = this._scriptIdToSourceUrl[scriptId] || grouppedByScriptCoverage[scriptId][0].url;
// Function was called, but source maps were not generated for the file
// Or its anonymous script, without source url
if (!source || !sourceMaps || !sourceUrl) {
return;
}
if (source instanceof Error) {
throw new Error([`JS Selectivity: Couldn't load source code from ${sourceUrl}:`, String(source)].join("\n"));
}
if (sourceMaps instanceof Error) {
throw new Error([`JS Selectivity: Couldn't load source maps from ${sourceMapUrl}`, String(sourceMaps)].join("\n"));
}
if ((0, utils_1.isCachedOnFs)(sourceMaps) && !sourceMapUrl) {
throw new Error("Assertation failed: souce map url has to present if source maps are fs-cached");
}
const [sourceString, sourceMapsString] = await Promise.all([
(0, utils_1.isCachedOnFs)(source) ? (0, fs_cache_1.getCachedSelectivityFile)(fs_cache_1.CacheType.Asset, sourceUrl) : source,
(0, utils_1.isCachedOnFs)(sourceMaps)
? (0, fs_cache_1.getCachedSelectivityFile)(fs_cache_1.CacheType.Asset, sourceMapUrl)
: sourceMaps,
]);
if (!sourceString || !sourceMapsString) {
throw new Error(`JS Selectivity: fs-cache is broken for ${sourceUrl}`);
}
const parsedSourceMapRanges = await (0, utils_1.parseSourceMapRanges)(sourceString, sourceMapsString, this._sourceRoot);
const dependingSourceFiles = (0, utils_1.extractSourceFilesDeps)(parsedSourceMapRanges, grouppedByScriptCoverage[scriptId], isSourceCodeFile);
for (const sourceFile of dependingSourceFiles.values()) {
totalDependingSourceFiles.add(sourceFile);
}
}));
return totalDependingSourceFiles;
}
finally {
this._debuggerOnScriptParsedFn && this._cdp.debugger.off("scriptParsed", this._debuggerOnScriptParsedFn);
}
}
}
exports.JSSelectivity = JSSelectivity;
//# sourceMappingURL=js-selectivity.js.map