@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
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# @nsxbet/playwright-orchestrator
|
|
2
|
+
|
|
3
|
+
Intelligent Playwright test distribution across CI shards using historical timing data.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
Default Playwright sharding (`--shard=N/M`) distributes tests by **file count**, not by duration. This creates significant imbalance:
|
|
8
|
+
|
|
9
|
+
| Shard | Duration | vs Fastest |
|
|
10
|
+
|-------|----------|------------|
|
|
11
|
+
| Shard 1 | ~31 min | +182% |
|
|
12
|
+
| Shard 2 | ~15 min | +36% |
|
|
13
|
+
| Shard 3 | ~22 min | +100% |
|
|
14
|
+
| Shard 4 | ~11 min | baseline |
|
|
15
|
+
|
|
16
|
+
Your CI is bottlenecked by the slowest shard, wasting runner time.
|
|
17
|
+
|
|
18
|
+
## The Solution
|
|
19
|
+
|
|
20
|
+
This orchestrator:
|
|
21
|
+
|
|
22
|
+
1. **Learns** test durations from previous runs
|
|
23
|
+
2. **Distributes** tests optimally using the CKK algorithm
|
|
24
|
+
3. **Balances** shards to within 10-15% of each other
|
|
25
|
+
|
|
26
|
+
Result: All shards finish at roughly the same time.
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Install
|
|
32
|
+
bun add -D @nsxbet/playwright-orchestrator
|
|
33
|
+
|
|
34
|
+
# Discover tests
|
|
35
|
+
playwright-orchestrator list-tests --test-dir ./e2e
|
|
36
|
+
|
|
37
|
+
# Assign tests to shards (with timing data)
|
|
38
|
+
playwright-orchestrator assign \
|
|
39
|
+
--test-dir ./e2e \
|
|
40
|
+
--timing-file ./timing-data.json \
|
|
41
|
+
--shards 4
|
|
42
|
+
|
|
43
|
+
# Extract timing from report
|
|
44
|
+
playwright-orchestrator extract-timing \
|
|
45
|
+
--report-file ./playwright-report/results.json \
|
|
46
|
+
--output-file ./shard-1-timing.json
|
|
47
|
+
|
|
48
|
+
# Merge timing data
|
|
49
|
+
playwright-orchestrator merge-timing \
|
|
50
|
+
--existing ./timing-data.json \
|
|
51
|
+
--new ./shard-1-timing.json ./shard-2-timing.json \
|
|
52
|
+
--output ./timing-data.json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## How It Works
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
59
|
+
│ Orchestrate │────▶│ Run Tests │────▶│ Merge Timing │
|
|
60
|
+
│ (1 job) │ │ (N parallel) │ │ (1 job) │
|
|
61
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
62
|
+
│ │ │
|
|
63
|
+
▼ ▼ ▼
|
|
64
|
+
Run CKK once Read shard-files Merge all shards
|
|
65
|
+
Output all shards from job outputs Update cache
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
1. **Orchestrate**: Run once, compute assignments for ALL shards
|
|
69
|
+
2. **Run Tests**: Each shard reads its files from `needs.orchestrate.outputs`
|
|
70
|
+
3. **Merge**: Collect timing from all shards, update history with EMA
|
|
71
|
+
|
|
72
|
+
## GitHub Actions (External Repositories)
|
|
73
|
+
|
|
74
|
+
Use the orchestrator in your own repository. The recommended pattern runs orchestration **once** before matrix jobs:
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
jobs:
|
|
78
|
+
# Phase 1: Orchestrate (runs once)
|
|
79
|
+
orchestrate:
|
|
80
|
+
runs-on: ubuntu-24.04
|
|
81
|
+
outputs:
|
|
82
|
+
shard-files: ${{ steps.orchestrate.outputs.shard-files }}
|
|
83
|
+
steps:
|
|
84
|
+
- uses: actions/checkout@v4
|
|
85
|
+
- uses: NSXBet/playwright-orchestrator/.github/actions/setup-orchestrator@v1
|
|
86
|
+
|
|
87
|
+
# YOU control cache location
|
|
88
|
+
- uses: actions/cache/restore@v4
|
|
89
|
+
with:
|
|
90
|
+
path: timing-data.json
|
|
91
|
+
key: playwright-timing-${{ github.ref_name }}
|
|
92
|
+
restore-keys: playwright-timing-
|
|
93
|
+
|
|
94
|
+
# Action handles all orchestration logic
|
|
95
|
+
- uses: NSXBet/playwright-orchestrator/.github/actions/orchestrate@v1
|
|
96
|
+
id: orchestrate
|
|
97
|
+
with:
|
|
98
|
+
test-dir: ./e2e
|
|
99
|
+
shards: 4
|
|
100
|
+
timing-file: timing-data.json
|
|
101
|
+
# No shard-index = outputs ALL shards
|
|
102
|
+
|
|
103
|
+
# Phase 2: Run tests (parallel matrix)
|
|
104
|
+
e2e:
|
|
105
|
+
needs: [orchestrate]
|
|
106
|
+
runs-on: ubuntu-24.04
|
|
107
|
+
strategy:
|
|
108
|
+
fail-fast: false
|
|
109
|
+
matrix:
|
|
110
|
+
shard: [1, 2, 3, 4]
|
|
111
|
+
steps:
|
|
112
|
+
- uses: actions/checkout@v4
|
|
113
|
+
|
|
114
|
+
# Action handles parsing + fallback
|
|
115
|
+
- uses: NSXBet/playwright-orchestrator/.github/actions/get-shard@v1
|
|
116
|
+
id: shard
|
|
117
|
+
with:
|
|
118
|
+
shard-files: ${{ needs.orchestrate.outputs.shard-files }}
|
|
119
|
+
shard-index: ${{ matrix.shard }}
|
|
120
|
+
shards: 4
|
|
121
|
+
|
|
122
|
+
# Just works - either files or --shard=N/M
|
|
123
|
+
- run: npx playwright test ${{ steps.shard.outputs.test-args }}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
See [docs/external-integration.md](./docs/external-integration.md) for complete workflow with timing data persistence.
|
|
127
|
+
|
|
128
|
+
## CLI Commands
|
|
129
|
+
|
|
130
|
+
| Command | Description |
|
|
131
|
+
|---------|-------------|
|
|
132
|
+
| `list-tests` | Discover tests in a project |
|
|
133
|
+
| `assign` | Distribute tests across shards |
|
|
134
|
+
| `extract-timing` | Extract timing from Playwright report |
|
|
135
|
+
| `merge-timing` | Merge timing data with EMA smoothing |
|
|
136
|
+
|
|
137
|
+
Run `playwright-orchestrator <command> --help` for details.
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# Install dependencies
|
|
143
|
+
make install
|
|
144
|
+
|
|
145
|
+
# Run quality checks
|
|
146
|
+
make lint # Biome linter
|
|
147
|
+
make typecheck # TypeScript
|
|
148
|
+
make test # Bun test
|
|
149
|
+
|
|
150
|
+
# Build
|
|
151
|
+
make build
|
|
152
|
+
|
|
153
|
+
# Run CI locally (requires Act)
|
|
154
|
+
make act-test
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
See [AGENTS.md](./AGENTS.md) for AI assistant instructions.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT
|
package/bin/run.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Assign extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
'test-dir': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
'timing-file': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
shards: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
'output-format': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'fallback-ms-per-line': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
'glob-pattern': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
level: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
private runTestLevel;
|
|
18
|
+
private runFileLevel;
|
|
19
|
+
private outputTestResult;
|
|
20
|
+
private outputResult;
|
|
21
|
+
private formatDuration;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=assign.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assign.d.ts","sourceRoot":"","sources":["../../src/commands/assign.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAS,MAAM,aAAa,CAAC;AAqB7C,MAAM,CAAC,OAAO,OAAO,MAAO,SAAQ,OAAO;IACzC,OAAgB,WAAW,SAC8C;IAEzE,OAAgB,QAAQ,WAItB;IAEF,OAAgB,KAAK;;;;;;;;;;MA4CnB;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;YAaZ,YAAY;YAyGZ,YAAY;IAuF1B,OAAO,CAAC,gBAAgB;IA0CxB,OAAO,CAAC,YAAY;IA6BpB,OAAO,CAAC,cAAc;CAQvB"}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { Command, Flags } from '@oclif/core';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
import { assignWithCKK, assignWithLPT, DEFAULT_CKK_TIMEOUT, DEFAULT_MS_PER_LINE, discoverTestsFromFiles, estimateDuration, formatAssignResult, generateGrepPatterns, getFileDuration, getTestDurations, isTimingDataV2, loadTimingData, } from '../core/index.js';
|
|
5
|
+
export default class Assign extends Command {
|
|
6
|
+
static description = 'Assign test files or tests to shards based on historical timing data';
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> assign --test-dir ./src/test/e2e --shards 4',
|
|
9
|
+
'<%= config.bin %> assign --test-dir ./e2e --timing-file ./timing.json --shards 4 --output-format json',
|
|
10
|
+
'<%= config.bin %> assign --test-dir ./e2e --shards 4 --level test --timeout 500',
|
|
11
|
+
];
|
|
12
|
+
static flags = {
|
|
13
|
+
'test-dir': Flags.string({
|
|
14
|
+
char: 'd',
|
|
15
|
+
description: 'Path to test directory containing spec files',
|
|
16
|
+
required: true,
|
|
17
|
+
}),
|
|
18
|
+
'timing-file': Flags.string({
|
|
19
|
+
char: 't',
|
|
20
|
+
description: 'Path to timing data JSON file (optional)',
|
|
21
|
+
}),
|
|
22
|
+
shards: Flags.integer({
|
|
23
|
+
char: 's',
|
|
24
|
+
description: 'Number of shards to distribute tests across',
|
|
25
|
+
required: true,
|
|
26
|
+
}),
|
|
27
|
+
'output-format': Flags.string({
|
|
28
|
+
char: 'f',
|
|
29
|
+
description: 'Output format',
|
|
30
|
+
default: 'json',
|
|
31
|
+
options: ['json', 'text'],
|
|
32
|
+
}),
|
|
33
|
+
'fallback-ms-per-line': Flags.integer({
|
|
34
|
+
description: 'Milliseconds per line for duration estimation',
|
|
35
|
+
default: DEFAULT_MS_PER_LINE,
|
|
36
|
+
}),
|
|
37
|
+
verbose: Flags.boolean({
|
|
38
|
+
char: 'v',
|
|
39
|
+
description: 'Show verbose output',
|
|
40
|
+
default: false,
|
|
41
|
+
}),
|
|
42
|
+
'glob-pattern': Flags.string({
|
|
43
|
+
description: 'Glob pattern for test files',
|
|
44
|
+
default: '**/*.spec.ts',
|
|
45
|
+
}),
|
|
46
|
+
level: Flags.string({
|
|
47
|
+
char: 'l',
|
|
48
|
+
description: 'Distribution level: file or test',
|
|
49
|
+
default: 'test',
|
|
50
|
+
options: ['file', 'test'],
|
|
51
|
+
}),
|
|
52
|
+
timeout: Flags.integer({
|
|
53
|
+
description: 'CKK algorithm timeout in milliseconds (test-level only)',
|
|
54
|
+
default: DEFAULT_CKK_TIMEOUT,
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
async run() {
|
|
58
|
+
const { flags } = await this.parse(Assign);
|
|
59
|
+
const testDir = path.resolve(flags['test-dir']);
|
|
60
|
+
const pattern = flags['glob-pattern'];
|
|
61
|
+
if (flags.level === 'test') {
|
|
62
|
+
await this.runTestLevel(testDir, pattern, flags);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
await this.runFileLevel(testDir, pattern, flags);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async runTestLevel(testDir, pattern, flags) {
|
|
69
|
+
// Discover tests
|
|
70
|
+
const tests = discoverTestsFromFiles(testDir, pattern);
|
|
71
|
+
if (tests.length === 0) {
|
|
72
|
+
this.warn(`No tests found in ${testDir} matching ${pattern}`);
|
|
73
|
+
this.outputTestResult({
|
|
74
|
+
shards: Object.fromEntries(Array.from({ length: flags.shards }, (_, i) => [i + 1, []])),
|
|
75
|
+
grepPatterns: Object.fromEntries(Array.from({ length: flags.shards }, (_, i) => [i + 1, ''])),
|
|
76
|
+
expectedDurations: Object.fromEntries(Array.from({ length: flags.shards }, (_, i) => [i + 1, 0])),
|
|
77
|
+
totalTests: 0,
|
|
78
|
+
estimatedTests: [],
|
|
79
|
+
isOptimal: true,
|
|
80
|
+
}, flags['output-format']);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (flags.verbose) {
|
|
84
|
+
this.log(`Found ${tests.length} tests in ${testDir}`);
|
|
85
|
+
}
|
|
86
|
+
// Load timing data if available
|
|
87
|
+
let timingData = null;
|
|
88
|
+
if (flags['timing-file']) {
|
|
89
|
+
const loadedData = loadTimingData(flags['timing-file']);
|
|
90
|
+
if (isTimingDataV2(loadedData)) {
|
|
91
|
+
timingData = loadedData;
|
|
92
|
+
}
|
|
93
|
+
else if (flags.verbose) {
|
|
94
|
+
this.warn('Timing data is v1 (file-level), will estimate all tests');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Get test durations
|
|
98
|
+
const testsWithDurations = getTestDurations(tests, timingData);
|
|
99
|
+
const estimatedTests = testsWithDurations
|
|
100
|
+
.filter((t) => t.estimated)
|
|
101
|
+
.map((t) => t.testId);
|
|
102
|
+
if (flags.verbose && estimatedTests.length > 0) {
|
|
103
|
+
this.log(`Estimated duration for ${estimatedTests.length} tests (no historical data)`);
|
|
104
|
+
}
|
|
105
|
+
// Convert to TestWithDuration format
|
|
106
|
+
const testInputs = testsWithDurations.map((t) => ({
|
|
107
|
+
testId: t.testId,
|
|
108
|
+
file: t.file,
|
|
109
|
+
duration: t.duration,
|
|
110
|
+
estimated: t.estimated,
|
|
111
|
+
}));
|
|
112
|
+
// Run CKK algorithm
|
|
113
|
+
const ckkResult = assignWithCKK(testInputs, flags.shards, flags.timeout);
|
|
114
|
+
if (flags.verbose) {
|
|
115
|
+
this.log(`Assignment ${ckkResult.isOptimal ? 'optimal' : 'near-optimal (LPT fallback)'}`);
|
|
116
|
+
this.log(`Makespan: ${this.formatDuration(ckkResult.makespan)}`);
|
|
117
|
+
}
|
|
118
|
+
// Generate grep patterns
|
|
119
|
+
const shardTests = {};
|
|
120
|
+
for (const assignment of ckkResult.assignments) {
|
|
121
|
+
shardTests[assignment.shardIndex] = assignment.tests;
|
|
122
|
+
}
|
|
123
|
+
const grepPatterns = generateGrepPatterns(shardTests);
|
|
124
|
+
// Build result
|
|
125
|
+
const result = {
|
|
126
|
+
shards: shardTests,
|
|
127
|
+
grepPatterns,
|
|
128
|
+
expectedDurations: Object.fromEntries(ckkResult.assignments.map((a) => [a.shardIndex, a.expectedDuration])),
|
|
129
|
+
totalTests: tests.length,
|
|
130
|
+
estimatedTests,
|
|
131
|
+
isOptimal: ckkResult.isOptimal,
|
|
132
|
+
};
|
|
133
|
+
this.outputTestResult(result, flags['output-format'], flags.verbose);
|
|
134
|
+
}
|
|
135
|
+
async runFileLevel(testDir, pattern, flags) {
|
|
136
|
+
// Find all test files
|
|
137
|
+
const testFiles = await glob(pattern, {
|
|
138
|
+
cwd: testDir,
|
|
139
|
+
nodir: true,
|
|
140
|
+
});
|
|
141
|
+
if (testFiles.length === 0) {
|
|
142
|
+
this.warn(`No test files found in ${testDir} matching ${pattern}`);
|
|
143
|
+
this.outputResult({
|
|
144
|
+
shards: Object.fromEntries(Array.from({ length: flags.shards }, (_, i) => [i + 1, []])),
|
|
145
|
+
expectedDurations: Object.fromEntries(Array.from({ length: flags.shards }, (_, i) => [i + 1, 0])),
|
|
146
|
+
totalFiles: 0,
|
|
147
|
+
estimatedFiles: [],
|
|
148
|
+
}, flags['output-format']);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (flags.verbose) {
|
|
152
|
+
this.log(`Found ${testFiles.length} test files in ${testDir}`);
|
|
153
|
+
}
|
|
154
|
+
// Load timing data if available
|
|
155
|
+
const timingData = flags['timing-file']
|
|
156
|
+
? loadTimingData(flags['timing-file'])
|
|
157
|
+
: { version: 1, updatedAt: '', files: {} };
|
|
158
|
+
// Build file list with durations
|
|
159
|
+
const filesWithDurations = [];
|
|
160
|
+
const estimatedFiles = [];
|
|
161
|
+
for (const file of testFiles) {
|
|
162
|
+
const historicalDuration = getFileDuration(timingData, file);
|
|
163
|
+
if (historicalDuration !== undefined) {
|
|
164
|
+
filesWithDurations.push({
|
|
165
|
+
file,
|
|
166
|
+
duration: historicalDuration,
|
|
167
|
+
estimated: false,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
// Estimate based on line count
|
|
172
|
+
const fullPath = path.join(testDir, file);
|
|
173
|
+
const estimated = estimateDuration(fullPath, flags['fallback-ms-per-line']);
|
|
174
|
+
filesWithDurations.push({
|
|
175
|
+
file,
|
|
176
|
+
duration: estimated,
|
|
177
|
+
estimated: true,
|
|
178
|
+
});
|
|
179
|
+
estimatedFiles.push(file);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (flags.verbose && estimatedFiles.length > 0) {
|
|
183
|
+
this.log(`Estimated duration for ${estimatedFiles.length} files (no historical data)`);
|
|
184
|
+
}
|
|
185
|
+
// Run LPT algorithm
|
|
186
|
+
const assignments = assignWithLPT(filesWithDurations, flags.shards);
|
|
187
|
+
const result = formatAssignResult(assignments, estimatedFiles);
|
|
188
|
+
// Output result
|
|
189
|
+
this.outputResult(result, flags['output-format'], flags.verbose);
|
|
190
|
+
}
|
|
191
|
+
outputTestResult(result, format, verbose = false) {
|
|
192
|
+
if (format === 'json') {
|
|
193
|
+
this.log(JSON.stringify(result));
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// Text format
|
|
197
|
+
this.log('\n=== Shard Assignments (Test-Level) ===\n');
|
|
198
|
+
for (const [shard, tests] of Object.entries(result.shards)) {
|
|
199
|
+
const duration = result.expectedDurations[Number(shard)];
|
|
200
|
+
const durationStr = this.formatDuration(duration ?? 0);
|
|
201
|
+
this.log(`Shard ${shard} (${durationStr}, ${tests.length} tests):`);
|
|
202
|
+
if (verbose) {
|
|
203
|
+
for (const testId of tests) {
|
|
204
|
+
const isEstimated = result.estimatedTests.includes(testId);
|
|
205
|
+
this.log(` - ${testId}${isEstimated ? ' (estimated)' : ''}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const grepPattern = result.grepPatterns[Number(shard)];
|
|
209
|
+
if (grepPattern && grepPattern.length < 100) {
|
|
210
|
+
this.log(` grep: "${grepPattern}"`);
|
|
211
|
+
}
|
|
212
|
+
else if (grepPattern) {
|
|
213
|
+
this.log(` grep: (${grepPattern.length} chars, use --grep-file)`);
|
|
214
|
+
}
|
|
215
|
+
this.log('');
|
|
216
|
+
}
|
|
217
|
+
this.log(`Total tests: ${result.totalTests}`);
|
|
218
|
+
this.log(`Optimal solution: ${result.isOptimal ? 'Yes' : 'No (LPT fallback)'}`);
|
|
219
|
+
if (result.estimatedTests.length > 0) {
|
|
220
|
+
this.log(`Tests with estimated duration: ${result.estimatedTests.length}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
outputResult(result, format, _verbose = false) {
|
|
225
|
+
if (format === 'json') {
|
|
226
|
+
this.log(JSON.stringify(result));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// Text format
|
|
230
|
+
this.log('\n=== Shard Assignments (File-Level) ===\n');
|
|
231
|
+
for (const [shard, files] of Object.entries(result.shards)) {
|
|
232
|
+
const duration = result.expectedDurations[Number(shard)];
|
|
233
|
+
const durationStr = this.formatDuration(duration ?? 0);
|
|
234
|
+
this.log(`Shard ${shard} (${durationStr}):`);
|
|
235
|
+
for (const file of files) {
|
|
236
|
+
const isEstimated = result.estimatedFiles.includes(file);
|
|
237
|
+
this.log(` - ${file}${isEstimated ? ' (estimated)' : ''}`);
|
|
238
|
+
}
|
|
239
|
+
this.log('');
|
|
240
|
+
}
|
|
241
|
+
this.log(`Total files: ${result.totalFiles}`);
|
|
242
|
+
if (result.estimatedFiles.length > 0) {
|
|
243
|
+
this.log(`Files with estimated duration: ${result.estimatedFiles.length}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
formatDuration(ms) {
|
|
248
|
+
const seconds = Math.round(ms / 1000);
|
|
249
|
+
const minutes = Math.floor(seconds / 60);
|
|
250
|
+
const remainingSeconds = seconds % 60;
|
|
251
|
+
return minutes > 0
|
|
252
|
+
? `${minutes}m ${remainingSeconds}s`
|
|
253
|
+
: `${remainingSeconds}s`;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=assign.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assign.js","sourceRoot":"","sources":["../../src/commands/assign.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class ExtractTiming extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
'report-file': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
'output-file': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
shard: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
project: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
level: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Extract test-level durations from Playwright report
|
|
16
|
+
*
|
|
17
|
+
* Each test is identified by: file::describe::testTitle
|
|
18
|
+
*/
|
|
19
|
+
private extractTestDurations;
|
|
20
|
+
/**
|
|
21
|
+
* Recursively extract test durations from a suite
|
|
22
|
+
*/
|
|
23
|
+
private extractTestsFromSuite;
|
|
24
|
+
/**
|
|
25
|
+
* Extract file durations from Playwright report (legacy file-level)
|
|
26
|
+
*
|
|
27
|
+
* The report structure has nested suites where the top-level suite
|
|
28
|
+
* represents the file. We sum up all test durations within each file.
|
|
29
|
+
*/
|
|
30
|
+
private extractFileDurations;
|
|
31
|
+
/**
|
|
32
|
+
* Normalize file path to be relative and consistent
|
|
33
|
+
*/
|
|
34
|
+
private normalizeFilePath;
|
|
35
|
+
/**
|
|
36
|
+
* Calculate total duration for a suite (recursively including nested suites)
|
|
37
|
+
*/
|
|
38
|
+
private calculateSuiteDuration;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=extract-timing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract-timing.d.ts","sourceRoot":"","sources":["../../src/commands/extract-timing.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAS,MAAM,aAAa,CAAC;AAQ7C,MAAM,CAAC,OAAO,OAAO,aAAc,SAAQ,OAAO;IAChD,OAAgB,WAAW,SACoD;IAE/E,OAAgB,QAAQ,WAGtB;IAEF,OAAgB,KAAK;;;;;;;MA+BnB;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAiE1B;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAY5B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA8C7B;;;;;OAKG;IACH,OAAO,CAAC,oBAAoB;IAc5B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAkBzB;;OAEG;IACH,OAAO,CAAC,sBAAsB;CAuB/B"}
|