Skip to content

Commit 66c1cbb

Browse files
authored
fix: combine multiple --project filters correctly (#10988)
1 parent fad263f commit 66c1cbb

8 files changed

Lines changed: 96 additions & 28 deletions

File tree

‎docs/guide/cli-generated.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -796,7 +796,7 @@ Minimum time in milliseconds it takes to spawn the typechecker
796796

797797
- **CLI:** `-p, --project <name>`
798798

799-
The name of the project to run if you are using Vitest workspace feature. This can be repeated for multiple projects: `--project=1 --project=2`. You can also filter projects using wildcards like `--project=packages*`, and exclude projects with `--project=!pattern`.
799+
The name of the project to run if you are using Vitest workspace feature. This can be repeated for multiple projects: `--project=1 --project=2`. You can also filter projects using wildcards like `--project=packages*`, and exclude projects with `--project=!pattern`. A project runs if it matches no negated pattern and, when regular patterns are also given, matches at least one of them.
800800

801801
### slowTestThreshold
802802

‎docs/guide/projects.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,15 @@ bun run test --project e2e --project unit
235235
```
236236
:::
237237

238+
The filter supports `*` wildcards and `!` exclusions. A project runs if it matches no negated pattern and, when regular patterns are also given, matches at least one of them:
239+
240+
```bash
241+
# run every project except "e2e"
242+
vitest --project '!e2e'
243+
# run every project starting with "unit", except "unit (browser)"
244+
vitest --project 'unit*' --project '!unit (browser)'
245+
```
246+
238247
## Configuration
239248

240249
Projects defined with an inline configuration inherit all options from the root-level configuration. This is controlled by the `extends` option, which is enabled by default since Vitest 5.0:

‎packages/vitest/src/node/cli/cli-config.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,7 @@ export const cliOptionsConfig: VitestCLIOptions = {
745745
project: {
746746
shorthand: 'p',
747747
description:
748-
'The name of the project to run if you are using Vitest workspace feature. This can be repeated for multiple projects: `--project=1 --project=2`. You can also filter projects using wildcards like `--project=packages*`, and exclude projects with `--project=!pattern`.',
748+
'The name of the project to run if you are using Vitest workspace feature. This can be repeated for multiple projects: `--project=1 --project=2`. You can also filter projects using wildcards like `--project=packages*`, and exclude projects with `--project=!pattern`. A project runs if it matches no negated pattern and, when regular patterns are also given, matches at least one of them.',
749749
argument: '<name>',
750750
array: true,
751751
},

‎packages/vitest/src/node/config/resolveConfig.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,9 +1251,12 @@ export function matchesProjectFilter(projects: string[], name: string): boolean
12511251
if (!projects.length) {
12521252
return true
12531253
}
1254-
return projects.some((project) => {
1255-
const regexp = wildcardPatternToRegExp(project)
1256-
return regexp.test(name)
1254+
if (isExcludedByProjectFilter(projects, name)) {
1255+
return false
1256+
}
1257+
const positives = projects.filter(project => !project.startsWith('!'))
1258+
return !positives.length || positives.some((project) => {
1259+
return wildcardPatternToRegExp(project).test(name)
12571260
})
12581261
}
12591262

‎packages/vitest/src/node/projects/resolveProjects.ts‎

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -814,10 +814,10 @@ function expandBrowserInstancesInEntries(
814814
continue
815815
}
816816

817-
const keepAllInstances = matchesEntryFilter(globalConfig.project, parentName, entry.ancestors)
818-
const filteredInstances = keepAllInstances
819-
? instances
820-
: instances.filter(instance => matchesProjectFilter(globalConfig.project, instance.name!))
817+
const parentMatches = matchesEntryFilter(globalConfig.project, parentName, entry.ancestors)
818+
const filteredInstances = instances.filter(instance => parentMatches
819+
? !isExcludedByProjectFilter(globalConfig.project, instance.name!)
820+
: matchesProjectFilter(globalConfig.project, instance.name!))
821821
if (!filteredInstances.length) {
822822
debug?.(`browser project ${projectLabel(parentName)} is dropped: no instances match the --project filter`)
823823
continue
@@ -1083,14 +1083,17 @@ function matchesEntryFilter(
10831083
if (!filter.length) {
10841084
return true
10851085
}
1086+
if (isEntryExcludedByFilter(filter, name, ancestors)) {
1087+
return false
1088+
}
1089+
const positives = filter.filter(project => !project.startsWith('!'))
1090+
if (!positives.length) {
1091+
return true
1092+
}
10861093
const names = [name, ...(ancestors || [])]
1087-
return filter.some((project) => {
1094+
return positives.some((project) => {
10881095
const regexp = wildcardPatternToRegExp(project)
1089-
// a negated pattern compiles into a negative lookahead: the entry is kept
1090-
// only when neither its name nor any of its containers match the exclusion
1091-
return project.startsWith('!')
1092-
? names.every(candidate => regexp.test(candidate))
1093-
: names.some(candidate => regexp.test(candidate))
1096+
return names.some(candidate => regexp.test(candidate))
10941097
})
10951098
}
10961099

‎packages/vitest/src/utils/base.ts‎

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,7 @@ export function escapeRegExp(s: string): string {
2424
}
2525

2626
export function wildcardPatternToRegExp(pattern: string): RegExp {
27-
const negated = pattern[0] === '!'
28-
29-
if (negated) {
30-
pattern = pattern.slice(1)
31-
}
32-
33-
let regexp = `${pattern.split('*').map(escapeRegExp).join('.*')}$`
34-
35-
if (negated) {
36-
regexp = `(?!${regexp})`
37-
}
38-
39-
return new RegExp(`^${regexp}`, 'i')
27+
return new RegExp(`^${pattern.split('*').map(escapeRegExp).join('.*')}$`, 'i')
4028
}
4129

4230
export function createIndexLocationsMap(source: string): Map<number, ModuleDefinitionLocation> {

‎test/e2e/test/config/browser-configs.test.ts‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,67 @@ test('negation wildcard filter excludes all matching browser instances', async (
526526
])
527527
})
528528

529+
test('negation filter excludes a single browser instance', async () => {
530+
const projects = await config({
531+
project: '!myproject (chromium)',
532+
projects: [
533+
{
534+
test: {
535+
name: 'myproject',
536+
browser: {
537+
enabled: true,
538+
provider: playwright(),
539+
headless: true,
540+
instances: [
541+
{ browser: 'chromium' },
542+
{ browser: 'firefox' },
543+
],
544+
},
545+
},
546+
},
547+
{
548+
test: {
549+
name: 'other',
550+
},
551+
},
552+
],
553+
})
554+
expect(projects.map(p => p.projectConfig.name)).toEqual([
555+
'other',
556+
'myproject (firefox)',
557+
])
558+
})
559+
560+
test('negation filter excludes a browser instance of a matching project', async () => {
561+
const projects = await config({
562+
project: ['myproject', '!myproject (chromium)'],
563+
projects: [
564+
{
565+
test: {
566+
name: 'myproject',
567+
browser: {
568+
enabled: true,
569+
provider: playwright(),
570+
headless: true,
571+
instances: [
572+
{ browser: 'chromium' },
573+
{ browser: 'firefox' },
574+
],
575+
},
576+
},
577+
},
578+
{
579+
test: {
580+
name: 'other',
581+
},
582+
},
583+
],
584+
})
585+
expect(projects.map(p => p.projectConfig.name)).toEqual([
586+
'myproject (firefox)',
587+
])
588+
})
589+
529590
test('filter for the global browser project includes all browser instances', async () => {
530591
const projects = await config({
531592
project: 'myproject',

‎test/e2e/test/projects.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,10 @@ describe('project filtering', () => {
10391039
{ pattern: '!project_1', expected: ['project_2', 'space_1'] },
10401040
{ pattern: '!project*', expected: ['space_1'] },
10411041
{ pattern: '!project', expected: allProjects },
1042+
{ pattern: ['!project_1', '!project_2'], expected: ['space_1'] },
1043+
{ pattern: ['!project_1', '!space_1'], expected: ['project_2'] },
1044+
{ pattern: ['project*', '!project_1'], expected: ['project_2'] },
1045+
{ pattern: ['*', '!space*'], expected: ['project_1', 'project_2'] },
10421046
])('should match projects correctly: $pattern', async ({ pattern, expected }) => {
10431047
const { ctx, stderr, stdout } = await runVitest({
10441048
root: 'fixtures/project',

0 commit comments

Comments
 (0)