Skip to content

Commit ea4899e

Browse files
authored
[compiler][snap] Support pattern of files to test as CLI argument (facebook#35148)
I've been trying out LLM agents for compiler development, and one thing i found is that the agent naturally wants to run `yarn snap <pattern>` to test a specific fixture, and I want to be able to tell it (directly or in rules/skills) to do this in order to get the debug output from all the compiler passes. Agents can figure out our current testfilter.txt file system but that's just tedious. So here we add support for `yarn snap -p <pattern>`. If you pass in a pattern with an extension, we target that extension specifically. If you pass in a .expect.md file, we look at that specific fixture. And if the pattern doesn't have extensions, we search for `<pattern>{.js,.jsx,.ts,.tsx}`. When patterns are enabled we automatically log as in debug mode (if there is a single match), and disable watch mode. Open to feedback!
1 parent b946a24 commit ea4899e

File tree

2 files changed

+66
-11
lines changed

2 files changed

+66
-11
lines changed

compiler/packages/snap/src/fixture-utils.ts

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,21 @@ function stripExtension(filename: string, extensions: Array<string>): string {
4444
return filename;
4545
}
4646

47+
/**
48+
* Strip all extensions from a filename
49+
* e.g., "foo.expect.md" -> "foo"
50+
*/
51+
function stripAllExtensions(filename: string): string {
52+
let result = filename;
53+
while (true) {
54+
const extension = path.extname(result);
55+
if (extension === '') {
56+
return result;
57+
}
58+
result = path.basename(result, extension);
59+
}
60+
}
61+
4762
export async function readTestFilter(): Promise<TestFilter | null> {
4863
if (!(await exists(FILTER_PATH))) {
4964
throw new Error(`testfilter file not found at \`${FILTER_PATH}\``);
@@ -111,11 +126,25 @@ async function readInputFixtures(
111126
} else {
112127
inputFiles = (
113128
await Promise.all(
114-
filter.paths.map(pattern =>
115-
glob.glob(`${pattern}{${INPUT_EXTENSIONS.join(',')}}`, {
129+
filter.paths.map(pattern => {
130+
// If the pattern already has an extension other than .expect.md,
131+
// search for the pattern directly. Otherwise, search for the
132+
// pattern with the expected input extensions added.
133+
// Eg
134+
// `alias-while` => search for `alias-while{.js,.jsx,.ts,.tsx}`
135+
// `alias-while.js` => search as-is
136+
// `alias-while.expect.md` => search for `alias-while{.js,.jsx,.ts,.tsx}`
137+
const basename = path.basename(pattern);
138+
const basenameWithoutExt = stripAllExtensions(basename);
139+
const hasExtension = basename !== basenameWithoutExt;
140+
const globPattern =
141+
hasExtension && !pattern.endsWith(SNAPSHOT_EXTENSION)
142+
? pattern
143+
: `${basenameWithoutExt}{${INPUT_EXTENSIONS.join(',')}}`;
144+
return glob.glob(globPattern, {
116145
cwd: rootDir,
117-
}),
118-
),
146+
});
147+
}),
119148
)
120149
).flat();
121150
}
@@ -150,11 +179,13 @@ async function readOutputFixtures(
150179
} else {
151180
outputFiles = (
152181
await Promise.all(
153-
filter.paths.map(pattern =>
154-
glob.glob(`${pattern}${SNAPSHOT_EXTENSION}`, {
182+
filter.paths.map(pattern => {
183+
// Strip all extensions and find matching .expect.md files
184+
const basenameWithoutExt = stripAllExtensions(pattern);
185+
return glob.glob(`${basenameWithoutExt}${SNAPSHOT_EXTENSION}`, {
155186
cwd: rootDir,
156-
}),
157-
),
187+
});
188+
}),
158189
)
159190
).flat();
160191
}

compiler/packages/snap/src/runner.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type RunnerOptions = {
3535
watch: boolean;
3636
filter: boolean;
3737
update: boolean;
38+
pattern?: string;
3839
};
3940

4041
const opts: RunnerOptions = yargs
@@ -62,9 +63,15 @@ const opts: RunnerOptions = yargs
6263
'Only run fixtures which match the contents of testfilter.txt',
6364
)
6465
.default('filter', false)
66+
.string('pattern')
67+
.alias('p', 'pattern')
68+
.describe(
69+
'pattern',
70+
'Optional glob pattern to filter fixtures (e.g., "error.*", "use-memo")',
71+
)
6572
.help('help')
6673
.strict()
67-
.parseSync(hideBin(process.argv));
74+
.parseSync(hideBin(process.argv)) as RunnerOptions;
6875

6976
/**
7077
* Do a test run and return the test results
@@ -171,7 +178,13 @@ export async function main(opts: RunnerOptions): Promise<void> {
171178
worker.getStderr().pipe(process.stderr);
172179
worker.getStdout().pipe(process.stdout);
173180

174-
if (opts.watch) {
181+
// If pattern is provided, force watch mode off and use pattern filter
182+
const shouldWatch = opts.watch && opts.pattern == null;
183+
if (opts.watch && opts.pattern != null) {
184+
console.warn('NOTE: --watch is ignored when a --pattern is supplied');
185+
}
186+
187+
if (shouldWatch) {
175188
makeWatchRunner(state => onChange(worker, state), opts.filter);
176189
if (opts.filter) {
177190
/**
@@ -216,7 +229,18 @@ export async function main(opts: RunnerOptions): Promise<void> {
216229
try {
217230
execSync('yarn build', {cwd: PROJECT_ROOT});
218231
console.log('Built compiler successfully with tsup');
219-
const testFilter = opts.filter ? await readTestFilter() : null;
232+
233+
// Determine which filter to use
234+
let testFilter: TestFilter | null = null;
235+
if (opts.pattern) {
236+
testFilter = {
237+
debug: true,
238+
paths: [opts.pattern],
239+
};
240+
} else if (opts.filter) {
241+
testFilter = await readTestFilter();
242+
}
243+
220244
const results = await runFixtures(worker, testFilter, 0);
221245
if (opts.update) {
222246
update(results);

0 commit comments

Comments
 (0)