Skip to content

Commit e2dbcab

Browse files
authored
feat(core): support run changed tests (#1243)
1 parent fc59ef6 commit e2dbcab

14 files changed

Lines changed: 605 additions & 13 deletions

File tree

e2e/filter/changed.test.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { mkdtemp, rm } from 'node:fs/promises';
2+
import { tmpdir } from 'node:os';
3+
import { dirname, join } from 'node:path';
4+
import { fileURLToPath } from 'node:url';
5+
import { describe, expect, it, onTestFinished } from '@rstest/core';
6+
import { prepareFixtures, runRstestCli } from '../scripts';
7+
8+
const __filename = fileURLToPath(import.meta.url);
9+
const __dirname = dirname(__filename);
10+
11+
const changedFixturePath = join(__dirname, 'fixtures-changed');
12+
13+
const collectRunTestFileLogs = (stdout: string) =>
14+
stdout
15+
.split('\n')
16+
.filter((log) => log.includes('.test.ts') && !log.startsWith('::error'))
17+
.sort();
18+
19+
const prepareChangedFixture = async (name: string) => {
20+
const fixturesTargetPath = await mkdtemp(join(tmpdir(), `rstest-${name}-`));
21+
22+
onTestFinished(() =>
23+
rm(fixturesTargetPath, {
24+
recursive: true,
25+
force: true,
26+
maxRetries: 10,
27+
retryDelay: 500,
28+
}),
29+
);
30+
31+
const { fs } = await prepareFixtures({
32+
fixturesPath: changedFixturePath,
33+
fixturesTargetPath,
34+
});
35+
36+
return { fixturesTargetPath, fs };
37+
};
38+
39+
const runGit = async (cwd: string, args: string[]) => {
40+
const { execFile } = await import('node:child_process');
41+
const { promisify } = await import('node:util');
42+
const execFileAsync = promisify(execFile);
43+
44+
await execFileAsync('git', args, { cwd });
45+
};
46+
47+
const initGitFixture = async (cwd: string) => {
48+
await runGit(cwd, ['-c', 'init.defaultBranch=main', 'init']);
49+
await runGit(cwd, ['add', '.']);
50+
await runGit(cwd, [
51+
'-c',
52+
'user.name=rstest',
53+
'-c',
54+
'user.email=rstest@example.com',
55+
'commit',
56+
'-m',
57+
'init',
58+
]);
59+
};
60+
61+
describe('changed test filtering', () => {
62+
it('should reject changed with positional filters', async () => {
63+
const { cli, expectExecFailed } = await runRstestCli({
64+
command: 'rstest',
65+
args: ['run', '--changed=HEAD', 'src/index.ts'],
66+
options: {
67+
nodeOptions: {
68+
cwd: changedFixturePath,
69+
},
70+
},
71+
});
72+
73+
await expectExecFailed();
74+
75+
expect(cli.stderr).toContain(
76+
'The `--changed` option cannot be used with positional filters.',
77+
);
78+
});
79+
80+
it('should reject changed with related options', async () => {
81+
const { cli, expectExecFailed } = await runRstestCli({
82+
command: 'rstest',
83+
args: ['run', '--changed', '--related'],
84+
options: {
85+
nodeOptions: {
86+
cwd: changedFixturePath,
87+
},
88+
},
89+
});
90+
91+
await expectExecFailed();
92+
93+
expect(cli.stderr).toContain(
94+
'Options `--related`, `--findRelatedTests`, and `--changed` cannot be used together.',
95+
);
96+
});
97+
98+
it('should print a clear error when changed cannot read Git state', async () => {
99+
const { fixturesTargetPath } =
100+
await prepareChangedFixture('changed-no-git');
101+
102+
const { cli, expectExecFailed } = await runRstestCli({
103+
command: 'rstest',
104+
args: ['run', '--changed'],
105+
options: {
106+
nodeOptions: {
107+
cwd: fixturesTargetPath,
108+
env: {
109+
GIT_CEILING_DIRECTORIES: dirname(fixturesTargetPath),
110+
},
111+
},
112+
},
113+
});
114+
115+
await expectExecFailed();
116+
117+
expect(cli.stderr).toContain(
118+
'Failed to resolve changed files for `--changed` from ',
119+
);
120+
expect(cli.stderr).toContain(
121+
'. Make sure the current root is inside a Git repository.',
122+
);
123+
expect(cli.stderr).toContain('Git error:');
124+
});
125+
126+
it('should run tests related to changed files', async () => {
127+
const { fixturesTargetPath, fs } =
128+
await prepareChangedFixture('changed-files');
129+
130+
await initGitFixture(fixturesTargetPath);
131+
132+
fs.update(join(fixturesTargetPath, 'src/index.ts'), (content) =>
133+
content.replace("greet('index')", "greet('index changed')"),
134+
);
135+
136+
const { cli, expectExecFailed } = await runRstestCli({
137+
command: 'rstest',
138+
args: ['run', '--changed'],
139+
options: {
140+
nodeOptions: {
141+
cwd: fixturesTargetPath,
142+
},
143+
},
144+
});
145+
146+
await expectExecFailed();
147+
148+
const logs = collectRunTestFileLogs(cli.stdout);
149+
150+
expect(logs).toMatchInlineSnapshot(`
151+
[
152+
" ✗ index.test.ts (1)",
153+
]
154+
`);
155+
});
156+
157+
it('should run tests related to files changed since a commit', async () => {
158+
const { fixturesTargetPath, fs } =
159+
await prepareChangedFixture('changed-commit');
160+
161+
await initGitFixture(fixturesTargetPath);
162+
163+
const { execFile } = await import('node:child_process');
164+
const { promisify } = await import('node:util');
165+
const execFileAsync = promisify(execFile);
166+
const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
167+
cwd: fixturesTargetPath,
168+
encoding: 'utf8',
169+
});
170+
const baseCommit = stdout.trim();
171+
172+
fs.update(join(fixturesTargetPath, 'src/other.ts'), (content) =>
173+
content.replace("greet('other')", "greet('other changed')"),
174+
);
175+
await runGit(fixturesTargetPath, ['add', '.']);
176+
await runGit(fixturesTargetPath, [
177+
'-c',
178+
'user.name=rstest',
179+
'-c',
180+
'user.email=rstest@example.com',
181+
'commit',
182+
'-m',
183+
'change other',
184+
]);
185+
fs.update(join(fixturesTargetPath, 'src/index.ts'), (content) =>
186+
content.replace("greet('index')", "greet('index changed')"),
187+
);
188+
189+
const { cli, expectExecFailed } = await runRstestCli({
190+
command: 'rstest',
191+
args: ['run', '--changed', baseCommit],
192+
options: {
193+
nodeOptions: {
194+
cwd: fixturesTargetPath,
195+
},
196+
},
197+
});
198+
199+
await expectExecFailed();
200+
201+
const logs = collectRunTestFileLogs(cli.stdout);
202+
203+
expect(logs).toMatchInlineSnapshot(`
204+
[
205+
" ✗ index.test.ts (1)",
206+
" ✗ other.test.ts (1)",
207+
]
208+
`);
209+
});
210+
211+
it('should pass when changed finds no files', async () => {
212+
const { fixturesTargetPath } = await prepareChangedFixture('changed-empty');
213+
214+
await initGitFixture(fixturesTargetPath);
215+
216+
const { cli, expectExecSuccess } = await runRstestCli({
217+
command: 'rstest',
218+
args: ['run', '--changed'],
219+
options: {
220+
nodeOptions: {
221+
cwd: fixturesTargetPath,
222+
},
223+
},
224+
});
225+
226+
await expectExecSuccess();
227+
228+
expect(cli.stdout).toContain('No test files found, exiting with code 0.');
229+
});
230+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { describe, expect, it } from '@rstest/core';
2+
import { sayHi } from './src/index';
3+
4+
describe('index', () => {
5+
it('should greet index', () => {
6+
expect(sayHi()).toBe('Hello, index!');
7+
});
8+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { describe, expect, it } from '@rstest/core';
2+
import { sayBye } from './src/other';
3+
4+
describe('other', () => {
5+
it('should greet other', () => {
6+
expect(sayBye()).toBe('Hello, other!');
7+
});
8+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { greet } from './shared';
2+
3+
export const sayHi = () => greet('index');
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { greet } from './shared';
2+
3+
export const sayBye = () => greet('other');
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const greet = (name: string) => `Hello, ${name}!`;

0 commit comments

Comments
 (0)