@nsxbet/playwright-orchestrator 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -0
- package/bin/run.js +5 -0
- package/dist/commands/assign.d.ts +23 -0
- package/dist/commands/assign.d.ts.map +1 -0
- package/dist/commands/assign.js +256 -0
- package/dist/commands/assign.js.map +1 -0
- package/dist/commands/extract-timing.d.ts +40 -0
- package/dist/commands/extract-timing.d.ts.map +1 -0
- package/dist/commands/extract-timing.js +196 -0
- package/dist/commands/extract-timing.js.map +1 -0
- package/dist/commands/list-tests.d.ts +16 -0
- package/dist/commands/list-tests.d.ts.map +1 -0
- package/dist/commands/list-tests.js +98 -0
- package/dist/commands/list-tests.js.map +1 -0
- package/dist/commands/merge-timing.d.ts +19 -0
- package/dist/commands/merge-timing.d.ts.map +1 -0
- package/dist/commands/merge-timing.js +217 -0
- package/dist/commands/merge-timing.js.map +1 -0
- package/dist/core/ckk-algorithm.d.ts +38 -0
- package/dist/core/ckk-algorithm.d.ts.map +1 -0
- package/dist/core/ckk-algorithm.js +192 -0
- package/dist/core/ckk-algorithm.js.map +1 -0
- package/dist/core/estimate.d.ts +72 -0
- package/dist/core/estimate.d.ts.map +1 -0
- package/dist/core/estimate.js +142 -0
- package/dist/core/estimate.js.map +1 -0
- package/dist/core/grep-pattern.d.ts +61 -0
- package/dist/core/grep-pattern.d.ts.map +1 -0
- package/dist/core/grep-pattern.js +104 -0
- package/dist/core/grep-pattern.js.map +1 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +9 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/lpt-algorithm.d.ts +28 -0
- package/dist/core/lpt-algorithm.d.ts.map +1 -0
- package/dist/core/lpt-algorithm.js +80 -0
- package/dist/core/lpt-algorithm.js.map +1 -0
- package/dist/core/slugify.d.ts +13 -0
- package/dist/core/slugify.d.ts.map +1 -0
- package/dist/core/slugify.js +19 -0
- package/dist/core/slugify.js.map +1 -0
- package/dist/core/test-discovery.d.ts +46 -0
- package/dist/core/test-discovery.d.ts.map +1 -0
- package/dist/core/test-discovery.js +192 -0
- package/dist/core/test-discovery.js.map +1 -0
- package/dist/core/timing-store.d.ts +90 -0
- package/dist/core/timing-store.d.ts.map +1 -0
- package/dist/core/timing-store.js +280 -0
- package/dist/core/timing-store.js.map +1 -0
- package/dist/core/types.d.ts +241 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +54 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Longest Processing Time First (LPT) algorithm for load balancing
|
|
3
|
+
*
|
|
4
|
+
* This greedy algorithm assigns jobs to workers by:
|
|
5
|
+
* 1. Sorting jobs by duration (descending)
|
|
6
|
+
* 2. Assigning each job to the worker with the smallest current load
|
|
7
|
+
*
|
|
8
|
+
* Time complexity: O(n log n) for sorting + O(n log k) for assignment = O(n log n)
|
|
9
|
+
* where n = number of files, k = number of shards
|
|
10
|
+
*
|
|
11
|
+
* @param files - Files with their durations
|
|
12
|
+
* @param numShards - Number of shards to distribute across
|
|
13
|
+
* @returns Shard assignments with expected durations
|
|
14
|
+
*/
|
|
15
|
+
export function assignWithLPT(files, numShards) {
|
|
16
|
+
// Initialize shards
|
|
17
|
+
const shards = Array.from({ length: numShards }, (_, i) => ({
|
|
18
|
+
shardIndex: i + 1, // 1-based index
|
|
19
|
+
files: [],
|
|
20
|
+
expectedDuration: 0,
|
|
21
|
+
}));
|
|
22
|
+
if (files.length === 0) {
|
|
23
|
+
return shards;
|
|
24
|
+
}
|
|
25
|
+
// Sort files by duration descending (longest first)
|
|
26
|
+
const sortedFiles = [...files].sort((a, b) => b.duration - a.duration);
|
|
27
|
+
// Assign each file to the shard with the smallest current load
|
|
28
|
+
for (const file of sortedFiles) {
|
|
29
|
+
// Find shard with minimum load
|
|
30
|
+
let minShard = shards[0];
|
|
31
|
+
for (const shard of shards) {
|
|
32
|
+
if (minShard && shard.expectedDuration < minShard.expectedDuration) {
|
|
33
|
+
minShard = shard;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Assign file to this shard
|
|
37
|
+
if (minShard) {
|
|
38
|
+
minShard.files.push(file.file);
|
|
39
|
+
minShard.expectedDuration += file.duration;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return shards;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Convert shard assignments to the format expected by the CLI output
|
|
46
|
+
*/
|
|
47
|
+
export function formatAssignResult(assignments, estimatedFiles) {
|
|
48
|
+
const shards = {};
|
|
49
|
+
const expectedDurations = {};
|
|
50
|
+
let totalFiles = 0;
|
|
51
|
+
for (const assignment of assignments) {
|
|
52
|
+
shards[assignment.shardIndex] = assignment.files;
|
|
53
|
+
expectedDurations[assignment.shardIndex] = assignment.expectedDuration;
|
|
54
|
+
totalFiles += assignment.files.length;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
shards,
|
|
58
|
+
expectedDurations,
|
|
59
|
+
totalFiles,
|
|
60
|
+
estimatedFiles,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Calculate the balance metric (max/min ratio) for the assignment
|
|
65
|
+
*
|
|
66
|
+
* A perfectly balanced assignment would have a ratio of 1.0
|
|
67
|
+
* The target is to keep this below 1.2 (20% difference)
|
|
68
|
+
*/
|
|
69
|
+
export function calculateBalanceRatio(assignments) {
|
|
70
|
+
const durations = assignments
|
|
71
|
+
.map((a) => a.expectedDuration)
|
|
72
|
+
.filter((d) => d > 0);
|
|
73
|
+
if (durations.length === 0) {
|
|
74
|
+
return 1.0;
|
|
75
|
+
}
|
|
76
|
+
const max = Math.max(...durations);
|
|
77
|
+
const min = Math.min(...durations);
|
|
78
|
+
return min > 0 ? max / min : 1.0;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=lpt-algorithm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lpt-algorithm.js","sourceRoot":"","sources":["../../src/core/lpt-algorithm.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,aAAa,CAC3B,KAAyB,EACzB,SAAiB;IAEjB,oBAAoB;IACpB,MAAM,MAAM,GAAsB,KAAK,CAAC,IAAI,CAC1C,EAAE,MAAM,EAAE,SAAS,EAAE,EACrB,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QACT,UAAU,EAAE,CAAC,GAAG,CAAC,EAAE,gBAAgB;QACnC,KAAK,EAAE,EAAE;QACT,gBAAgB,EAAE,CAAC;KACpB,CAAC,CACH,CAAC;IAEF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,oDAAoD;IACpD,MAAM,WAAW,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;IAEvE,+DAA+D;IAC/D,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,+BAA+B;QAC/B,IAAI,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IAAI,QAAQ,IAAI,KAAK,CAAC,gBAAgB,GAAG,QAAQ,CAAC,gBAAgB,EAAE,CAAC;gBACnE,QAAQ,GAAG,KAAK,CAAC;YACnB,CAAC;QACH,CAAC;QAED,4BAA4B;QAC5B,IAAI,QAAQ,EAAE,CAAC;YACb,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC/B,QAAQ,CAAC,gBAAgB,IAAI,IAAI,CAAC,QAAQ,CAAC;QAC7C,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAChC,WAA8B,EAC9B,cAAwB;IAExB,MAAM,MAAM,GAA6B,EAAE,CAAC;IAC5C,MAAM,iBAAiB,GAA2B,EAAE,CAAC;IACrD,IAAI,UAAU,GAAG,CAAC,CAAC;IAEnB,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC;QACjD,iBAAiB,CAAC,UAAU,CAAC,UAAU,CAAC,GAAG,UAAU,CAAC,gBAAgB,CAAC;QACvE,UAAU,IAAI,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC;IACxC,CAAC;IAED,OAAO;QACL,MAAM;QACN,iBAAiB;QACjB,UAAU;QACV,cAAc;KACf,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,WAA8B;IAClE,MAAM,SAAS,GAAG,WAAW;SAC1B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,gBAAgB,CAAC;SAC9B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAExB,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,GAAG,CAAC;IACb,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,CAAC;IACnC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,CAAC;IAEnC,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slugify a string for use in cache keys
|
|
3
|
+
*
|
|
4
|
+
* Converts to lowercase, replaces spaces and special characters with hyphens,
|
|
5
|
+
* and removes consecutive hyphens.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* slugify("Mobile Chrome") // "mobile-chrome"
|
|
9
|
+
* slugify("feature/ABC-123") // "feature-abc-123"
|
|
10
|
+
* slugify("refs/heads/main") // "refs-heads-main"
|
|
11
|
+
*/
|
|
12
|
+
export declare function slugify(input: string): string;
|
|
13
|
+
//# sourceMappingURL=slugify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slugify.d.ts","sourceRoot":"","sources":["../../src/core/slugify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM7C"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slugify a string for use in cache keys
|
|
3
|
+
*
|
|
4
|
+
* Converts to lowercase, replaces spaces and special characters with hyphens,
|
|
5
|
+
* and removes consecutive hyphens.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* slugify("Mobile Chrome") // "mobile-chrome"
|
|
9
|
+
* slugify("feature/ABC-123") // "feature-abc-123"
|
|
10
|
+
* slugify("refs/heads/main") // "refs-heads-main"
|
|
11
|
+
*/
|
|
12
|
+
export function slugify(input) {
|
|
13
|
+
return input
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
|
|
16
|
+
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
|
|
17
|
+
.replace(/-+/g, '-'); // Remove consecutive hyphens
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=slugify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slugify.js","sourceRoot":"","sources":["../../src/core/slugify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,OAAO,KAAK;SACT,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,wCAAwC;SACpE,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,kCAAkC;SAC1D,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,6BAA6B;AACvD,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { DiscoveredTest } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Discover tests by running Playwright with --list flag
|
|
4
|
+
*
|
|
5
|
+
* @param testDir - Path to test directory
|
|
6
|
+
* @param project - Optional Playwright project name
|
|
7
|
+
* @returns List of discovered tests
|
|
8
|
+
*/
|
|
9
|
+
export declare function discoverTests(testDir: string, project?: string): DiscoveredTest[];
|
|
10
|
+
/**
|
|
11
|
+
* Parse Playwright --list JSON output
|
|
12
|
+
*
|
|
13
|
+
* @param jsonOutput - Raw JSON output from Playwright --list
|
|
14
|
+
* @returns List of discovered tests
|
|
15
|
+
*/
|
|
16
|
+
export declare function parsePlaywrightListOutput(jsonOutput: string): DiscoveredTest[];
|
|
17
|
+
/**
|
|
18
|
+
* Discover tests by scanning test files directly (fallback method)
|
|
19
|
+
*
|
|
20
|
+
* This parses test files to find test definitions when Playwright --list isn't available.
|
|
21
|
+
* Uses regex to find test() and it() calls.
|
|
22
|
+
*
|
|
23
|
+
* @param testDir - Path to test directory
|
|
24
|
+
* @param globPattern - Glob pattern for test files
|
|
25
|
+
* @returns List of discovered tests
|
|
26
|
+
*/
|
|
27
|
+
export declare function discoverTestsFromFiles(testDir: string, globPattern?: string): DiscoveredTest[];
|
|
28
|
+
/**
|
|
29
|
+
* Parse test definitions from source code
|
|
30
|
+
*
|
|
31
|
+
* Extracts test() and it() calls with their describe() context.
|
|
32
|
+
* This is a simple regex-based parser that handles common patterns.
|
|
33
|
+
*
|
|
34
|
+
* @param source - Source code content
|
|
35
|
+
* @param fileName - Name of the source file
|
|
36
|
+
* @returns List of discovered tests
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseTestsFromSource(source: string, fileName: string): DiscoveredTest[];
|
|
39
|
+
/**
|
|
40
|
+
* Group tests by file
|
|
41
|
+
*
|
|
42
|
+
* @param tests - List of discovered tests
|
|
43
|
+
* @returns Map of file name to tests
|
|
44
|
+
*/
|
|
45
|
+
export declare function groupTestsByFile(tests: DiscoveredTest[]): Map<string, DiscoveredTest[]>;
|
|
46
|
+
//# sourceMappingURL=test-discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-discovery.d.ts","sourceRoot":"","sources":["../../src/core/test-discovery.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,cAAc,EAGf,MAAM,YAAY,CAAC;AAGpB;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,MAAM,GACf,cAAc,EAAE,CAsBlB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,MAAM,GACjB,cAAc,EAAE,CAsBlB;AA8CD;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,EACf,WAAW,GAAE,MAAuB,GACnC,cAAc,EAAE,CAalB;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,cAAc,EAAE,CA0DlB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,EAAE,GACtB,GAAG,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC,CAU/B"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { glob } from 'glob';
|
|
5
|
+
import { buildTestId } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Discover tests by running Playwright with --list flag
|
|
8
|
+
*
|
|
9
|
+
* @param testDir - Path to test directory
|
|
10
|
+
* @param project - Optional Playwright project name
|
|
11
|
+
* @returns List of discovered tests
|
|
12
|
+
*/
|
|
13
|
+
export function discoverTests(testDir, project) {
|
|
14
|
+
const projectFlag = project ? `--project="${project}"` : '';
|
|
15
|
+
const cmd = `npx playwright test --list --reporter=json ${projectFlag}`.trim();
|
|
16
|
+
try {
|
|
17
|
+
const output = execSync(cmd, {
|
|
18
|
+
cwd: testDir,
|
|
19
|
+
encoding: 'utf-8',
|
|
20
|
+
maxBuffer: 50 * 1024 * 1024, // 50MB buffer for large test suites
|
|
21
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
22
|
+
});
|
|
23
|
+
return parsePlaywrightListOutput(output);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
// Playwright might exit with non-zero even for --list if there are issues
|
|
27
|
+
const execError = error;
|
|
28
|
+
if (execError.stdout) {
|
|
29
|
+
return parsePlaywrightListOutput(execError.stdout);
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parse Playwright --list JSON output
|
|
36
|
+
*
|
|
37
|
+
* @param jsonOutput - Raw JSON output from Playwright --list
|
|
38
|
+
* @returns List of discovered tests
|
|
39
|
+
*/
|
|
40
|
+
export function parsePlaywrightListOutput(jsonOutput) {
|
|
41
|
+
const tests = [];
|
|
42
|
+
try {
|
|
43
|
+
const data = JSON.parse(jsonOutput);
|
|
44
|
+
for (const suite of data.suites) {
|
|
45
|
+
extractTestsFromSuite(suite, [], tests);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Try parsing line by line if JSON is malformed (older Playwright versions)
|
|
50
|
+
// or if output contains additional text
|
|
51
|
+
const jsonMatch = jsonOutput.match(/\{[\s\S]*\}/);
|
|
52
|
+
if (jsonMatch) {
|
|
53
|
+
const data = JSON.parse(jsonMatch[0]);
|
|
54
|
+
for (const suite of data.suites) {
|
|
55
|
+
extractTestsFromSuite(suite, [], tests);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return tests;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Recursively extract tests from a Playwright suite
|
|
63
|
+
*/
|
|
64
|
+
function extractTestsFromSuite(suite, parentTitles, tests) {
|
|
65
|
+
const currentTitles = suite.title && suite.title !== ''
|
|
66
|
+
? [...parentTitles, suite.title]
|
|
67
|
+
: parentTitles;
|
|
68
|
+
// Process specs (actual tests)
|
|
69
|
+
if (suite.specs) {
|
|
70
|
+
for (const spec of suite.specs) {
|
|
71
|
+
const titlePath = [...currentTitles, spec.title];
|
|
72
|
+
const file = getRelativeFilePath(spec.file || suite.file);
|
|
73
|
+
tests.push({
|
|
74
|
+
file,
|
|
75
|
+
title: spec.title,
|
|
76
|
+
titlePath,
|
|
77
|
+
testId: buildTestId(file, titlePath),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Process nested suites
|
|
82
|
+
if (suite.suites) {
|
|
83
|
+
for (const nestedSuite of suite.suites) {
|
|
84
|
+
extractTestsFromSuite(nestedSuite, currentTitles, tests);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get relative file path from absolute path
|
|
90
|
+
*/
|
|
91
|
+
function getRelativeFilePath(filePath) {
|
|
92
|
+
// Extract just the filename for test IDs
|
|
93
|
+
return path.basename(filePath);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Discover tests by scanning test files directly (fallback method)
|
|
97
|
+
*
|
|
98
|
+
* This parses test files to find test definitions when Playwright --list isn't available.
|
|
99
|
+
* Uses regex to find test() and it() calls.
|
|
100
|
+
*
|
|
101
|
+
* @param testDir - Path to test directory
|
|
102
|
+
* @param globPattern - Glob pattern for test files
|
|
103
|
+
* @returns List of discovered tests
|
|
104
|
+
*/
|
|
105
|
+
export function discoverTestsFromFiles(testDir, globPattern = '**/*.spec.ts') {
|
|
106
|
+
const tests = [];
|
|
107
|
+
const files = glob.sync(globPattern, { cwd: testDir, absolute: true });
|
|
108
|
+
for (const filePath of files) {
|
|
109
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
110
|
+
const fileName = path.basename(filePath);
|
|
111
|
+
const fileTests = parseTestsFromSource(content, fileName);
|
|
112
|
+
tests.push(...fileTests);
|
|
113
|
+
}
|
|
114
|
+
return tests;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Parse test definitions from source code
|
|
118
|
+
*
|
|
119
|
+
* Extracts test() and it() calls with their describe() context.
|
|
120
|
+
* This is a simple regex-based parser that handles common patterns.
|
|
121
|
+
*
|
|
122
|
+
* @param source - Source code content
|
|
123
|
+
* @param fileName - Name of the source file
|
|
124
|
+
* @returns List of discovered tests
|
|
125
|
+
*/
|
|
126
|
+
export function parseTestsFromSource(source, fileName) {
|
|
127
|
+
const tests = [];
|
|
128
|
+
// Match describe blocks and test/it calls
|
|
129
|
+
// This is a simplified parser - for full accuracy, use Playwright --list
|
|
130
|
+
const describeRegex = /(?:test\.)?describe\s*\(\s*['"`]([^'"`]+)['"`]/g;
|
|
131
|
+
const testRegex = /(?:test|it)\s*\(\s*['"`]([^'"`]+)['"`]/g;
|
|
132
|
+
// Find all describe blocks with their positions
|
|
133
|
+
const describes = [];
|
|
134
|
+
// Extract all describe blocks
|
|
135
|
+
for (const match of source.matchAll(describeRegex)) {
|
|
136
|
+
// Find matching closing brace (simplified - counts braces)
|
|
137
|
+
const start = match.index ?? 0;
|
|
138
|
+
let braceCount = 0;
|
|
139
|
+
let end = start;
|
|
140
|
+
let foundOpen = false;
|
|
141
|
+
for (let i = start; i < source.length; i++) {
|
|
142
|
+
if (source[i] === '{') {
|
|
143
|
+
braceCount++;
|
|
144
|
+
foundOpen = true;
|
|
145
|
+
}
|
|
146
|
+
else if (source[i] === '}') {
|
|
147
|
+
braceCount--;
|
|
148
|
+
if (foundOpen && braceCount === 0) {
|
|
149
|
+
end = i;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
describes.push({ title: match[1] ?? '', start, end });
|
|
155
|
+
}
|
|
156
|
+
// Find all tests
|
|
157
|
+
for (const match of source.matchAll(testRegex)) {
|
|
158
|
+
const testTitle = match[1] ?? '';
|
|
159
|
+
const testPos = match.index ?? 0;
|
|
160
|
+
// Find which describe blocks contain this test
|
|
161
|
+
const titlePath = [];
|
|
162
|
+
for (const desc of describes) {
|
|
163
|
+
if (testPos > desc.start && testPos < desc.end) {
|
|
164
|
+
titlePath.push(desc.title);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
titlePath.push(testTitle);
|
|
168
|
+
tests.push({
|
|
169
|
+
file: fileName,
|
|
170
|
+
title: testTitle,
|
|
171
|
+
titlePath,
|
|
172
|
+
testId: buildTestId(fileName, titlePath),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return tests;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Group tests by file
|
|
179
|
+
*
|
|
180
|
+
* @param tests - List of discovered tests
|
|
181
|
+
* @returns Map of file name to tests
|
|
182
|
+
*/
|
|
183
|
+
export function groupTestsByFile(tests) {
|
|
184
|
+
const grouped = new Map();
|
|
185
|
+
for (const test of tests) {
|
|
186
|
+
const existing = grouped.get(test.file) || [];
|
|
187
|
+
existing.push(test);
|
|
188
|
+
grouped.set(test.file, existing);
|
|
189
|
+
}
|
|
190
|
+
return grouped;
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=test-discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-discovery.js","sourceRoot":"","sources":["../../src/core/test-discovery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ShardTimingArtifact, TestShardTimingArtifact, TimingData, TimingDataV1, TimingDataV2 } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Default EMA smoothing factor (alpha)
|
|
4
|
+
* Higher values give more weight to recent measurements
|
|
5
|
+
*/
|
|
6
|
+
export declare const DEFAULT_EMA_ALPHA = 0.3;
|
|
7
|
+
/**
|
|
8
|
+
* Default number of days after which to prune old entries
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_PRUNE_DAYS = 30;
|
|
11
|
+
/**
|
|
12
|
+
* Load timing data from a JSON file
|
|
13
|
+
*
|
|
14
|
+
* Supports both v1 (file-level) and v2 (test-level) formats.
|
|
15
|
+
*
|
|
16
|
+
* @param filePath - Path to the timing data JSON file
|
|
17
|
+
* @returns Timing data, or empty data if file doesn't exist
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadTimingData(filePath: string): TimingData;
|
|
20
|
+
/**
|
|
21
|
+
* Save timing data to a JSON file
|
|
22
|
+
*/
|
|
23
|
+
export declare function saveTimingData(filePath: string, data: TimingData): void;
|
|
24
|
+
/**
|
|
25
|
+
* Calculate Exponential Moving Average for duration
|
|
26
|
+
*
|
|
27
|
+
* Formula: newDuration = α * measuredDuration + (1 - α) * oldDuration
|
|
28
|
+
*
|
|
29
|
+
* @param oldDuration - Previous duration value
|
|
30
|
+
* @param newDuration - New measured duration
|
|
31
|
+
* @param alpha - Smoothing factor (0-1), higher = more weight on new value
|
|
32
|
+
*/
|
|
33
|
+
export declare function calculateEMA(oldDuration: number, newDuration: number, alpha?: number): number;
|
|
34
|
+
/**
|
|
35
|
+
* Merge new timing measurements into existing timing data using EMA (v1 - file-level)
|
|
36
|
+
*
|
|
37
|
+
* @param existing - Existing timing data (v1)
|
|
38
|
+
* @param newMeasurements - New measurements from shard artifacts
|
|
39
|
+
* @param alpha - EMA smoothing factor
|
|
40
|
+
* @returns Updated timing data
|
|
41
|
+
*/
|
|
42
|
+
export declare function mergeTimingData(existing: TimingData, newMeasurements: ShardTimingArtifact[], alpha?: number): TimingDataV1;
|
|
43
|
+
/**
|
|
44
|
+
* Merge new timing measurements into existing timing data using EMA (v2 - test-level)
|
|
45
|
+
*
|
|
46
|
+
* @param existing - Existing timing data (v2)
|
|
47
|
+
* @param newMeasurements - New measurements from shard artifacts
|
|
48
|
+
* @param alpha - EMA smoothing factor
|
|
49
|
+
* @returns Updated timing data
|
|
50
|
+
*/
|
|
51
|
+
export declare function mergeTestTimingData(existing: TimingDataV2 | null, newMeasurements: TestShardTimingArtifact[], alpha?: number): TimingDataV2;
|
|
52
|
+
/**
|
|
53
|
+
* Prune old entries from timing data (v1 - file-level)
|
|
54
|
+
*
|
|
55
|
+
* Removes entries that:
|
|
56
|
+
* 1. Haven't been run in more than `days` days
|
|
57
|
+
* 2. No longer exist in the current test files (if provided)
|
|
58
|
+
*
|
|
59
|
+
* @param data - Timing data to prune
|
|
60
|
+
* @param days - Number of days after which to remove entries
|
|
61
|
+
* @param currentFiles - Optional list of current test files (to remove deleted tests)
|
|
62
|
+
* @returns Pruned timing data
|
|
63
|
+
*/
|
|
64
|
+
export declare function pruneTimingData(data: TimingData, days?: number, currentFiles?: string[]): TimingData;
|
|
65
|
+
/**
|
|
66
|
+
* Prune old entries from timing data (v2 - test-level)
|
|
67
|
+
*
|
|
68
|
+
* @param data - Timing data to prune
|
|
69
|
+
* @param days - Number of days after which to remove entries
|
|
70
|
+
* @param currentTestIds - Optional list of current test IDs (to remove deleted tests)
|
|
71
|
+
* @returns Pruned timing data
|
|
72
|
+
*/
|
|
73
|
+
export declare function pruneTestTimingData(data: TimingDataV2, days?: number, currentTestIds?: string[]): TimingDataV2;
|
|
74
|
+
/**
|
|
75
|
+
* Get duration for a file from timing data (v1)
|
|
76
|
+
*
|
|
77
|
+
* @param data - Timing data
|
|
78
|
+
* @param file - File name
|
|
79
|
+
* @returns Duration in ms, or undefined if not found
|
|
80
|
+
*/
|
|
81
|
+
export declare function getFileDuration(data: TimingData, file: string): number | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Get duration for a test from timing data (v2)
|
|
84
|
+
*
|
|
85
|
+
* @param data - Timing data (v2)
|
|
86
|
+
* @param testId - Test ID
|
|
87
|
+
* @returns Duration in ms, or undefined if not found
|
|
88
|
+
*/
|
|
89
|
+
export declare function getTestDuration(data: TimingDataV2, testId: string): number | undefined;
|
|
90
|
+
//# sourceMappingURL=timing-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing-store.d.ts","sourceRoot":"","sources":["../../src/core/timing-store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,mBAAmB,EACnB,uBAAuB,EAEvB,UAAU,EACV,YAAY,EACZ,YAAY,EACb,MAAM,YAAY,CAAC;AASpB;;;GAGG;AACH,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAErC;;GAEG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CA4B3D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CAGvE;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAC1B,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,KAAK,GAAE,MAA0B,GAChC,MAAM,CAER;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,UAAU,EACpB,eAAe,EAAE,mBAAmB,EAAE,EACtC,KAAK,GAAE,MAA0B,GAChC,YAAY,CAqCd;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,YAAY,GAAG,IAAI,EAC7B,eAAe,EAAE,uBAAuB,EAAE,EAC1C,KAAK,GAAE,MAA0B,GAChC,YAAY,CAmCd;AAkDD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,IAAI,GAAE,MAA2B,EACjC,YAAY,CAAC,EAAE,MAAM,EAAE,GACtB,UAAU,CAgCZ;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,YAAY,EAClB,IAAI,GAAE,MAA2B,EACjC,cAAc,CAAC,EAAE,MAAM,EAAE,GACxB,YAAY,CA4Bd;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,MAAM,GACX,MAAM,GAAG,SAAS,CAcpB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,MAAM,GACb,MAAM,GAAG,SAAS,CAEpB"}
|