-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTestRunner.ts
More file actions
196 lines (172 loc) · 5.05 KB
/
Copy pathTestRunner.ts
File metadata and controls
196 lines (172 loc) · 5.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
/**
* TestRunner - Discovers and runs prompt tests
*/
import { glob } from "glob";
import { pathToFileURL } from "node:url";
import type { PromptWithTests, TestResults } from "@marrakesh/core";
export interface RunnerOptions {
bail?: boolean;
}
export interface RunnerResults {
total: number;
passed: number;
failed: number;
duration: number;
promptResults: Array<{
promptName: string;
results: TestResults;
}>;
}
/**
* Test runner that discovers and executes prompt tests
*/
export class TestRunner {
private options: Required<RunnerOptions>;
private progressCallback?: (event: { type: string; data: unknown }) => void;
private static tsxRegistered = false;
constructor(options: RunnerOptions = {}) {
this.options = {
bail: options.bail ?? false,
};
}
setProgressCallback(
callback: (event: { type: string; data: unknown }) => void,
): void {
this.progressCallback = callback;
}
/**
* Ensure tsx is registered for TypeScript support
*/
private async ensureTsxRegistered(): Promise<void> {
if (!TestRunner.tsxRegistered) {
try {
// Try to register tsx for TypeScript support
await import("tsx/esm");
TestRunner.tsxRegistered = true;
} catch {
try {
// Fallback to main tsx import
await import("tsx");
TestRunner.tsxRegistered = true;
} catch {
// If tsx is not available, we'll handle it in the import
console.warn(
"tsx not available, TypeScript files may not work properly",
);
TestRunner.tsxRegistered = true;
}
}
}
}
/**
* Find all test files matching the pattern and run them
*/
async findAndRun(pattern: string): Promise<RunnerResults> {
const files = await this.findTestFilesPrivate(pattern);
const prompts = await this.loadPrompts(files);
return this.runTests(prompts);
}
/**
* Find test files matching the glob pattern (public method)
*/
async findTestFiles(pattern: string): Promise<string[]> {
return this.findTestFilesPrivate(pattern);
}
/**
* Find test files matching the glob pattern
*/
private async findTestFilesPrivate(pattern: string): Promise<string[]> {
// Search for files matching pattern
const files = await glob(pattern, {
ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"],
absolute: true,
});
return files;
}
/**
* Load PromptWithTests instances from files
*/
private async loadPrompts(
files: string[],
): Promise<Array<{ name: string; prompt: PromptWithTests }>> {
const prompts: Array<{ name: string; prompt: PromptWithTests }> = [];
for (const file of files) {
try {
// Ensure tsx is registered for TypeScript files
if (file.endsWith(".ts") || file.endsWith(".tsx")) {
await this.ensureTsxRegistered();
}
// Use direct import for all files (both JS and TS with tsx registered)
const fileUrl = pathToFileURL(file).href;
const module = await import(fileUrl);
// Look for exported PromptWithTests instances
for (const [exportName, exportValue] of Object.entries(module)) {
if (this.isPromptWithTests(exportValue)) {
prompts.push({
name: exportName,
prompt: exportValue as PromptWithTests,
});
}
}
} catch (error) {
console.warn(`Failed to load ${file}:`, error);
}
}
return prompts;
}
/**
* Check if a value is a PromptWithTests instance
*/
private isPromptWithTests(value: unknown): boolean {
return (
value !== null &&
value !== undefined &&
typeof value === "object" &&
"run" in value &&
"getTestCases" in value
);
}
/**
* Run all discovered tests
*/
async runTests(
prompts: Array<{ name: string; prompt: PromptWithTests }>,
): Promise<RunnerResults> {
const startTime = Date.now();
const promptResults: Array<{
promptName: string;
results: TestResults;
}> = [];
for (const { name, prompt } of prompts) {
try {
// Run the tests with progress callback (this will trigger analytics tracking)
const results = await prompt.run({
bail: this.options.bail,
onProgress: this.progressCallback,
});
promptResults.push({
promptName: name,
results,
});
// Bail on first failure if option is set
if (this.options.bail && results.failed > 0) {
break;
}
} catch (error) {
console.error(`Error running tests for ${name}:`, error);
}
}
// Aggregate results
const total = promptResults.reduce((sum, r) => sum + r.results.total, 0);
const passed = promptResults.reduce((sum, r) => sum + r.results.passed, 0);
const failed = promptResults.reduce((sum, r) => sum + r.results.failed, 0);
const finalResults = {
total,
passed,
failed,
duration: Date.now() - startTime,
promptResults,
};
return finalResults;
}
}