testplane
Version:
Tests framework based on mocha and wdio
401 lines • 17.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isCachedOnFs = exports.readTestDependencies = exports.getTestDependenciesPath = exports.getTestSelectivityDumpId = exports.getSelectivityTestsPath = exports.readHashFileContents = exports.getSelectivityHashesPath = exports.shallowSortObject = exports.mergeSourceDependencies = exports.transformSourceDependencies = exports.isDataProtocol = exports.hasProtocol = exports.extractSourceFilesDeps = exports.parseSourceMapRanges = exports.patchSourceMapSources = exports.fetchTextWithBrowserFallback = void 0;
const lodash_1 = require("lodash");
const trace_mapping_1 = require("@jridgewell/trace-mapping");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const url_1 = require("url");
const logger = __importStar(require("../../../utils/logger"));
const fs_2 = require("../../../utils/fs");
const constants_1 = require("./constants");
const json_utils_1 = require("./json-utils");
const fs_cache_1 = require("./fs-cache");
/**
* Tries to fetch text by url from node.js, then falls back to "fetch" from browser, if node.js fetch fails
* @param url text url
* @param runtime CDP runtime domain
* @param sessionId CDP session id
* @returns text
*/
const fetchTextWithBrowserFallback = async (url, runtime, sessionId) => {
const isSourceMapEmbedded = new url_1.URL(url).protocol === "data:";
if (isSourceMapEmbedded) {
// With "data" protocol it just decodes embedded source maps without actual network requests
// So we can do it directly from node.js
return fetch(url).then(r => r.text());
}
// At first, trying to fetch sourceMaps directly from the node.js
// Then falling back to do it via browser (if Testplane has no direct network access to it, for example)
try {
return await fetch(url).then(r => r.text());
}
catch {
return runtime
.evaluate(sessionId, {
expression: `fetch("${url.replaceAll('"', '\\"')}").then(r => r.text())`,
awaitPromise: true,
returnByValue: true,
})
.then(r => r.result.value);
}
};
exports.fetchTextWithBrowserFallback = fetchTextWithBrowserFallback;
const isWebpackProtocol = (posixRelativeSourceFilePath) => {
return posixRelativeSourceFilePath.startsWith(constants_1.WEBPACK_PROTOCOL);
};
/**
* Replaces "webpack://" protocol with source path because "source-map" doesn't work well with protocol paths
* @param sourceMap Raw source maps in https://tc39.es/ecma426/ format
* @param sourceRoot Source root
*/
const patchSourceMapSources = (sourceMap, sourceRoot) => {
sourceMap.sourceRoot = sourceRoot || sourceMap.sourceRoot;
for (let i = 0; i < sourceMap.sources.length; i++) {
if (sourceMap.sources[i] && isWebpackProtocol(sourceMap.sources[i])) {
sourceMap.sources[i] = sourceMap.sources[i].slice(constants_1.WEBPACK_PROTOCOL.length);
}
}
return sourceMap;
};
exports.patchSourceMapSources = patchSourceMapSources;
const getSourceLineOffsetArray = (source) => {
let sourceOffset = source.indexOf("\n");
const offsetToLine = [0];
while (sourceOffset !== -1) {
offsetToLine.push(++sourceOffset);
sourceOffset = source.indexOf("\n", sourceOffset);
}
return offsetToLine;
};
const parseSourceMapRanges = async (source, sourceMaps, sourceRoot) => {
let jsonCachedResult = null;
try {
const cachedResult = await (0, fs_cache_1.getCachedSelectivityFile)(fs_cache_1.CacheType.SourceMapParseResult, sourceMaps);
jsonCachedResult = cachedResult ? JSON.parse(cachedResult) : null;
}
catch { } // eslint-disable-line no-empty
if (jsonCachedResult) {
return jsonCachedResult;
}
const lineOffsetArray = getSourceLineOffsetArray(source);
const sourceMapsParsed = (0, exports.patchSourceMapSources)(JSON.parse(sourceMaps), sourceRoot);
const traceMap = new trace_mapping_1.TraceMap(sourceMapsParsed);
const result = {
offset: [],
filename: [],
};
// "eachMapping" walks segments in generated order (line asc, then column asc).
// A single source can still appear in several disjoint segments (bundlers interleave/concatenate modules),
let lastRecordedSource;
(0, trace_mapping_1.eachMapping)(traceMap, mapping => {
const mappingSource = mapping.source || "";
const lineOffset = lineOffsetArray[mapping.generatedLine - 1];
// Skip out-of-range
if (typeof lineOffset === "undefined") {
return;
}
if (mappingSource === lastRecordedSource) {
return;
}
result.offset.push(lineOffset + mapping.generatedColumn);
result.filename.push(mappingSource);
lastRecordedSource = mappingSource;
});
(0, fs_cache_1.setCachedSelectivityFile)(fs_cache_1.CacheType.SourceMapParseResult, sourceMaps, JSON.stringify(result)).catch(() => { });
return result;
};
exports.parseSourceMapRanges = parseSourceMapRanges;
/**
* Given compiled code, its source map, and the executed offsets
* It returns the original source files touched
* Useful for turning coverage ranges into real TS/JS module dependencies
* @param source Compiled source code
* @param sourceMaps Source maps JSON string
* @param coverages CDP script coverages
* @param sourceRoot Source root
*/
const extractSourceFilesDeps = ({ offset, filename }, coverages, sourceFilterFn) => {
const dependantSourceFiles = new Set();
if (!offset.length) {
return dependantSourceFiles;
}
for (const scriptCoverage of coverages) {
for (const functionCall of scriptCoverage.functions) {
// ranges[0] is the function's full extent in V8 coverage; nested ranges are sub-blocks
const range = functionCall.ranges[0];
if (!range || !range.count) {
continue;
}
// Ignore full-file wrapper
if (range.startOffset <= offset[0] && range.endOffset >= offset[offset.length - 1]) {
const filesCount = filename.reduce((acc, val) => acc + (val ? 1 : 0), 0);
if (filesCount > 2) {
continue;
}
}
const startIdx = Math.max(0, (0, lodash_1.sortedLastIndex)(offset, range.startOffset) - 1);
for (let idx = startIdx; idx < offset.length && offset[idx] < range.endOffset; idx++) {
const sourceFile = filename[idx];
if (sourceFile && (!sourceFilterFn || sourceFilterFn(sourceFile))) {
dependantSourceFiles.add(sourceFile);
}
}
}
}
return dependantSourceFiles;
};
exports.extractSourceFilesDeps = extractSourceFilesDeps;
/**
* @returns True, if fileUrlLikePath has some kind of protocol ("data://", "webpack://", "turbopack://", "file://")...
*/
const hasProtocol = (fileUrlLikePath) => {
if (!fileUrlLikePath.includes("://")) {
return false;
}
try {
return Boolean(new url_1.URL(fileUrlLikePath).protocol);
}
catch {
return false;
}
};
exports.hasProtocol = hasProtocol;
const getProtocol = (fileUrlLikePath) => {
if (!fileUrlLikePath.includes("://")) {
return null;
}
try {
return new url_1.URL(fileUrlLikePath).protocol;
}
catch {
return null;
}
};
const isDataProtocol = (fileUrlLikePath) => fileUrlLikePath.startsWith("data:");
exports.isDataProtocol = isDataProtocol;
const ensurePosixRelativeDependencyPathExists = (posixRelativePath) => {
const relativePath = posixRelativePath.replaceAll(path_1.default.posix.sep, path_1.default.sep);
if (fs_1.default.existsSync(relativePath)) {
return;
}
throw new Error([
`Selectivity: Couldn't find "${relativePath}", which is test's dependency`,
"Please ensure 'sources' of generated source maps contain valid paths to existing files",
"Configuring 'sourceRoot' in Testplane selectivity config also might help",
].join("\n"));
};
const warnUnsupportedProtocol = (0, lodash_1.memoize)((protocol, dependency) => {
logger.warn(`Selectivity: Ignoring dependencies of unsupported protocol "${protocol}" (example: "${dependency}")`);
});
/**
* @param dependencies.css set of css dependency URI's
* @param dependencies.js set of js dependency URI's
* @param dependencies.png set of png dependency URI's
* @returns sorted uniq arrays of relative paths
*/
const transformSourceDependencies = ({ css: cssDependencies, js: jsDependencies, png: pngDependencies, }, mapDependencyPathFn, scope) => {
const nodeModulesLabel = "node_modules/";
const cssSet = new Set();
const jsSet = new Set();
const modulesSet = new Set();
const pngSet = new Set();
const classifyDependency = (dependency, typedResultSet, reason) => {
dependency = decodeURIComponent((0, fs_2.softFileURLToPath)(dependency));
const protocol = getProtocol(dependency);
if (protocol) {
warnUnsupportedProtocol(protocol, dependency);
return;
}
const initialDependencyRelativePath = path_1.default.posix.relative(path_1.default.posix.resolve(), path_1.default.posix.resolve(dependency));
const mapDependencyPathResult = mapDependencyPathFn
? mapDependencyPathFn({
scope,
reason,
relativePath: initialDependencyRelativePath,
})
: true;
if (!mapDependencyPathResult) {
return;
}
const dependencyRelativePath = mapDependencyPathResult === true ? initialDependencyRelativePath : mapDependencyPathResult;
const nodeModulesLabelPos = dependencyRelativePath.indexOf(nodeModulesLabel);
if (nodeModulesLabelPos === -1) {
ensurePosixRelativeDependencyPathExists(dependencyRelativePath);
typedResultSet.add(dependencyRelativePath);
return;
}
const modulePos = nodeModulesLabelPos + nodeModulesLabel.length;
const isScopedDependency = dependencyRelativePath[modulePos] === "@";
let moduleEndPos;
if (isScopedDependency) {
const scopeEndPos = dependencyRelativePath.indexOf("/", modulePos + 1);
moduleEndPos = scopeEndPos === -1 ? -1 : dependencyRelativePath.indexOf("/", scopeEndPos + 1);
}
else {
moduleEndPos = dependencyRelativePath.indexOf("/", modulePos + 1);
}
if (moduleEndPos === -1) {
ensurePosixRelativeDependencyPathExists(dependencyRelativePath);
typedResultSet.add(dependencyRelativePath);
}
else {
const modulePath = dependencyRelativePath.slice(0, moduleEndPos);
ensurePosixRelativeDependencyPathExists(modulePath);
modulesSet.add(modulePath);
}
};
if (cssDependencies) {
for (const cssDependency of cssDependencies.values()) {
// Testplane can't have css dependencies
classifyDependency(cssDependency, cssSet, "browser-css-import");
}
}
if (jsDependencies) {
for (const jsDependency of jsDependencies.values()) {
classifyDependency(jsDependency, jsSet, scope === "browser" ? "browser-js-coverage" : "testplane-js-import");
}
}
if (pngDependencies) {
for (const pngDependency of pngDependencies.values()) {
// Browser png dependencies are not tracked
classifyDependency(pngDependency, pngSet, "testplane-assert-view-reference");
}
}
const cmpStr = (a, b) => a.localeCompare(b);
return {
css: Array.from(cssSet).sort(cmpStr),
js: Array.from(jsSet).sort(cmpStr),
modules: Array.from(modulesSet).sort(cmpStr),
png: Array.from(pngSet).sort(cmpStr),
};
};
exports.transformSourceDependencies = transformSourceDependencies;
/** Merges two sorted deps array into one with uniq values */
const mergeSourceDependencies = (a, b) => {
const result = { css: [], js: [], modules: [], png: [] };
for (const depType of Object.keys(result)) {
let aInd = 0, bInd = 0;
if (!a[depType]) {
if (!b[depType]) {
continue;
}
result[depType] = b[depType];
continue;
}
else if (!b[depType]) {
result[depType] = a[depType];
continue;
}
while (aInd < a[depType].length || bInd < b[depType].length) {
let compareResult;
if (bInd >= b[depType].length) {
compareResult = -1;
}
else if (aInd >= a[depType].length) {
compareResult = 1;
}
else {
compareResult = a[depType][aInd].localeCompare(b[depType][bInd]);
}
if (compareResult < 0) {
result[depType].push(a[depType][aInd]);
do {
aInd++;
} while (a[depType][aInd] === a[depType][aInd - 1]);
}
else if (compareResult > 0) {
result[depType].push(b[depType][bInd]);
do {
bInd++;
} while (b[depType][bInd] === b[depType][bInd - 1]);
}
else {
result[depType].push(a[depType][aInd]);
do {
aInd++;
} while (a[depType][aInd] === a[depType][aInd - 1]);
do {
bInd++;
} while (b[depType][bInd] === b[depType][bInd - 1]);
}
}
}
return result;
};
exports.mergeSourceDependencies = mergeSourceDependencies;
// Ensures file consistency
const shallowSortObject = (obj) => {
const testBrowsers = Object.keys(obj).sort((a, b) => a.localeCompare(b));
for (const testBrowser of testBrowsers) {
const testBrowserDeps = obj[testBrowser];
delete obj[testBrowser];
obj[testBrowser] = testBrowserDeps;
}
};
exports.shallowSortObject = shallowSortObject;
const getSelectivityHashesPath = (testDependenciesPath) => path_1.default.join(testDependenciesPath, "hashes.json");
exports.getSelectivityHashesPath = getSelectivityHashesPath;
const readHashFileContents = (selectivityHashesPath, compression) => (0, json_utils_1.readJsonWithCompression)(selectivityHashesPath, compression, {
defaultValue: { files: {}, modules: {}, patterns: {} },
})
.catch(() => ({ files: {}, modules: {}, patterns: {} }))
.then(res => {
res.files ||= {};
res.modules ||= {};
res.patterns ||= {};
return res;
});
exports.readHashFileContents = readHashFileContents;
const getSelectivityTestsPath = (testDependenciesPath) => path_1.default.join(testDependenciesPath, "tests");
exports.getSelectivityTestsPath = getSelectivityTestsPath;
const getTestSelectivityDumpId = (test) => test.id;
exports.getTestSelectivityDumpId = getTestSelectivityDumpId;
const getTestDependenciesPath = (selectivityTestsPath, test) => path_1.default.join(selectivityTestsPath, `${(0, exports.getTestSelectivityDumpId)(test)}.json`);
exports.getTestDependenciesPath = getTestDependenciesPath;
/** @returns `Promise<Record<BrowserID, Record<DepType, NormalizedDependencies>>>` */
const readTestDependencies = async (selectivityTestsPath, test, compression) => {
const result = (await (0, json_utils_1.readJsonWithCompression)((0, exports.getTestDependenciesPath)(selectivityTestsPath, test), compression, {
defaultValue: {},
}).catch(() => ({})));
for (const browserId in result) {
for (const depType in result[browserId]) {
const currentDeps = result[browserId][depType];
currentDeps.css ||= [];
currentDeps.js ||= [];
currentDeps.modules ||= [];
currentDeps.png ||= [];
}
}
return result;
};
exports.readTestDependencies = readTestDependencies;
const isCachedOnFs = (value) => value === true;
exports.isCachedOnFs = isCachedOnFs;
//# sourceMappingURL=utils.js.map