@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,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default timeout for CKK algorithm in milliseconds
|
|
3
|
+
*/
|
|
4
|
+
export const DEFAULT_CKK_TIMEOUT = 500;
|
|
5
|
+
/**
|
|
6
|
+
* Complete Karmarkar-Karp (CKK) inspired algorithm for multi-way number partitioning
|
|
7
|
+
*
|
|
8
|
+
* This algorithm finds the optimal distribution of tests across shards by:
|
|
9
|
+
* 1. Using branch and bound search with aggressive pruning
|
|
10
|
+
* 2. Starting with LPT solution as upper bound
|
|
11
|
+
* 3. Exploring assignment tree, pruning when partial makespan exceeds best
|
|
12
|
+
*
|
|
13
|
+
* Falls back to LPT if search exceeds timeout.
|
|
14
|
+
*
|
|
15
|
+
* @param tests - Tests with their durations
|
|
16
|
+
* @param numShards - Number of shards to distribute across
|
|
17
|
+
* @param timeoutMs - Maximum time to search for optimal solution
|
|
18
|
+
* @returns Optimal (or near-optimal) shard assignments
|
|
19
|
+
*/
|
|
20
|
+
export function assignWithCKK(tests, numShards, timeoutMs = DEFAULT_CKK_TIMEOUT) {
|
|
21
|
+
if (tests.length === 0) {
|
|
22
|
+
return {
|
|
23
|
+
assignments: createEmptyAssignments(numShards),
|
|
24
|
+
makespan: 0,
|
|
25
|
+
isOptimal: true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (numShards <= 0) {
|
|
29
|
+
throw new Error('Number of shards must be positive');
|
|
30
|
+
}
|
|
31
|
+
if (numShards >= tests.length) {
|
|
32
|
+
// More shards than tests - each test gets its own shard
|
|
33
|
+
return assignOnePerShard(tests, numShards);
|
|
34
|
+
}
|
|
35
|
+
// Sort tests by duration descending for better pruning
|
|
36
|
+
const sortedTests = [...tests].sort((a, b) => b.duration - a.duration);
|
|
37
|
+
// Get LPT solution as upper bound
|
|
38
|
+
const lptResult = assignWithLPTInternal(sortedTests, numShards);
|
|
39
|
+
let bestMakespan = lptResult.makespan;
|
|
40
|
+
let bestAssignment = lptResult.assignments;
|
|
41
|
+
let isOptimal = false;
|
|
42
|
+
// For small inputs, try to find optimal solution
|
|
43
|
+
const startTime = Date.now();
|
|
44
|
+
// Branch and bound search
|
|
45
|
+
const shardLoads = new Array(numShards).fill(0);
|
|
46
|
+
const shardTests = Array.from({ length: numShards }, () => []);
|
|
47
|
+
function search(testIndex) {
|
|
48
|
+
// Check timeout
|
|
49
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
50
|
+
return false; // Timeout, stop search
|
|
51
|
+
}
|
|
52
|
+
// All tests assigned
|
|
53
|
+
if (testIndex >= sortedTests.length) {
|
|
54
|
+
const currentMakespan = Math.max(...shardLoads);
|
|
55
|
+
if (currentMakespan < bestMakespan) {
|
|
56
|
+
bestMakespan = currentMakespan;
|
|
57
|
+
bestAssignment = shardLoads.map((load, i) => ({
|
|
58
|
+
shardIndex: i + 1,
|
|
59
|
+
tests: [...(shardTests[i] ?? [])],
|
|
60
|
+
expectedDuration: load,
|
|
61
|
+
}));
|
|
62
|
+
isOptimal = true;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
const test = sortedTests[testIndex];
|
|
67
|
+
if (!test)
|
|
68
|
+
return true;
|
|
69
|
+
// Calculate lower bound: current max + remaining items distributed perfectly
|
|
70
|
+
const remainingDuration = sortedTests
|
|
71
|
+
.slice(testIndex)
|
|
72
|
+
.reduce((sum, t) => sum + t.duration, 0);
|
|
73
|
+
const currentMax = Math.max(...shardLoads);
|
|
74
|
+
const totalAfter = shardLoads.reduce((sum, l) => sum + l, 0) + remainingDuration;
|
|
75
|
+
const lowerBound = Math.max(currentMax, Math.ceil(totalAfter / numShards));
|
|
76
|
+
// Prune if lower bound exceeds best
|
|
77
|
+
if (lowerBound >= bestMakespan) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
// Try assigning to each shard, starting with least loaded
|
|
81
|
+
const shardOrder = shardLoads
|
|
82
|
+
.map((load, i) => ({ load, index: i }))
|
|
83
|
+
.sort((a, b) => a.load - b.load)
|
|
84
|
+
.map((s) => s.index);
|
|
85
|
+
// Skip duplicate loads to avoid redundant exploration
|
|
86
|
+
const seenLoads = new Set();
|
|
87
|
+
for (const shardIdx of shardOrder) {
|
|
88
|
+
const load = shardLoads[shardIdx];
|
|
89
|
+
if (load === undefined)
|
|
90
|
+
continue;
|
|
91
|
+
// Skip if we've already tried a shard with this load
|
|
92
|
+
if (seenLoads.has(load)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
seenLoads.add(load);
|
|
96
|
+
// Prune: if adding to this shard exceeds best makespan, skip
|
|
97
|
+
if (load + test.duration >= bestMakespan) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// Assign test to shard
|
|
101
|
+
shardLoads[shardIdx] = load + test.duration;
|
|
102
|
+
shardTests[shardIdx]?.push(test.testId);
|
|
103
|
+
const completed = search(testIndex + 1);
|
|
104
|
+
if (!completed) {
|
|
105
|
+
// Timeout - restore and return
|
|
106
|
+
shardLoads[shardIdx] = load;
|
|
107
|
+
shardTests[shardIdx]?.pop();
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
// Restore state for backtracking
|
|
111
|
+
shardLoads[shardIdx] = load;
|
|
112
|
+
shardTests[shardIdx]?.pop();
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
// Only run search for reasonable input sizes
|
|
117
|
+
if (tests.length <= 50) {
|
|
118
|
+
search(0);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
assignments: bestAssignment,
|
|
122
|
+
makespan: bestMakespan,
|
|
123
|
+
isOptimal,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Simple LPT algorithm for internal use
|
|
128
|
+
*/
|
|
129
|
+
function assignWithLPTInternal(sortedTests, numShards) {
|
|
130
|
+
const shards = Array.from({ length: numShards }, (_, i) => ({
|
|
131
|
+
shardIndex: i + 1,
|
|
132
|
+
tests: [],
|
|
133
|
+
expectedDuration: 0,
|
|
134
|
+
}));
|
|
135
|
+
for (const test of sortedTests) {
|
|
136
|
+
// Find shard with minimum load
|
|
137
|
+
let minShard = shards[0];
|
|
138
|
+
for (const shard of shards) {
|
|
139
|
+
if (minShard && shard.expectedDuration < minShard.expectedDuration) {
|
|
140
|
+
minShard = shard;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (minShard) {
|
|
144
|
+
minShard.tests.push(test.testId);
|
|
145
|
+
minShard.expectedDuration += test.duration;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const makespan = Math.max(...shards.map((s) => s.expectedDuration));
|
|
149
|
+
return { assignments: shards, makespan };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Create empty shard assignments
|
|
153
|
+
*/
|
|
154
|
+
function createEmptyAssignments(numShards) {
|
|
155
|
+
return Array.from({ length: numShards }, (_, i) => ({
|
|
156
|
+
shardIndex: i + 1,
|
|
157
|
+
tests: [],
|
|
158
|
+
expectedDuration: 0,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Assign one test per shard when there are more shards than tests
|
|
163
|
+
*/
|
|
164
|
+
function assignOnePerShard(tests, numShards) {
|
|
165
|
+
const assignments = createEmptyAssignments(numShards);
|
|
166
|
+
tests.forEach((test, i) => {
|
|
167
|
+
const assignment = assignments[i];
|
|
168
|
+
if (assignment) {
|
|
169
|
+
assignment.tests.push(test.testId);
|
|
170
|
+
assignment.expectedDuration = test.duration;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
const makespan = tests.length > 0 ? Math.max(...tests.map((t) => t.duration)) : 0;
|
|
174
|
+
return {
|
|
175
|
+
assignments,
|
|
176
|
+
makespan,
|
|
177
|
+
isOptimal: true,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Calculate theoretical lower bound for makespan
|
|
182
|
+
* This is the best possible makespan if we could partition perfectly
|
|
183
|
+
*/
|
|
184
|
+
export function calculateLowerBound(tests, numShards) {
|
|
185
|
+
if (tests.length === 0)
|
|
186
|
+
return 0;
|
|
187
|
+
const totalDuration = tests.reduce((sum, t) => sum + t.duration, 0);
|
|
188
|
+
const maxSingleTest = Math.max(...tests.map((t) => t.duration));
|
|
189
|
+
// Lower bound is max of: largest single item OR total/shards
|
|
190
|
+
return Math.max(maxSingleTest, Math.ceil(totalDuration / numShards));
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=ckk-algorithm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ckk-algorithm.js","sourceRoot":"","sources":["../../src/core/ckk-algorithm.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { DiscoveredTest, TimingDataV2 } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Default milliseconds per line for duration estimation
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_MS_PER_LINE = 100;
|
|
6
|
+
/**
|
|
7
|
+
* Default duration for a test when no estimation is possible (30 seconds)
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEFAULT_TEST_DURATION = 30000;
|
|
10
|
+
/**
|
|
11
|
+
* Count the number of lines in a file
|
|
12
|
+
*/
|
|
13
|
+
export declare function countLines(filePath: string): number;
|
|
14
|
+
/**
|
|
15
|
+
* Estimate the duration of a test file based on its line count
|
|
16
|
+
*
|
|
17
|
+
* @param filePath - Path to the test file
|
|
18
|
+
* @param msPerLine - Milliseconds per line (default: 100)
|
|
19
|
+
* @returns Estimated duration in milliseconds
|
|
20
|
+
*/
|
|
21
|
+
export declare function estimateDuration(filePath: string, msPerLine?: number): number;
|
|
22
|
+
/**
|
|
23
|
+
* Estimate durations for multiple files
|
|
24
|
+
*
|
|
25
|
+
* @param testDir - Base test directory
|
|
26
|
+
* @param files - List of file paths (relative to testDir)
|
|
27
|
+
* @param msPerLine - Milliseconds per line
|
|
28
|
+
* @returns Map of file paths to estimated durations
|
|
29
|
+
*/
|
|
30
|
+
export declare function estimateDurations(testDir: string, files: string[], msPerLine?: number): Map<string, number>;
|
|
31
|
+
/**
|
|
32
|
+
* Estimate the duration of a single test using fallback strategy:
|
|
33
|
+
* 1. Same-file average: Use average duration of known tests in the same file
|
|
34
|
+
* 2. Global average: Use average duration across all known tests
|
|
35
|
+
* 3. Default constant: Use DEFAULT_TEST_DURATION (30 seconds)
|
|
36
|
+
*
|
|
37
|
+
* @param testId - Test ID to estimate
|
|
38
|
+
* @param timingData - Existing timing data (v2)
|
|
39
|
+
* @returns Estimated duration in milliseconds
|
|
40
|
+
*/
|
|
41
|
+
export declare function estimateTestDuration(testId: string, timingData: TimingDataV2 | null): number;
|
|
42
|
+
/**
|
|
43
|
+
* Get durations for a list of discovered tests
|
|
44
|
+
*
|
|
45
|
+
* Uses timing data when available, falls back to estimation
|
|
46
|
+
*
|
|
47
|
+
* @param tests - List of discovered tests
|
|
48
|
+
* @param timingData - Existing timing data (v2)
|
|
49
|
+
* @returns Array of tests with their durations and estimation flag
|
|
50
|
+
*/
|
|
51
|
+
export declare function getTestDurations(tests: DiscoveredTest[], timingData: TimingDataV2 | null): Array<{
|
|
52
|
+
testId: string;
|
|
53
|
+
file: string;
|
|
54
|
+
duration: number;
|
|
55
|
+
estimated: boolean;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Calculate average test duration from timing data
|
|
59
|
+
*
|
|
60
|
+
* @param timingData - Timing data (v2)
|
|
61
|
+
* @returns Average duration in milliseconds, or DEFAULT_TEST_DURATION if no data
|
|
62
|
+
*/
|
|
63
|
+
export declare function calculateAverageTestDuration(timingData: TimingDataV2 | null): number;
|
|
64
|
+
/**
|
|
65
|
+
* Calculate average test duration for tests in a specific file
|
|
66
|
+
*
|
|
67
|
+
* @param file - File name
|
|
68
|
+
* @param timingData - Timing data (v2)
|
|
69
|
+
* @returns Average duration in milliseconds, or null if no tests found for file
|
|
70
|
+
*/
|
|
71
|
+
export declare function calculateFileAverageTestDuration(file: string, timingData: TimingDataV2 | null): number | null;
|
|
72
|
+
//# sourceMappingURL=estimate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"estimate.d.ts","sourceRoot":"","sources":["../../src/core/estimate.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/D;;GAEG;AACH,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC;;GAEG;AACH,eAAO,MAAM,qBAAqB,QAAQ,CAAC;AAE3C;;GAEG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQnD;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,MAA4B,GACtC,MAAM,CAGR;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EAAE,EACf,SAAS,GAAE,MAA4B,GACtC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CASrB;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,YAAY,GAAG,IAAI,GAC9B,MAAM,CA0BR;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,EAAE,EACvB,UAAU,EAAE,YAAY,GAAG,IAAI,GAC9B,KAAK,CAAC;IACP,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC,CAoBD;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE,YAAY,GAAG,IAAI,GAC9B,MAAM,CAQR;AAED;;;;;;GAMG;AACH,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,YAAY,GAAG,IAAI,GAC9B,MAAM,GAAG,IAAI,CAef"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { parseTestId } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Default milliseconds per line for duration estimation
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_MS_PER_LINE = 100;
|
|
8
|
+
/**
|
|
9
|
+
* Default duration for a test when no estimation is possible (30 seconds)
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_TEST_DURATION = 30000;
|
|
12
|
+
/**
|
|
13
|
+
* Count the number of lines in a file
|
|
14
|
+
*/
|
|
15
|
+
export function countLines(filePath) {
|
|
16
|
+
try {
|
|
17
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
18
|
+
return content.split('\n').length;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// If file can't be read, return a reasonable default
|
|
22
|
+
return 50;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Estimate the duration of a test file based on its line count
|
|
27
|
+
*
|
|
28
|
+
* @param filePath - Path to the test file
|
|
29
|
+
* @param msPerLine - Milliseconds per line (default: 100)
|
|
30
|
+
* @returns Estimated duration in milliseconds
|
|
31
|
+
*/
|
|
32
|
+
export function estimateDuration(filePath, msPerLine = DEFAULT_MS_PER_LINE) {
|
|
33
|
+
const lines = countLines(filePath);
|
|
34
|
+
return lines * msPerLine;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Estimate durations for multiple files
|
|
38
|
+
*
|
|
39
|
+
* @param testDir - Base test directory
|
|
40
|
+
* @param files - List of file paths (relative to testDir)
|
|
41
|
+
* @param msPerLine - Milliseconds per line
|
|
42
|
+
* @returns Map of file paths to estimated durations
|
|
43
|
+
*/
|
|
44
|
+
export function estimateDurations(testDir, files, msPerLine = DEFAULT_MS_PER_LINE) {
|
|
45
|
+
const estimates = new Map();
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
const fullPath = path.join(testDir, file);
|
|
48
|
+
estimates.set(file, estimateDuration(fullPath, msPerLine));
|
|
49
|
+
}
|
|
50
|
+
return estimates;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Estimate the duration of a single test using fallback strategy:
|
|
54
|
+
* 1. Same-file average: Use average duration of known tests in the same file
|
|
55
|
+
* 2. Global average: Use average duration across all known tests
|
|
56
|
+
* 3. Default constant: Use DEFAULT_TEST_DURATION (30 seconds)
|
|
57
|
+
*
|
|
58
|
+
* @param testId - Test ID to estimate
|
|
59
|
+
* @param timingData - Existing timing data (v2)
|
|
60
|
+
* @returns Estimated duration in milliseconds
|
|
61
|
+
*/
|
|
62
|
+
export function estimateTestDuration(testId, timingData) {
|
|
63
|
+
if (!timingData || Object.keys(timingData.tests).length === 0) {
|
|
64
|
+
return DEFAULT_TEST_DURATION;
|
|
65
|
+
}
|
|
66
|
+
const { file } = parseTestId(testId);
|
|
67
|
+
// Strategy 1: Same-file average
|
|
68
|
+
const sameFileTests = Object.entries(timingData.tests).filter(([, data]) => data.file === file);
|
|
69
|
+
if (sameFileTests.length > 0) {
|
|
70
|
+
const sum = sameFileTests.reduce((acc, [, data]) => acc + data.duration, 0);
|
|
71
|
+
return Math.round(sum / sameFileTests.length);
|
|
72
|
+
}
|
|
73
|
+
// Strategy 2: Global average
|
|
74
|
+
const allTests = Object.values(timingData.tests);
|
|
75
|
+
if (allTests.length > 0) {
|
|
76
|
+
const sum = allTests.reduce((acc, data) => acc + data.duration, 0);
|
|
77
|
+
return Math.round(sum / allTests.length);
|
|
78
|
+
}
|
|
79
|
+
// Strategy 3: Default constant
|
|
80
|
+
return DEFAULT_TEST_DURATION;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get durations for a list of discovered tests
|
|
84
|
+
*
|
|
85
|
+
* Uses timing data when available, falls back to estimation
|
|
86
|
+
*
|
|
87
|
+
* @param tests - List of discovered tests
|
|
88
|
+
* @param timingData - Existing timing data (v2)
|
|
89
|
+
* @returns Array of tests with their durations and estimation flag
|
|
90
|
+
*/
|
|
91
|
+
export function getTestDurations(tests, timingData) {
|
|
92
|
+
return tests.map((test) => {
|
|
93
|
+
const existing = timingData?.tests[test.testId];
|
|
94
|
+
if (existing) {
|
|
95
|
+
return {
|
|
96
|
+
testId: test.testId,
|
|
97
|
+
file: test.file,
|
|
98
|
+
duration: existing.duration,
|
|
99
|
+
estimated: false,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
testId: test.testId,
|
|
104
|
+
file: test.file,
|
|
105
|
+
duration: estimateTestDuration(test.testId, timingData),
|
|
106
|
+
estimated: true,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Calculate average test duration from timing data
|
|
112
|
+
*
|
|
113
|
+
* @param timingData - Timing data (v2)
|
|
114
|
+
* @returns Average duration in milliseconds, or DEFAULT_TEST_DURATION if no data
|
|
115
|
+
*/
|
|
116
|
+
export function calculateAverageTestDuration(timingData) {
|
|
117
|
+
if (!timingData || Object.keys(timingData.tests).length === 0) {
|
|
118
|
+
return DEFAULT_TEST_DURATION;
|
|
119
|
+
}
|
|
120
|
+
const durations = Object.values(timingData.tests).map((t) => t.duration);
|
|
121
|
+
const sum = durations.reduce((acc, d) => acc + d, 0);
|
|
122
|
+
return Math.round(sum / durations.length);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Calculate average test duration for tests in a specific file
|
|
126
|
+
*
|
|
127
|
+
* @param file - File name
|
|
128
|
+
* @param timingData - Timing data (v2)
|
|
129
|
+
* @returns Average duration in milliseconds, or null if no tests found for file
|
|
130
|
+
*/
|
|
131
|
+
export function calculateFileAverageTestDuration(file, timingData) {
|
|
132
|
+
if (!timingData) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const fileTests = Object.values(timingData.tests).filter((t) => t.file === file);
|
|
136
|
+
if (fileTests.length === 0) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const sum = fileTests.reduce((acc, t) => acc + t.duration, 0);
|
|
140
|
+
return Math.round(sum / fileTests.length);
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=estimate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"estimate.js","sourceRoot":"","sources":["../../src/core/estimate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maximum length for a grep pattern before switching to grep-file
|
|
3
|
+
*/
|
|
4
|
+
export declare const MAX_GREP_PATTERN_LENGTH = 4000;
|
|
5
|
+
/**
|
|
6
|
+
* Escape regex special characters in a string
|
|
7
|
+
*
|
|
8
|
+
* @param str - String to escape
|
|
9
|
+
* @returns Escaped string safe for use in regex
|
|
10
|
+
*/
|
|
11
|
+
export declare function escapeRegex(str: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Extract the test title from a test ID
|
|
14
|
+
* The title is the last part of the titlePath
|
|
15
|
+
*
|
|
16
|
+
* @param testId - Test ID in format file::describe::testTitle
|
|
17
|
+
* @returns The test title
|
|
18
|
+
*/
|
|
19
|
+
export declare function extractTitleFromTestId(testId: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Generate a grep pattern from a list of test IDs
|
|
22
|
+
*
|
|
23
|
+
* Uses the test title (last part of titlePath) for matching.
|
|
24
|
+
* Escapes regex special characters to ensure exact matching.
|
|
25
|
+
*
|
|
26
|
+
* @param testIds - List of test IDs to include in pattern
|
|
27
|
+
* @returns Grep pattern string that matches any of the tests
|
|
28
|
+
*/
|
|
29
|
+
export declare function generateGrepPattern(testIds: string[]): string;
|
|
30
|
+
/**
|
|
31
|
+
* Generate grep patterns for multiple shards
|
|
32
|
+
*
|
|
33
|
+
* @param shardTests - Map of shard index to list of test IDs
|
|
34
|
+
* @returns Map of shard index to grep pattern
|
|
35
|
+
*/
|
|
36
|
+
export declare function generateGrepPatterns(shardTests: Record<number, string[]>): Record<number, string>;
|
|
37
|
+
/**
|
|
38
|
+
* Check if a grep pattern is too long and should use --grep-file instead
|
|
39
|
+
*
|
|
40
|
+
* @param pattern - Grep pattern to check
|
|
41
|
+
* @returns True if pattern exceeds maximum length
|
|
42
|
+
*/
|
|
43
|
+
export declare function isPatternTooLong(pattern: string): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Generate content for a grep-file (one pattern per line)
|
|
46
|
+
*
|
|
47
|
+
* @param testIds - List of test IDs
|
|
48
|
+
* @returns File content with one escaped title per line
|
|
49
|
+
*/
|
|
50
|
+
export declare function generateGrepFileContent(testIds: string[]): string;
|
|
51
|
+
/**
|
|
52
|
+
* Determine the best grep strategy for a list of tests
|
|
53
|
+
*
|
|
54
|
+
* @param testIds - List of test IDs
|
|
55
|
+
* @returns Object with strategy ('pattern' or 'file') and content
|
|
56
|
+
*/
|
|
57
|
+
export declare function determineGrepStrategy(testIds: string[]): {
|
|
58
|
+
strategy: 'pattern' | 'file';
|
|
59
|
+
content: string;
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=grep-pattern.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grep-pattern.d.ts","sourceRoot":"","sources":["../../src/core/grep-pattern.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAY7D;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GACnC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAOjE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG;IACxD,QAAQ,EAAE,SAAS,GAAG,MAAM,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;CACjB,CAeA"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { parseTestId } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Regex special characters that need escaping in grep patterns
|
|
4
|
+
*/
|
|
5
|
+
const REGEX_SPECIAL_CHARS = /[.*+?^${}()|[\]\\]/g;
|
|
6
|
+
/**
|
|
7
|
+
* Maximum length for a grep pattern before switching to grep-file
|
|
8
|
+
*/
|
|
9
|
+
export const MAX_GREP_PATTERN_LENGTH = 4000;
|
|
10
|
+
/**
|
|
11
|
+
* Escape regex special characters in a string
|
|
12
|
+
*
|
|
13
|
+
* @param str - String to escape
|
|
14
|
+
* @returns Escaped string safe for use in regex
|
|
15
|
+
*/
|
|
16
|
+
export function escapeRegex(str) {
|
|
17
|
+
return str.replace(REGEX_SPECIAL_CHARS, '\\$&');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Extract the test title from a test ID
|
|
21
|
+
* The title is the last part of the titlePath
|
|
22
|
+
*
|
|
23
|
+
* @param testId - Test ID in format file::describe::testTitle
|
|
24
|
+
* @returns The test title
|
|
25
|
+
*/
|
|
26
|
+
export function extractTitleFromTestId(testId) {
|
|
27
|
+
const { titlePath } = parseTestId(testId);
|
|
28
|
+
return titlePath[titlePath.length - 1] || testId;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Generate a grep pattern from a list of test IDs
|
|
32
|
+
*
|
|
33
|
+
* Uses the test title (last part of titlePath) for matching.
|
|
34
|
+
* Escapes regex special characters to ensure exact matching.
|
|
35
|
+
*
|
|
36
|
+
* @param testIds - List of test IDs to include in pattern
|
|
37
|
+
* @returns Grep pattern string that matches any of the tests
|
|
38
|
+
*/
|
|
39
|
+
export function generateGrepPattern(testIds) {
|
|
40
|
+
if (testIds.length === 0) {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
const titles = testIds.map((id) => {
|
|
44
|
+
const title = extractTitleFromTestId(id);
|
|
45
|
+
return escapeRegex(title);
|
|
46
|
+
});
|
|
47
|
+
// Use OR operator to match any of the titles
|
|
48
|
+
return titles.join('|');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Generate grep patterns for multiple shards
|
|
52
|
+
*
|
|
53
|
+
* @param shardTests - Map of shard index to list of test IDs
|
|
54
|
+
* @returns Map of shard index to grep pattern
|
|
55
|
+
*/
|
|
56
|
+
export function generateGrepPatterns(shardTests) {
|
|
57
|
+
const patterns = {};
|
|
58
|
+
for (const [shardIndex, testIds] of Object.entries(shardTests)) {
|
|
59
|
+
patterns[Number(shardIndex)] = generateGrepPattern(testIds);
|
|
60
|
+
}
|
|
61
|
+
return patterns;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Check if a grep pattern is too long and should use --grep-file instead
|
|
65
|
+
*
|
|
66
|
+
* @param pattern - Grep pattern to check
|
|
67
|
+
* @returns True if pattern exceeds maximum length
|
|
68
|
+
*/
|
|
69
|
+
export function isPatternTooLong(pattern) {
|
|
70
|
+
return pattern.length > MAX_GREP_PATTERN_LENGTH;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Generate content for a grep-file (one pattern per line)
|
|
74
|
+
*
|
|
75
|
+
* @param testIds - List of test IDs
|
|
76
|
+
* @returns File content with one escaped title per line
|
|
77
|
+
*/
|
|
78
|
+
export function generateGrepFileContent(testIds) {
|
|
79
|
+
const titles = testIds.map((id) => {
|
|
80
|
+
const title = extractTitleFromTestId(id);
|
|
81
|
+
return escapeRegex(title);
|
|
82
|
+
});
|
|
83
|
+
return titles.join('\n');
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Determine the best grep strategy for a list of tests
|
|
87
|
+
*
|
|
88
|
+
* @param testIds - List of test IDs
|
|
89
|
+
* @returns Object with strategy ('pattern' or 'file') and content
|
|
90
|
+
*/
|
|
91
|
+
export function determineGrepStrategy(testIds) {
|
|
92
|
+
if (testIds.length === 0) {
|
|
93
|
+
return { strategy: 'pattern', content: '' };
|
|
94
|
+
}
|
|
95
|
+
const pattern = generateGrepPattern(testIds);
|
|
96
|
+
if (isPatternTooLong(pattern)) {
|
|
97
|
+
return {
|
|
98
|
+
strategy: 'file',
|
|
99
|
+
content: generateGrepFileContent(testIds),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return { strategy: 'pattern', content: pattern };
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=grep-pattern.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grep-pattern.js","sourceRoot":"","sources":["../../src/core/grep-pattern.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC;;GAEG;AACH,MAAM,mBAAmB,GAAG,qBAAqB,CAAC;AAElD;;GAEG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,OAAO,GAAG,CAAC,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;AAClD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAc;IACnD,MAAM,EAAE,SAAS,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1C,OAAO,SAAS,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,MAAM,CAAC;AACnD,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAiB;IACnD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QAChC,MAAM,KAAK,GAAG,sBAAsB,CAAC,EAAE,CAAC,CAAC;QACzC,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,6CAA6C;IAC7C,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAClC,UAAoC;IAEpC,MAAM,QAAQ,GAA2B,EAAE,CAAC;IAE5C,KAAK,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/D,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC9D,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,OAAO,OAAO,CAAC,MAAM,GAAG,uBAAuB,CAAC;AAClD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAiB;IACvD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QAChC,MAAM,KAAK,GAAG,sBAAsB,CAAC,EAAE,CAAC,CAAC;QACzC,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAiB;IAIrD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAC9C,CAAC;IAED,MAAM,OAAO,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAE7C,IAAI,gBAAgB,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9B,OAAO;YACL,QAAQ,EAAE,MAAM;YAChB,OAAO,EAAE,uBAAuB,CAAC,OAAO,CAAC;SAC1C,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AACnD,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './ckk-algorithm.js';
|
|
2
|
+
export * from './estimate.js';
|
|
3
|
+
export * from './grep-pattern.js';
|
|
4
|
+
export * from './lpt-algorithm.js';
|
|
5
|
+
export * from './slugify.js';
|
|
6
|
+
export * from './test-discovery.js';
|
|
7
|
+
export * from './timing-store.js';
|
|
8
|
+
export * from './types.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './ckk-algorithm.js';
|
|
2
|
+
export * from './estimate.js';
|
|
3
|
+
export * from './grep-pattern.js';
|
|
4
|
+
export * from './lpt-algorithm.js';
|
|
5
|
+
export * from './slugify.js';
|
|
6
|
+
export * from './test-discovery.js';
|
|
7
|
+
export * from './timing-store.js';
|
|
8
|
+
export * from './types.js';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AssignResult, FileWithDuration, ShardAssignment } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Longest Processing Time First (LPT) algorithm for load balancing
|
|
4
|
+
*
|
|
5
|
+
* This greedy algorithm assigns jobs to workers by:
|
|
6
|
+
* 1. Sorting jobs by duration (descending)
|
|
7
|
+
* 2. Assigning each job to the worker with the smallest current load
|
|
8
|
+
*
|
|
9
|
+
* Time complexity: O(n log n) for sorting + O(n log k) for assignment = O(n log n)
|
|
10
|
+
* where n = number of files, k = number of shards
|
|
11
|
+
*
|
|
12
|
+
* @param files - Files with their durations
|
|
13
|
+
* @param numShards - Number of shards to distribute across
|
|
14
|
+
* @returns Shard assignments with expected durations
|
|
15
|
+
*/
|
|
16
|
+
export declare function assignWithLPT(files: FileWithDuration[], numShards: number): ShardAssignment[];
|
|
17
|
+
/**
|
|
18
|
+
* Convert shard assignments to the format expected by the CLI output
|
|
19
|
+
*/
|
|
20
|
+
export declare function formatAssignResult(assignments: ShardAssignment[], estimatedFiles: string[]): AssignResult;
|
|
21
|
+
/**
|
|
22
|
+
* Calculate the balance metric (max/min ratio) for the assignment
|
|
23
|
+
*
|
|
24
|
+
* A perfectly balanced assignment would have a ratio of 1.0
|
|
25
|
+
* The target is to keep this below 1.2 (20% difference)
|
|
26
|
+
*/
|
|
27
|
+
export declare function calculateBalanceRatio(assignments: ShardAssignment[]): number;
|
|
28
|
+
//# sourceMappingURL=lpt-algorithm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lpt-algorithm.d.ts","sourceRoot":"","sources":["../../src/core/lpt-algorithm.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAChB,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,gBAAgB,EAAE,EACzB,SAAS,EAAE,MAAM,GAChB,eAAe,EAAE,CAoCnB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,eAAe,EAAE,EAC9B,cAAc,EAAE,MAAM,EAAE,GACvB,YAAY,CAiBd;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,eAAe,EAAE,GAAG,MAAM,CAa5E"}
|