Skip to content

Commit 786bf05

Browse files
authored
perf(core): optimize updateTestModes traversal (#1202)
1 parent 75ce306 commit 786bf05

2 files changed

Lines changed: 167 additions & 45 deletions

File tree

packages/core/src/runtime/runner/task.ts

Lines changed: 105 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -27,97 +27,152 @@ export const getTestStatus = (
2727
: 'pass';
2828
};
2929

30-
function hasOnlyTest(test: Test[]): boolean {
31-
return test.some((t) => {
32-
return t.runMode === 'only' || (t.type === 'suite' && hasOnlyTest(t.tests));
33-
});
34-
}
30+
type TestModeContext = {
31+
shouldSkipByName?: (test: TestCase) => boolean;
32+
suiteHasOnlyDescendants: WeakMap<TestSuite, boolean>;
33+
};
34+
35+
const collectOnlyTests = (
36+
tests: Test[],
37+
suiteHasOnlyDescendants: WeakMap<TestSuite, boolean>,
38+
): boolean => {
39+
let hasOnly = false;
40+
41+
for (const test of tests) {
42+
const childrenHaveOnly =
43+
test.type === 'suite'
44+
? collectOnlyTests(test.tests, suiteHasOnlyDescendants)
45+
: false;
46+
47+
if (test.type === 'suite') {
48+
suiteHasOnlyDescendants.set(test, childrenHaveOnly);
49+
}
50+
51+
if (test.runMode === 'only' || childrenHaveOnly) {
52+
hasOnly = true;
53+
}
54+
}
55+
56+
return hasOnly;
57+
};
58+
59+
const createShouldSkipByName = (
60+
testNamePattern?: RegExp | string,
61+
): ((test: TestCase) => boolean) | undefined => {
62+
if (!testNamePattern) {
63+
return undefined;
64+
}
65+
66+
const regex =
67+
typeof testNamePattern === 'string'
68+
? new RegExp(testNamePattern)
69+
: testNamePattern;
70+
const delimiter = regex.toString().includes(TEST_DELIMITER)
71+
? TEST_DELIMITER
72+
: '';
73+
74+
return (test: TestCase) => {
75+
if (regex.global || regex.sticky) {
76+
regex.lastIndex = 0;
77+
}
78+
79+
return !regex.test(getTaskNameWithPrefix(test, delimiter));
80+
};
81+
};
3582

3683
const shouldTestSkip = (
3784
test: TestCase,
3885
runOnly: boolean,
39-
testNamePattern?: RegExp | string,
86+
shouldSkipByName?: (test: TestCase) => boolean,
4087
) => {
4188
if (runOnly && test.runMode !== 'only') {
4289
return true;
4390
}
4491

45-
const delimiter = testNamePattern?.toString().includes(TEST_DELIMITER)
46-
? TEST_DELIMITER
47-
: '';
48-
49-
if (
50-
testNamePattern &&
51-
!getTaskNameWithPrefix(test, delimiter).match(testNamePattern)
52-
) {
92+
if (shouldSkipByName?.(test)) {
5393
return true;
5494
}
5595

5696
return false;
5797
};
5898

59-
export const traverseUpdateTestRunMode = (
99+
const traverseUpdateTestRunModeWithContext = (
60100
testSuite: TestSuite,
61101
parentRunMode: TestRunMode,
62102
runOnly: boolean,
63-
testNamePattern?: RegExp | string,
103+
context: TestModeContext,
64104
): void => {
65105
if (testSuite.tests.length === 0) {
66106
return;
67107
}
68108

69-
if (
70-
runOnly &&
71-
testSuite.runMode !== 'only' &&
72-
!hasOnlyTest(testSuite.tests)
73-
) {
109+
const childrenHaveOnly =
110+
context.suiteHasOnlyDescendants.get(testSuite) ?? false;
111+
112+
if (runOnly && testSuite.runMode !== 'only' && !childrenHaveOnly) {
74113
testSuite.runMode = 'skip';
75114
} else if (['skip', 'todo'].includes(parentRunMode)) {
76115
testSuite.runMode = parentRunMode;
77116
}
78117

79-
const tests = testSuite.tests.map((test) => {
80-
const runSubOnly =
81-
runOnly && testSuite.runMode !== 'only'
82-
? runOnly
83-
: hasOnlyTest(testSuite.tests);
118+
const runSubOnly =
119+
runOnly && testSuite.runMode !== 'only' ? runOnly : childrenHaveOnly;
120+
let hasRunTest = false;
121+
let allTodoTest = true;
84122

123+
for (const test of testSuite.tests) {
85124
if (test.type === 'case') {
86125
if (['skip', 'todo'].includes(testSuite.runMode)) {
87126
test.runMode = testSuite.runMode;
88127
}
89-
if (shouldTestSkip(test, runSubOnly, testNamePattern)) {
128+
if (shouldTestSkip(test, runSubOnly, context.shouldSkipByName)) {
90129
test.runMode = 'skip';
91130
}
92-
return test;
131+
} else {
132+
traverseUpdateTestRunModeWithContext(
133+
test,
134+
testSuite.runMode,
135+
runSubOnly,
136+
context,
137+
);
93138
}
94-
traverseUpdateTestRunMode(
95-
test,
96-
testSuite.runMode,
97-
runSubOnly,
98-
testNamePattern,
99-
);
100-
return test;
101-
});
139+
140+
if (test.runMode === 'run' || test.runMode === 'only') {
141+
hasRunTest = true;
142+
}
143+
144+
if (test.runMode !== 'todo') {
145+
allTodoTest = false;
146+
}
147+
}
102148

103149
if (testSuite.runMode !== 'run') {
104150
return;
105151
}
106152

107-
const hasRunTest = tests.some(
108-
(test) => test.runMode === 'run' || test.runMode === 'only',
109-
);
110-
111153
if (hasRunTest) {
112154
testSuite.runMode = 'run';
113155
return;
114156
}
115157

116-
const allTodoTest = tests.every((test) => test.runMode === 'todo');
117-
118158
testSuite.runMode = allTodoTest ? 'todo' : 'skip';
119159
};
120160

161+
export const traverseUpdateTestRunMode = (
162+
testSuite: TestSuite,
163+
parentRunMode: TestRunMode,
164+
runOnly: boolean,
165+
testNamePattern?: RegExp | string,
166+
): void => {
167+
const suiteHasOnlyDescendants = new WeakMap<TestSuite, boolean>();
168+
collectOnlyTests([testSuite], suiteHasOnlyDescendants);
169+
170+
traverseUpdateTestRunModeWithContext(testSuite, parentRunMode, runOnly, {
171+
shouldSkipByName: createShouldSkipByName(testNamePattern),
172+
suiteHasOnlyDescendants,
173+
});
174+
};
175+
121176
/**
122177
* sets the runMode of the test based on the runMode of its parent suite
123178
* - if the parent suite is 'todo', set the test to 'todo'
@@ -136,12 +191,17 @@ export const updateTestModes = (
136191
tests: Test[],
137192
testNamePattern?: RegExp | string,
138193
): void => {
139-
const hasOnly = hasOnlyTest(tests);
194+
const suiteHasOnlyDescendants = new WeakMap<TestSuite, boolean>();
195+
const hasOnly = collectOnlyTests(tests, suiteHasOnlyDescendants);
196+
const shouldSkipByName = createShouldSkipByName(testNamePattern);
140197

141198
for (const test of tests) {
142199
if (test.type === 'suite') {
143-
traverseUpdateTestRunMode(test, 'run', hasOnly, testNamePattern);
144-
} else if (shouldTestSkip(test, hasOnly, testNamePattern)) {
200+
traverseUpdateTestRunModeWithContext(test, 'run', hasOnly, {
201+
shouldSkipByName,
202+
suiteHasOnlyDescendants,
203+
});
204+
} else if (shouldTestSkip(test, hasOnly, shouldSkipByName)) {
145205
test.runMode = 'skip';
146206
}
147207
}

packages/core/tests/runner/runner.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,4 +339,66 @@ describe('traverseUpdateTest', () => {
339339
]
340340
`);
341341
});
342+
343+
it('updateTestMode with string and global regex patterns', () => {
344+
const createTests = (): [TestSuite, TestCase] => [
345+
{
346+
name: 'testA',
347+
runMode: 'run',
348+
type: 'suite',
349+
tests: [
350+
{
351+
name: 'test-0',
352+
type: 'case',
353+
runMode: 'run',
354+
},
355+
{
356+
name: 'test-1',
357+
type: 'case',
358+
runMode: 'run',
359+
},
360+
{
361+
name: 'test-2',
362+
type: 'suite',
363+
runMode: 'run',
364+
tests: [
365+
{
366+
name: 'test-2-1',
367+
type: 'case',
368+
runMode: 'run',
369+
},
370+
{
371+
name: 'test-2-2',
372+
type: 'case',
373+
runMode: 'run',
374+
},
375+
],
376+
},
377+
],
378+
} as TestSuite,
379+
{
380+
name: 'testB',
381+
runMode: 'run',
382+
type: 'case',
383+
} as TestCase,
384+
];
385+
386+
const stringPatternTests = createTests();
387+
traverseUpdateTest(stringPatternTests, '2-1');
388+
389+
expect(stringPatternTests[0].tests[0]?.runMode).toBe('skip');
390+
expect(stringPatternTests[0].tests[1]?.runMode).toBe('skip');
391+
expect(
392+
(stringPatternTests[0].tests[2] as TestSuite).tests[0]?.runMode,
393+
).toBe('run');
394+
expect(
395+
(stringPatternTests[0].tests[2] as TestSuite).tests[1]?.runMode,
396+
).toBe('skip');
397+
expect(stringPatternTests[1].runMode).toBe('skip');
398+
399+
const globalRegexTests = createTests();
400+
traverseUpdateTest(globalRegexTests, /2-1/g);
401+
402+
expect(globalRegexTests).toEqual(stringPatternTests);
403+
});
342404
});

0 commit comments

Comments
 (0)