Implements Phases 1-8 of the TFTSR implementation plan. Rust backend (Tauri 2.x, src-tauri/): - Multi-provider AI: OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama - PII detection engine: 11 regex patterns with overlap resolution - SQLCipher AES-256 encrypted database with 10 versioned migrations - 28 Tauri IPC commands for triage, analysis, document, and system ops - Ollama: hardware probe, model recommendations, pull/delete with events - RCA and blameless post-mortem Markdown document generators - PDF export via printpdf - Audit log: SHA-256 hash of every external data send - Integration stubs for Confluence, ServiceNow, Azure DevOps (v0.2) Frontend (React 18 + TypeScript + Vite, src/): - 9 pages: full triage workflow NewIssue→LogUpload→Triage→Resolution→RCA→Postmortem→History+Settings - 7 components: ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI primitives - 3 Zustand stores: session, settings (persisted), history - Type-safe tauriCommands.ts matching Rust backend types exactly - 8 IT domain system prompts (Linux, Windows, Network, K8s, DB, Virt, HW, Obs) DevOps: - .woodpecker/test.yml: rustfmt, clippy, cargo test, tsc, vitest on every push - .woodpecker/release.yml: linux/amd64 + linux/arm64 builds, Gogs release upload Verified: - cargo check: zero errors - tsc --noEmit: zero errors - vitest run: 13/13 unit tests passing Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
630 lines
21 KiB
JavaScript
630 lines
21 KiB
JavaScript
// src/node/ConfigParser.ts
|
|
import path3 from "node:path";
|
|
import logger from "@wdio/logger";
|
|
import { deepmerge, deepmergeCustom } from "deepmerge-ts";
|
|
|
|
// src/node/FileSystemPathService.ts
|
|
import fs from "node:fs";
|
|
import url from "node:url";
|
|
import path from "node:path";
|
|
import { sync as globSync } from "glob";
|
|
|
|
// src/node/RequireLibrary.ts
|
|
import createJITI from "jiti";
|
|
var RequireLibrary = class {
|
|
#jiti = createJITI(import.meta.url);
|
|
import(module) {
|
|
return import(module).catch((err) => {
|
|
if (err instanceof Error && err.message.includes("does not provide an export named")) {
|
|
return this.#jiti(module);
|
|
}
|
|
throw err;
|
|
});
|
|
}
|
|
};
|
|
|
|
// src/node/FileSystemPathService.ts
|
|
function lowercaseWinDriveLetter(p) {
|
|
return p.replace(/^[A-Za-z]:\\/, (match) => match.toLowerCase());
|
|
}
|
|
var FileSystemPathService = class {
|
|
#moduleRequireService = new RequireLibrary();
|
|
loadFile(path4) {
|
|
if (!path4) {
|
|
throw new Error("A path is required");
|
|
}
|
|
return this.#moduleRequireService.import(path4);
|
|
}
|
|
isFile(filepath) {
|
|
return fs.existsSync(filepath) && fs.lstatSync(filepath).isFile();
|
|
}
|
|
/**
|
|
* find test files based on a glob pattern
|
|
* @param pattern file pattern to glob
|
|
* @param rootDir directory of wdio config file
|
|
* @returns files matching the glob pattern
|
|
*/
|
|
glob(pattern, rootDir) {
|
|
const globResult = globSync(pattern, {
|
|
cwd: rootDir,
|
|
matchBase: true
|
|
}) || [];
|
|
const fileName = pattern.startsWith(path.sep) ? pattern : path.resolve(rootDir, pattern);
|
|
if (!pattern.includes("*") && !globResult.includes(pattern) && !globResult.map(lowercaseWinDriveLetter).includes(lowercaseWinDriveLetter(fileName)) && fs.existsSync(fileName)) {
|
|
globResult.push(fileName);
|
|
}
|
|
return globResult.sort();
|
|
}
|
|
ensureAbsolutePath(filepath, rootDir) {
|
|
if (filepath.startsWith("file://")) {
|
|
return filepath;
|
|
}
|
|
const p = path.isAbsolute(filepath) ? path.normalize(filepath) : path.resolve(rootDir, filepath);
|
|
return url.pathToFileURL(p).href;
|
|
}
|
|
};
|
|
|
|
// src/node/utils.ts
|
|
import url2 from "node:url";
|
|
import path2 from "node:path";
|
|
function makeRelativeToCWD(files = []) {
|
|
const returnFiles = [];
|
|
for (const file of files) {
|
|
if (Array.isArray(file)) {
|
|
returnFiles.push(makeRelativeToCWD(file));
|
|
continue;
|
|
}
|
|
returnFiles.push(
|
|
file.startsWith("file:///") ? url2.fileURLToPath(file) : file.includes("/") && !file.includes("*") ? path2.resolve(process.cwd(), file) : file
|
|
);
|
|
}
|
|
return returnFiles;
|
|
}
|
|
|
|
// src/constants.ts
|
|
var DEFAULT_TIMEOUT = 1e4;
|
|
var DEFAULT_MAX_INSTANCES_PER_CAPABILITY = 100;
|
|
var DEFAULT_CONFIGS = () => ({
|
|
specs: [],
|
|
suites: {},
|
|
exclude: [],
|
|
capabilities: [],
|
|
outputDir: void 0,
|
|
logLevel: "info",
|
|
logLevels: {},
|
|
groupLogsByTestSpec: false,
|
|
excludeDriverLogs: [],
|
|
bail: 0,
|
|
waitforInterval: 100,
|
|
waitforTimeout: 5e3,
|
|
framework: "mocha",
|
|
reporters: [],
|
|
services: [],
|
|
maxInstances: 100,
|
|
maxInstancesPerCapability: DEFAULT_MAX_INSTANCES_PER_CAPABILITY,
|
|
injectGlobals: true,
|
|
filesToWatch: [],
|
|
connectionRetryTimeout: 12e4,
|
|
connectionRetryCount: 3,
|
|
execArgv: [],
|
|
runnerEnv: {},
|
|
runner: "local",
|
|
shard: {
|
|
current: 1,
|
|
total: 1
|
|
},
|
|
specFileRetries: 0,
|
|
specFileRetriesDelay: 0,
|
|
specFileRetriesDeferred: false,
|
|
autoAssertOnTestEnd: true,
|
|
reporterSyncInterval: 100,
|
|
reporterSyncTimeout: 5e3,
|
|
cucumberFeaturesWithLineNumbers: [],
|
|
/**
|
|
* framework defaults
|
|
*/
|
|
mochaOpts: {
|
|
timeout: DEFAULT_TIMEOUT
|
|
},
|
|
jasmineOpts: {
|
|
defaultTimeoutInterval: DEFAULT_TIMEOUT
|
|
},
|
|
cucumberOpts: {
|
|
timeout: DEFAULT_TIMEOUT
|
|
},
|
|
/**
|
|
* hooks
|
|
*/
|
|
onPrepare: [],
|
|
onWorkerStart: [],
|
|
onWorkerEnd: [],
|
|
before: [],
|
|
beforeSession: [],
|
|
beforeSuite: [],
|
|
beforeHook: [],
|
|
beforeTest: [],
|
|
beforeCommand: [],
|
|
afterCommand: [],
|
|
afterTest: [],
|
|
afterHook: [],
|
|
afterSuite: [],
|
|
afterSession: [],
|
|
after: [],
|
|
onComplete: [],
|
|
onReload: [],
|
|
beforeAssertion: [],
|
|
afterAssertion: [],
|
|
/**
|
|
* cucumber specific hooks
|
|
*/
|
|
beforeFeature: [],
|
|
beforeScenario: [],
|
|
beforeStep: [],
|
|
afterStep: [],
|
|
afterScenario: [],
|
|
afterFeature: []
|
|
});
|
|
var SUPPORTED_HOOKS = [
|
|
"before",
|
|
"beforeSession",
|
|
"beforeSuite",
|
|
"beforeHook",
|
|
"beforeTest",
|
|
"beforeCommand",
|
|
"afterCommand",
|
|
"afterTest",
|
|
"afterHook",
|
|
"afterSuite",
|
|
"afterSession",
|
|
"after",
|
|
"beforeAssertion",
|
|
"afterAssertion",
|
|
// @ts-ignore not defined in core hooks but added with cucumber
|
|
"beforeFeature",
|
|
"beforeScenario",
|
|
"beforeStep",
|
|
"afterStep",
|
|
"afterScenario",
|
|
"afterFeature",
|
|
"onReload",
|
|
"onPrepare",
|
|
"onWorkerStart",
|
|
"onWorkerEnd",
|
|
"onComplete"
|
|
];
|
|
var SUPPORTED_FILE_EXTENSIONS = [
|
|
".js",
|
|
".jsx",
|
|
".mjs",
|
|
".mts",
|
|
".es6",
|
|
".ts",
|
|
".tsx",
|
|
".feature",
|
|
".coffee",
|
|
".cjs"
|
|
];
|
|
var NO_NAMED_CONFIG_EXPORT = 'No named export object called "config" found. Make sure you export the config object via `export.config = { ... }` when using CommonJS or `export const config = { ... }` when using ESM. Read more on this on https://webdriver.io/docs/configurationfile !';
|
|
|
|
// src/utils.ts
|
|
var validObjectOrArray = (object) => Array.isArray(object) && object.length > 0 || typeof object === "object" && Object.keys(object).length > 0;
|
|
function removeLineNumbers(filePath) {
|
|
const matcher = filePath.match(/:\d+(:\d+$|$)/);
|
|
if (matcher) {
|
|
filePath = filePath.substring(0, matcher.index);
|
|
}
|
|
return filePath;
|
|
}
|
|
function isCucumberFeatureWithLineNumber(spec) {
|
|
const specs = Array.isArray(spec) ? spec : [spec];
|
|
return specs.some((s) => /:\d+(:\d+$|$)/.test(s));
|
|
}
|
|
|
|
// src/node/ConfigParser.ts
|
|
var log = logger("@wdio/config:ConfigParser");
|
|
var MERGE_DUPLICATION = ["services", "reporters", "capabilities"];
|
|
var ConfigParser = class _ConfigParser {
|
|
constructor(configFilePath, _initialConfig = {}, _pathService = new FileSystemPathService()) {
|
|
this._initialConfig = _initialConfig;
|
|
this._pathService = _pathService;
|
|
this.#configFilePath = configFilePath;
|
|
this._config = Object.assign(
|
|
{ rootDir: path3.dirname(configFilePath) },
|
|
DEFAULT_CONFIGS()
|
|
);
|
|
if (_initialConfig.spec) {
|
|
_initialConfig.spec = makeRelativeToCWD(_initialConfig.spec);
|
|
}
|
|
this.merge(_initialConfig, false);
|
|
}
|
|
#isInitialised = false;
|
|
#configFilePath;
|
|
_config;
|
|
_capabilities = [];
|
|
/**
|
|
* initializes the config object
|
|
*/
|
|
async initialize(object = {}) {
|
|
if (!this.#isInitialised) {
|
|
await this.addConfigFile(this.#configFilePath);
|
|
}
|
|
this.merge({ ...object });
|
|
if (Object.keys(this._initialConfig || {}).includes("coverage")) {
|
|
if (this._config.runner === "browser") {
|
|
this._config.runner = ["browser", {
|
|
coverage: { enabled: this._initialConfig.coverage }
|
|
}];
|
|
} else if (Array.isArray(this._config.runner) && this._config.runner[0] === "browser") {
|
|
this._config.runner[1].coverage = {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
...this._config.runner[1].coverage,
|
|
enabled: this._initialConfig.coverage
|
|
};
|
|
}
|
|
}
|
|
this.#isInitialised = true;
|
|
}
|
|
/**
|
|
* merges config file with default values
|
|
* @param {string} filename path of file relative to current directory
|
|
*/
|
|
async addConfigFile(filename) {
|
|
if (typeof filename !== "string") {
|
|
throw new Error("addConfigFile requires filepath");
|
|
}
|
|
const filePath = this._pathService.ensureAbsolutePath(filename, process.cwd());
|
|
try {
|
|
const importedModule = await this._pathService.loadFile(filePath);
|
|
const config = importedModule.config || importedModule.default?.config;
|
|
if (typeof config !== "object") {
|
|
throw new Error(NO_NAMED_CONFIG_EXPORT);
|
|
}
|
|
const configFileCapabilities = config.capabilities;
|
|
if (!configFileCapabilities) {
|
|
throw new Error(`No \`capabilities\` property found in WebdriverIO.Config defined in file: ${filePath}`);
|
|
}
|
|
const fileConfig = Object.assign({}, config);
|
|
const defaultTo = Array.isArray(this._capabilities) ? [] : {};
|
|
this._capabilities = deepmerge(this._capabilities, fileConfig.capabilities || defaultTo);
|
|
delete fileConfig.capabilities;
|
|
this.addService(fileConfig);
|
|
for (const hookName of SUPPORTED_HOOKS) {
|
|
delete fileConfig[hookName];
|
|
}
|
|
this._config = deepmerge(this._config, fileConfig);
|
|
delete this._config.watch;
|
|
} catch (e) {
|
|
if (!filename.endsWith("&log_errors=false")) {
|
|
log.error(`Failed loading configuration file: ${filePath}:`, e.message);
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
/**
|
|
* merge external object with config object
|
|
* @param {Object} object desired object to merge into the config object
|
|
* @param {boolean} [addPathToSpecs=true] this flag determines whether it is necessary to find paths to specs if the --spec parameter was passed in CLI
|
|
*/
|
|
merge(object = {}, addPathToSpecs = true) {
|
|
const spec = Array.isArray(object.spec) ? object.spec : [];
|
|
const exclude = Array.isArray(object.exclude) ? object.exclude : [];
|
|
const customDeepMerge = deepmergeCustom({
|
|
mergeArrays: ([oldValue, newValue], utils, meta) => {
|
|
const key = meta?.key;
|
|
if (meta && MERGE_DUPLICATION.includes(key)) {
|
|
const origWithoutObjectEntries = oldValue.filter((value) => typeof value !== "object");
|
|
return Array.from(new Set(deepmerge(newValue, origWithoutObjectEntries)));
|
|
}
|
|
return utils.actions.defaultMerge;
|
|
}
|
|
});
|
|
this._config = customDeepMerge(this._config, object);
|
|
if (object["wdio:specs"] && object["wdio:specs"].length > 0) {
|
|
this._config.specs = object["wdio:specs"];
|
|
} else if (object.specs && object.specs.length > 0) {
|
|
this._config.specs = object.specs;
|
|
}
|
|
if (object["wdio:exclude"] && object["wdio:exclude"].length > 0) {
|
|
this._config.exclude = object["wdio:exclude"];
|
|
} else if (object.exclude && object.exclude.length > 0) {
|
|
this._config.exclude = object.exclude;
|
|
}
|
|
if (object.suite && object.suite.length > 0) {
|
|
this._config.suite = this._config.suite?.filter((suite, idx, suites) => suites.indexOf(suite) === idx);
|
|
}
|
|
this._capabilities = validObjectOrArray(this._config.capabilities) ? this._config.capabilities : this._capabilities;
|
|
if (this._config.spec && isCucumberFeatureWithLineNumber(this._config.spec)) {
|
|
this._config.cucumberFeaturesWithLineNumbers = Array.isArray(this._config.spec) ? [...new Set(this._config.spec)] : [this._config.spec];
|
|
}
|
|
if (addPathToSpecs && spec.length > 0) {
|
|
this._config.specs = this.setFilePathToFilterOptions(spec, this._config.specs, object.group);
|
|
}
|
|
if (exclude.length > 0 && allKeywordsContainPath(exclude)) {
|
|
this._config.exclude = this.setFilePathToFilterOptions(exclude, this._config.exclude);
|
|
} else if (exclude.length > 0) {
|
|
this._config.exclude = exclude;
|
|
}
|
|
}
|
|
/**
|
|
* Add hooks from an existing service to the runner config.
|
|
* @param {object} service - an object that contains hook methods.
|
|
*/
|
|
addService(service) {
|
|
const addHook = (hookName, hook) => {
|
|
const existingHooks = this._config[hookName];
|
|
if (!existingHooks) {
|
|
this._config[hookName] = hook.bind(service);
|
|
} else if (typeof existingHooks === "function") {
|
|
this._config[hookName] = [existingHooks, hook.bind(service)];
|
|
} else {
|
|
this._config[hookName] = [...existingHooks, hook.bind(service)];
|
|
}
|
|
};
|
|
for (const hookName of SUPPORTED_HOOKS) {
|
|
const hooksToBeAdded = service[hookName];
|
|
if (!hooksToBeAdded) {
|
|
continue;
|
|
}
|
|
if (typeof hooksToBeAdded === "function") {
|
|
addHook(hookName, hooksToBeAdded);
|
|
} else if (Array.isArray(hooksToBeAdded)) {
|
|
for (const hookToAdd of hooksToBeAdded) {
|
|
if (typeof hookToAdd === "function") {
|
|
addHook(hookName, hookToAdd);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* determine what specs to run based on the spec(s), suite(s), exclude
|
|
* attributes from CLI, config and capabilities
|
|
*/
|
|
getSpecs(capSpecs, capExclude) {
|
|
const isSpecParamPassed = Array.isArray(this._config.spec) && this._config.spec.length > 0;
|
|
const repeat = this._config.repeat;
|
|
let specs = _ConfigParser.getFilePaths(this._config.specs, this._config.rootDir, this._pathService);
|
|
const configSuiteNames = Object.keys(this._config.suites || {});
|
|
const excludeSuites = [];
|
|
const excludeSpecPatterns = [];
|
|
for (const item of this._config.exclude || []) {
|
|
if (!isSpecPattern(item) && configSuiteNames.includes(item)) {
|
|
excludeSuites.push(item);
|
|
} else {
|
|
excludeSpecPatterns.push(item);
|
|
}
|
|
}
|
|
let exclude = allKeywordsContainPath(excludeSpecPatterns) ? _ConfigParser.getFilePaths(excludeSpecPatterns, this._config.rootDir, this._pathService) : excludeSpecPatterns;
|
|
const suites = Array.isArray(this._config.suite) ? this._config.suite : [];
|
|
if (Array.isArray(capExclude)) {
|
|
exclude = [...exclude, ..._ConfigParser.getFilePaths(capExclude, this._config.rootDir, this._pathService)];
|
|
}
|
|
if (!isSpecParamPassed && Array.isArray(capSpecs)) {
|
|
specs = _ConfigParser.getFilePaths(capSpecs, this._config.rootDir, this._pathService);
|
|
}
|
|
if (suites.length > 0) {
|
|
let suiteSpecs = [];
|
|
for (const suiteName of suites) {
|
|
if (excludeSuites.includes(suiteName)) {
|
|
log.info(`Suite "${suiteName}" excluded via --exclude`);
|
|
continue;
|
|
}
|
|
const suite = this._config.suites?.[suiteName];
|
|
if (!suite) {
|
|
log.warn(`No suite was found with name "${suiteName}"`);
|
|
}
|
|
if (Array.isArray(suite)) {
|
|
suiteSpecs = suiteSpecs.concat(_ConfigParser.getFilePaths(suite, this._config.rootDir, this._pathService));
|
|
}
|
|
}
|
|
const nonExcludedSuites = suites.filter((s) => !excludeSuites.includes(s));
|
|
if (suiteSpecs.length === 0 && nonExcludedSuites.length > 0) {
|
|
throw new Error(`The suite(s) "${nonExcludedSuites.join('", "')}" you specified don't exist in your config file or doesn't contain any files!`);
|
|
}
|
|
specs = isSpecParamPassed ? [...specs, ...suiteSpecs] : suiteSpecs;
|
|
}
|
|
specs = filterDublicationArrayItems(specs);
|
|
const hasSubsetOfSpecsDefined = isSpecParamPassed || suites.length > 0;
|
|
if (repeat && hasSubsetOfSpecsDefined) {
|
|
specs = Array.from({ length: repeat }, () => specs).flat();
|
|
} else if (repeat && !hasSubsetOfSpecsDefined) {
|
|
throw new Error("The --repeat flag requires that either the --spec or --suite flag is also set");
|
|
}
|
|
return this.shard(
|
|
this.filterSpecs(specs, exclude)
|
|
);
|
|
}
|
|
/**
|
|
* sets config attribute with file paths from filtering
|
|
* options from cli argument
|
|
*
|
|
* @param {string[]} cliArgFileList list of files in a string form
|
|
* @param {Object} config config object that stores the spec and exclude attributes
|
|
* cli argument
|
|
* @return {String[]} List of files that should be included or excluded
|
|
*/
|
|
setFilePathToFilterOptions(cliArgFileList, specs, group) {
|
|
const filesToFilter = /* @__PURE__ */ new Set();
|
|
const fileList = _ConfigParser.getFilePaths(specs, this._config.rootDir, this._pathService);
|
|
cliArgFileList.forEach((filteredFile) => {
|
|
filteredFile = removeLineNumbers(filteredFile);
|
|
const globMatchedFiles = _ConfigParser.getFilePaths(
|
|
group ? [[filteredFile]] : [filteredFile],
|
|
this._config.rootDir,
|
|
this._pathService
|
|
);
|
|
if (this._pathService.isFile(filteredFile)) {
|
|
filesToFilter.add(
|
|
this._pathService.ensureAbsolutePath(
|
|
filteredFile,
|
|
path3.dirname(this.#configFilePath)
|
|
)
|
|
);
|
|
} else if (globMatchedFiles.length) {
|
|
globMatchedFiles.forEach((file) => filesToFilter.add(file));
|
|
} else {
|
|
fileList.forEach((file) => {
|
|
if (typeof file === "string") {
|
|
if (isValidRegex(filteredFile) && file.match(filteredFile)) {
|
|
filesToFilter.add(file);
|
|
}
|
|
} else if (Array.isArray(file)) {
|
|
file.forEach((subFile) => {
|
|
if (isValidRegex(filteredFile) && subFile.match(filteredFile)) {
|
|
filesToFilter.add(subFile);
|
|
}
|
|
});
|
|
} else {
|
|
log.warn("Unexpected entry in specs that is neither string nor array: ", file);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
if (filesToFilter.size === 0) {
|
|
throw new Error(`spec file(s) ${cliArgFileList.join(", ")} not found`);
|
|
}
|
|
return [...filesToFilter];
|
|
}
|
|
/**
|
|
* return configs
|
|
*/
|
|
getConfig() {
|
|
if (!this.#isInitialised) {
|
|
throw new Error('ConfigParser was not initialized, call "await config.initialize()" first!');
|
|
}
|
|
return this._config;
|
|
}
|
|
/**
|
|
* return capabilities
|
|
*/
|
|
getCapabilities(i) {
|
|
if (!this.#isInitialised) {
|
|
throw new Error('ConfigParser was not initialized, call "await config.initialize()" first!');
|
|
}
|
|
if (typeof i === "number" && Array.isArray(this._capabilities) && this._capabilities[i]) {
|
|
return this._capabilities[i];
|
|
}
|
|
return this._capabilities;
|
|
}
|
|
/**
|
|
* returns a flattened list of globbed files
|
|
*
|
|
* @param {String[] | String[][]} patterns list of files to glob
|
|
* @param {Boolean} omitWarnings to indicate omission of warnings
|
|
* @param {FileSystemPathService} findAndGlob system path service for expanding globbed file names
|
|
* @param {number} hierarchyDepth depth to prevent recursive calling beyond a depth of 1
|
|
* @return {String[] | String[][]} list of files
|
|
*/
|
|
static getFilePaths(patterns, rootDir, findAndGlob = new FileSystemPathService(), hierarchyDepth) {
|
|
let files = [];
|
|
let groupedFiles = [];
|
|
if (typeof patterns === "string") {
|
|
patterns = [patterns];
|
|
}
|
|
if (!Array.isArray(patterns)) {
|
|
throw new Error("specs or exclude property should be an array of strings, specs may also be an array of string arrays");
|
|
}
|
|
patterns = patterns.map((pattern) => {
|
|
if (Array.isArray(pattern)) {
|
|
return pattern.map((subPattern) => removeLineNumbers(subPattern));
|
|
}
|
|
return removeLineNumbers(pattern);
|
|
});
|
|
for (let pattern of patterns) {
|
|
if (Array.isArray(pattern) && !hierarchyDepth) {
|
|
groupedFiles = _ConfigParser.getFilePaths(pattern, rootDir, findAndGlob, 1);
|
|
files.push(groupedFiles);
|
|
} else if (Array.isArray(pattern) && hierarchyDepth) {
|
|
log.error("Unexpected depth of hierarchical arrays");
|
|
} else if (pattern.startsWith("file://")) {
|
|
files.push(pattern);
|
|
} else {
|
|
pattern = pattern.toString().replace(/\\/g, "/");
|
|
let filenames = findAndGlob.glob(pattern, rootDir);
|
|
filenames = filenames.filter(
|
|
(filename) => SUPPORTED_FILE_EXTENSIONS.find(
|
|
(ext) => filename.endsWith(ext)
|
|
)
|
|
);
|
|
filenames = filenames.map((filename) => findAndGlob.ensureAbsolutePath(filename, rootDir));
|
|
if (filenames.length === 0) {
|
|
log.warn("pattern", pattern, "did not match any file");
|
|
}
|
|
files = [...files, ...new Set(filenames)];
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
/**
|
|
* returns specs files with the excludes filtered
|
|
*
|
|
* @param {String[] | String[][]} spec files - list of spec files
|
|
* @param {string[]} excludeList files - list of exclude files
|
|
* @return {String[] | String[][]} list of spec files with excludes removed
|
|
*/
|
|
filterSpecs(specs, excludeList) {
|
|
if (allKeywordsContainPath(excludeList)) {
|
|
const filteredSpec2 = specs.reduce((returnVal, currSpec) => {
|
|
if (Array.isArray(currSpec)) {
|
|
returnVal.push(currSpec.filter((specItem) => !excludeList.includes(specItem)));
|
|
} else if (excludeList.indexOf(currSpec) === -1) {
|
|
returnVal.push(currSpec);
|
|
}
|
|
return returnVal;
|
|
}, []);
|
|
return filterEmptyArrayItems(filteredSpec2);
|
|
}
|
|
const filteredSpec = specs.reduce((returnVal, currSpec) => {
|
|
if (Array.isArray(currSpec)) {
|
|
returnVal.push(currSpec.filter((specItem) => !excludeList.some((excludeVal) => specItem.includes(excludeVal))));
|
|
}
|
|
const isSpecExcluded = excludeList.some((excludedVal) => currSpec.includes(excludedVal));
|
|
if (!isSpecExcluded) {
|
|
returnVal.push(currSpec);
|
|
}
|
|
return returnVal;
|
|
}, []);
|
|
return filterEmptyArrayItems(filteredSpec);
|
|
}
|
|
shard(specs) {
|
|
if (!this._config.shard || this._config.shard.total === 1) {
|
|
return specs;
|
|
}
|
|
const { total, current } = this._config.shard;
|
|
const totalSpecs = specs.length;
|
|
const specsPerShard = Math.max(Math.round(totalSpecs / total), 1);
|
|
const end = current === total ? void 0 : specsPerShard * current;
|
|
return specs.slice(current * specsPerShard - specsPerShard, end);
|
|
}
|
|
};
|
|
function allKeywordsContainPath(excludedSpecList) {
|
|
return excludedSpecList.every((val) => val.includes("/") || val.includes("\\") || val.includes("*"));
|
|
}
|
|
function isSpecPattern(str) {
|
|
if (/[/\\*?]/.test(str)) {
|
|
return true;
|
|
}
|
|
if (/\.(js|ts|mjs|cjs|es6|feature)$/i.test(str)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
function filterEmptyArrayItems(specList) {
|
|
return specList.filter((item) => Array.isArray(item) && item.length || !Array.isArray(item));
|
|
}
|
|
function filterDublicationArrayItems(specList) {
|
|
return [...new Set(specList.map((item) => Array.isArray(item) ? [...new Set(item)] : item))];
|
|
}
|
|
function isValidRegex(expression) {
|
|
try {
|
|
new RegExp(expression);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
export {
|
|
ConfigParser,
|
|
FileSystemPathService
|
|
};
|