tryscript 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +223 -0
- package/dist/bin.cjs +390 -0
- package/dist/bin.cjs.map +1 -0
- package/dist/bin.d.cts +2 -0
- package/dist/bin.d.mts +2 -0
- package/dist/bin.mjs +389 -0
- package/dist/bin.mjs.map +1 -0
- package/dist/index.cjs +11 -0
- package/dist/index.d.cts +165 -0
- package/dist/index.d.mts +165 -0
- package/dist/index.mjs +4 -0
- package/dist/src-CeUA446P.cjs +422 -0
- package/dist/src-CeUA446P.cjs.map +1 -0
- package/dist/src-UjaSQrqA.mjs +328 -0
- package/dist/src-UjaSQrqA.mjs.map +1 -0
- package/docs/tryscript-reference.md +163 -0
- package/package.json +76 -0
package/dist/bin.mjs
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { a as createExecutionContext, i as cleanupExecutionContext, l as loadConfig, n as matchOutput, o as runBlock, s as parseTestFile, t as VERSION, u as mergeConfig } from "./src-UjaSQrqA.mjs";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import pc from "picocolors";
|
|
11
|
+
import fg from "fast-glob";
|
|
12
|
+
import { createPatch } from "diff";
|
|
13
|
+
import { writeFile } from "atomically";
|
|
14
|
+
|
|
15
|
+
//#region src/lib/reporter.ts
|
|
16
|
+
/**
|
|
17
|
+
* Create a unified diff between expected and actual output.
|
|
18
|
+
*/
|
|
19
|
+
function createDiff(expected, actual, filename) {
|
|
20
|
+
return createPatch(filename, expected, actual, "expected", "actual").split("\n").slice(4).map((line) => {
|
|
21
|
+
if (line.startsWith("+")) return pc.green(line);
|
|
22
|
+
if (line.startsWith("-")) return pc.red(line);
|
|
23
|
+
if (line.startsWith("@")) return pc.cyan(line);
|
|
24
|
+
return line;
|
|
25
|
+
}).join("\n");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Format a duration in milliseconds for display.
|
|
29
|
+
*/
|
|
30
|
+
function formatDuration(ms) {
|
|
31
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
32
|
+
return `${(ms / 1e3).toFixed(2)}s`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Report results for a single file.
|
|
36
|
+
*/
|
|
37
|
+
function reportFile(result, options) {
|
|
38
|
+
const filename = result.file.path;
|
|
39
|
+
const status = result.passed ? pc.green(pc.bold("PASS")) : pc.red(pc.bold("FAIL"));
|
|
40
|
+
if (options.quiet && result.passed) return;
|
|
41
|
+
console.error(`${status} ${filename}`);
|
|
42
|
+
for (const blockResult of result.results) {
|
|
43
|
+
const name = blockResult.block.name ?? `Line ${blockResult.block.lineNumber}`;
|
|
44
|
+
if (blockResult.passed) {
|
|
45
|
+
if (!options.quiet) console.error(` ${pc.green("✓")} ${name}`);
|
|
46
|
+
} else {
|
|
47
|
+
console.error(` ${pc.red("✗")} ${name}`);
|
|
48
|
+
if (blockResult.error) console.error(` ${pc.red(blockResult.error)}`);
|
|
49
|
+
else {
|
|
50
|
+
if (blockResult.actualExitCode !== blockResult.block.expectedExitCode) console.error(` Expected exit code ${blockResult.block.expectedExitCode}, got ${blockResult.actualExitCode}`);
|
|
51
|
+
if (options.diff && blockResult.diff) {
|
|
52
|
+
console.error("");
|
|
53
|
+
console.error(blockResult.diff);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
console.error("");
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Report final summary.
|
|
62
|
+
*/
|
|
63
|
+
function reportSummary(summary, _options) {
|
|
64
|
+
const parts = [];
|
|
65
|
+
if (summary.totalPassed > 0) parts.push(pc.green(`${summary.totalPassed} passed`));
|
|
66
|
+
if (summary.totalFailed > 0) parts.push(pc.red(`${summary.totalFailed} failed`));
|
|
67
|
+
const duration = formatDuration(summary.duration);
|
|
68
|
+
const line = `${parts.join(", ")} (${duration})`;
|
|
69
|
+
console.log(line);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/lib/updater.ts
|
|
74
|
+
/**
|
|
75
|
+
* Update a test file with actual output from test results.
|
|
76
|
+
*/
|
|
77
|
+
async function updateTestFile(file, results) {
|
|
78
|
+
let content = file.rawContent;
|
|
79
|
+
const changes = [];
|
|
80
|
+
const blocksWithResults = file.blocks.map((block, i) => ({
|
|
81
|
+
block,
|
|
82
|
+
result: results[i]
|
|
83
|
+
})).reverse();
|
|
84
|
+
for (const { block, result } of blocksWithResults) {
|
|
85
|
+
if (!result) continue;
|
|
86
|
+
if (result.passed) continue;
|
|
87
|
+
if (result.error) continue;
|
|
88
|
+
const newBlockContent = buildUpdatedBlock(block, result);
|
|
89
|
+
const blockStart = content.indexOf(block.rawContent);
|
|
90
|
+
if (blockStart !== -1) {
|
|
91
|
+
content = content.slice(0, blockStart) + newBlockContent + content.slice(blockStart + block.rawContent.length);
|
|
92
|
+
changes.push(block.name ?? `Line ${block.lineNumber}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (changes.length > 0) await writeFile(file.path, content);
|
|
96
|
+
return {
|
|
97
|
+
updated: changes.length > 0,
|
|
98
|
+
changes
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Build an updated console block with new expected output.
|
|
103
|
+
*/
|
|
104
|
+
function buildUpdatedBlock(block, result) {
|
|
105
|
+
const lines = ["```console", ...block.command.split("\n").map((line, i) => {
|
|
106
|
+
return i === 0 ? `$ ${line}` : `> ${line}`;
|
|
107
|
+
})];
|
|
108
|
+
const trimmedOutput = result.actualOutput.trimEnd();
|
|
109
|
+
if (trimmedOutput) lines.push(trimmedOutput);
|
|
110
|
+
lines.push(`? ${result.actualExitCode}`, "```");
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/cli/commands/run.ts
|
|
116
|
+
async function runCommand(files, options) {
|
|
117
|
+
const startTime = Date.now();
|
|
118
|
+
const opts = {
|
|
119
|
+
diff: options.diff !== false,
|
|
120
|
+
verbose: options.verbose ?? false,
|
|
121
|
+
quiet: options.quiet ?? false,
|
|
122
|
+
update: options.update ?? false,
|
|
123
|
+
failFast: options.failFast ?? false,
|
|
124
|
+
filter: options.filter
|
|
125
|
+
};
|
|
126
|
+
const testFiles = await fg(files.length > 0 ? files : ["**/*.tryscript.md"], {
|
|
127
|
+
ignore: ["**/node_modules/**", "**/dist/**"],
|
|
128
|
+
absolute: true,
|
|
129
|
+
dot: false
|
|
130
|
+
});
|
|
131
|
+
if (testFiles.length === 0) {
|
|
132
|
+
console.error(pc.yellow("No test files found"));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
const globalConfig = await loadConfig(process.cwd());
|
|
136
|
+
const fileResults = [];
|
|
137
|
+
let shouldStop = false;
|
|
138
|
+
for (const filePath of testFiles) {
|
|
139
|
+
if (shouldStop) break;
|
|
140
|
+
const testFile = parseTestFile(await readFile(filePath, "utf-8"), filePath);
|
|
141
|
+
const config = mergeConfig(globalConfig, testFile.config);
|
|
142
|
+
let blocksToRun = testFile.blocks;
|
|
143
|
+
if (opts.filter) {
|
|
144
|
+
const filterPattern = new RegExp(opts.filter, "i");
|
|
145
|
+
blocksToRun = blocksToRun.filter((b) => b.name ? filterPattern.test(b.name) : true);
|
|
146
|
+
}
|
|
147
|
+
if (blocksToRun.length === 0) continue;
|
|
148
|
+
const ctx = await createExecutionContext(config, filePath);
|
|
149
|
+
const results = [];
|
|
150
|
+
try {
|
|
151
|
+
for (const block of blocksToRun) {
|
|
152
|
+
const result = await runBlock(block, ctx);
|
|
153
|
+
const matches = matchOutput(result.actualOutput, block.expectedOutput, {
|
|
154
|
+
root: ctx.tempDir,
|
|
155
|
+
cwd: ctx.tempDir
|
|
156
|
+
}, config.patterns ?? {});
|
|
157
|
+
const exitCodeMatches = result.actualExitCode === block.expectedExitCode;
|
|
158
|
+
result.passed = matches && exitCodeMatches && !result.error;
|
|
159
|
+
if (!result.passed && opts.diff) result.diff = createDiff(block.expectedOutput, result.actualOutput, `${filePath}:${block.lineNumber}`);
|
|
160
|
+
results.push(result);
|
|
161
|
+
if (!result.passed && opts.failFast) {
|
|
162
|
+
shouldStop = true;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} finally {
|
|
167
|
+
await cleanupExecutionContext(ctx);
|
|
168
|
+
}
|
|
169
|
+
const fileResult = {
|
|
170
|
+
file: testFile,
|
|
171
|
+
results,
|
|
172
|
+
passed: results.every((r) => r.passed),
|
|
173
|
+
duration: results.reduce((sum, r) => sum + r.duration, 0)
|
|
174
|
+
};
|
|
175
|
+
fileResults.push(fileResult);
|
|
176
|
+
reportFile(fileResult, opts);
|
|
177
|
+
if (opts.update && !fileResult.passed) {
|
|
178
|
+
const { updated, changes } = await updateTestFile(testFile, results);
|
|
179
|
+
if (updated) console.error(pc.yellow(` ↻ Updated: ${changes.join(", ")}`));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const summary = {
|
|
183
|
+
files: fileResults,
|
|
184
|
+
totalPassed: fileResults.reduce((sum, f) => sum + f.results.filter((r) => r.passed).length, 0),
|
|
185
|
+
totalFailed: fileResults.reduce((sum, f) => sum + f.results.filter((r) => !r.passed).length, 0),
|
|
186
|
+
totalBlocks: fileResults.reduce((sum, f) => sum + f.results.length, 0),
|
|
187
|
+
duration: Date.now() - startTime
|
|
188
|
+
};
|
|
189
|
+
reportSummary(summary, opts);
|
|
190
|
+
process.exit(summary.totalFailed > 0 ? 1 : 0);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/cli/commands/readme.ts
|
|
195
|
+
/**
|
|
196
|
+
* Get the path to the README.md file.
|
|
197
|
+
* Works both during development and when installed as a package.
|
|
198
|
+
*/
|
|
199
|
+
function getReadmePath() {
|
|
200
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
201
|
+
if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "README.md");
|
|
202
|
+
return join(dirname(dirname(dirname(thisDir))), "README.md");
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Load the README content.
|
|
206
|
+
*/
|
|
207
|
+
function loadReadme() {
|
|
208
|
+
const readmePath = getReadmePath();
|
|
209
|
+
try {
|
|
210
|
+
return readFileSync(readmePath, "utf-8");
|
|
211
|
+
} catch (error) {
|
|
212
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
213
|
+
throw new Error(`Failed to load README from ${readmePath}: ${message}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Apply basic terminal formatting to markdown content.
|
|
218
|
+
* Colorizes headers, code blocks, and other elements for better readability.
|
|
219
|
+
*/
|
|
220
|
+
function formatMarkdown$1(content, useColors) {
|
|
221
|
+
if (!useColors) return content;
|
|
222
|
+
const lines = content.split("\n");
|
|
223
|
+
const formatted = [];
|
|
224
|
+
let inCodeBlock = false;
|
|
225
|
+
for (const line of lines) {
|
|
226
|
+
if (line.startsWith("```")) {
|
|
227
|
+
inCodeBlock = !inCodeBlock;
|
|
228
|
+
formatted.push(pc.dim(line));
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (inCodeBlock) {
|
|
232
|
+
formatted.push(pc.dim(line));
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (line.startsWith("# ")) {
|
|
236
|
+
formatted.push(pc.bold(pc.cyan(line)));
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (line.startsWith("## ")) {
|
|
240
|
+
formatted.push(pc.bold(pc.blue(line)));
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (line.startsWith("### ")) {
|
|
244
|
+
formatted.push(pc.bold(line));
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
|
|
248
|
+
return pc.yellow(code);
|
|
249
|
+
});
|
|
250
|
+
formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
|
|
251
|
+
return pc.bold(text);
|
|
252
|
+
});
|
|
253
|
+
formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
|
|
254
|
+
return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
|
|
255
|
+
});
|
|
256
|
+
formatted.push(formattedLine);
|
|
257
|
+
}
|
|
258
|
+
return formatted.join("\n");
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Check if stdout is an interactive terminal.
|
|
262
|
+
*/
|
|
263
|
+
function isInteractive$1() {
|
|
264
|
+
return process.stdout.isTTY === true;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Register the readme command.
|
|
268
|
+
*/
|
|
269
|
+
function registerReadmeCommand(program) {
|
|
270
|
+
program.command("readme").description("Display README documentation").option("--raw", "Output raw markdown without formatting").action((options) => {
|
|
271
|
+
try {
|
|
272
|
+
const formatted = formatMarkdown$1(loadReadme(), !options.raw && isInteractive$1());
|
|
273
|
+
console.log(formatted);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
276
|
+
console.error(pc.red(`Error: ${message}`));
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/cli/commands/docs.ts
|
|
284
|
+
/**
|
|
285
|
+
* Get the path to the tryscript-reference.md file.
|
|
286
|
+
* Works both during development and when installed as a package.
|
|
287
|
+
*/
|
|
288
|
+
function getDocsPath() {
|
|
289
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
290
|
+
if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "docs", "tryscript-reference.md");
|
|
291
|
+
return join(dirname(dirname(dirname(thisDir))), "docs", "tryscript-reference.md");
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Load the docs content.
|
|
295
|
+
*/
|
|
296
|
+
function loadDocs() {
|
|
297
|
+
const docsPath = getDocsPath();
|
|
298
|
+
try {
|
|
299
|
+
return readFileSync(docsPath, "utf-8");
|
|
300
|
+
} catch (error) {
|
|
301
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
302
|
+
throw new Error(`Failed to load reference docs from ${docsPath}: ${message}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Apply basic terminal formatting to markdown content.
|
|
307
|
+
* Colorizes headers, code blocks, and other elements for better readability.
|
|
308
|
+
*/
|
|
309
|
+
function formatMarkdown(content, useColors) {
|
|
310
|
+
if (!useColors) return content;
|
|
311
|
+
const lines = content.split("\n");
|
|
312
|
+
const formatted = [];
|
|
313
|
+
let inCodeBlock = false;
|
|
314
|
+
for (const line of lines) {
|
|
315
|
+
if (line.startsWith("```")) {
|
|
316
|
+
inCodeBlock = !inCodeBlock;
|
|
317
|
+
formatted.push(pc.dim(line));
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (inCodeBlock) {
|
|
321
|
+
formatted.push(pc.dim(line));
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (line.startsWith("# ")) {
|
|
325
|
+
formatted.push(pc.bold(pc.cyan(line)));
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (line.startsWith("## ")) {
|
|
329
|
+
formatted.push(pc.bold(pc.blue(line)));
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (line.startsWith("### ")) {
|
|
333
|
+
formatted.push(pc.bold(line));
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
|
|
337
|
+
return pc.yellow(code);
|
|
338
|
+
});
|
|
339
|
+
formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
|
|
340
|
+
return pc.bold(text);
|
|
341
|
+
});
|
|
342
|
+
formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
|
|
343
|
+
return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
|
|
344
|
+
});
|
|
345
|
+
formatted.push(formattedLine);
|
|
346
|
+
}
|
|
347
|
+
return formatted.join("\n");
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Check if stdout is an interactive terminal.
|
|
351
|
+
*/
|
|
352
|
+
function isInteractive() {
|
|
353
|
+
return process.stdout.isTTY === true;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Register the docs command.
|
|
357
|
+
*/
|
|
358
|
+
function registerDocsCommand(program) {
|
|
359
|
+
program.command("docs").description("Display concise syntax reference").option("--raw", "Output raw markdown without formatting").action((options) => {
|
|
360
|
+
try {
|
|
361
|
+
const formatted = formatMarkdown(loadDocs(), !options.raw && isInteractive());
|
|
362
|
+
console.log(formatted);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
365
|
+
console.error(pc.red(`Error: ${message}`));
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
//#endregion
|
|
372
|
+
//#region src/cli/cli.ts
|
|
373
|
+
function run(argv) {
|
|
374
|
+
const program = new Command().name("tryscript").version(VERSION, "--version", "Show version number").description("Golden testing for CLI applications").showHelpAfterError("(use --help for usage)").argument("[files...]", "Test files to run (default: **/*.tryscript.md)").option("--update", "Update golden files with actual output").option("--diff", "Show diff on failure (default: true)").option("--no-diff", "Hide diff on failure").option("--fail-fast", "Stop on first failure").option("--filter <pattern>", "Filter tests by name pattern").option("--verbose", "Show detailed output including passing test output").option("--quiet", "Suppress non-essential output (only show failures)").action(runCommand);
|
|
375
|
+
registerReadmeCommand(program);
|
|
376
|
+
registerDocsCommand(program);
|
|
377
|
+
program.parseAsync(argv).catch((err) => {
|
|
378
|
+
console.error(pc.red(`Error: ${err.message}`));
|
|
379
|
+
process.exit(2);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
//#endregion
|
|
384
|
+
//#region src/bin.ts
|
|
385
|
+
run(process.argv);
|
|
386
|
+
|
|
387
|
+
//#endregion
|
|
388
|
+
export { };
|
|
389
|
+
//# sourceMappingURL=bin.mjs.map
|
package/dist/bin.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.mjs","names":["parts: string[]","changes: string[]","lines: string[]","fileResults: TestFileResult[]","results: TestBlockResult[]","fileResult: TestFileResult","summary: TestRunSummary","formatMarkdown","formatted: string[]","isInteractive","formatted: string[]"],"sources":["../src/lib/reporter.ts","../src/lib/updater.ts","../src/cli/commands/run.ts","../src/cli/commands/readme.ts","../src/cli/commands/docs.ts","../src/cli/cli.ts","../src/bin.ts"],"sourcesContent":["import pc from 'picocolors';\nimport { createPatch } from 'diff';\nimport type { TestFileResult, TestRunSummary } from './types.js';\n\nexport interface ReporterOptions {\n diff: boolean;\n verbose: boolean;\n quiet: boolean;\n}\n\n/**\n * Create a unified diff between expected and actual output.\n */\nexport function createDiff(expected: string, actual: string, filename: string): string {\n const patch = createPatch(filename, expected, actual, 'expected', 'actual');\n // Remove the header lines (first 4 lines)\n const lines = patch.split('\\n').slice(4);\n return lines\n .map((line) => {\n if (line.startsWith('+')) {\n return pc.green(line);\n }\n if (line.startsWith('-')) {\n return pc.red(line);\n }\n if (line.startsWith('@')) {\n return pc.cyan(line);\n }\n return line;\n })\n .join('\\n');\n}\n\n/**\n * Format a duration in milliseconds for display.\n */\nfunction formatDuration(ms: number): string {\n if (ms < 1000) {\n return `${ms}ms`;\n }\n return `${(ms / 1000).toFixed(2)}s`;\n}\n\n/**\n * Report results for a single file.\n */\nexport function reportFile(result: TestFileResult, options: ReporterOptions): void {\n const filename = result.file.path;\n const status = result.passed ? pc.green(pc.bold('PASS')) : pc.red(pc.bold('FAIL'));\n\n if (options.quiet && result.passed) {\n return;\n }\n\n // File header\n console.error(`${status} ${filename}`);\n\n // Individual block results\n for (const blockResult of result.results) {\n const name = blockResult.block.name ?? `Line ${blockResult.block.lineNumber}`;\n\n if (blockResult.passed) {\n if (!options.quiet) {\n console.error(` ${pc.green('✓')} ${name}`);\n }\n } else {\n console.error(` ${pc.red('✗')} ${name}`);\n\n // Show error details\n if (blockResult.error) {\n console.error(` ${pc.red(blockResult.error)}`);\n } else {\n // Exit code mismatch\n if (blockResult.actualExitCode !== blockResult.block.expectedExitCode) {\n console.error(\n ` Expected exit code ${blockResult.block.expectedExitCode}, got ${blockResult.actualExitCode}`,\n );\n }\n\n // Output mismatch with diff\n if (options.diff && blockResult.diff) {\n console.error('');\n console.error(blockResult.diff);\n }\n }\n }\n }\n\n console.error('');\n}\n\n/**\n * Report final summary.\n */\nexport function reportSummary(summary: TestRunSummary, _options: ReporterOptions): void {\n const parts: string[] = [];\n\n if (summary.totalPassed > 0) {\n parts.push(pc.green(`${summary.totalPassed} passed`));\n }\n if (summary.totalFailed > 0) {\n parts.push(pc.red(`${summary.totalFailed} failed`));\n }\n\n const duration = formatDuration(summary.duration);\n const line = `${parts.join(', ')} (${duration})`;\n\n // Summary goes to stdout (can be piped/parsed)\n console.log(line);\n}\n","import { writeFile } from 'atomically';\nimport type { TestFile, TestBlock, TestBlockResult } from './types.js';\n\n/**\n * Update a test file with actual output from test results.\n */\nexport async function updateTestFile(\n file: TestFile,\n results: TestBlockResult[],\n): Promise<{ updated: boolean; changes: string[] }> {\n let content = file.rawContent;\n const changes: string[] = [];\n\n // Process blocks in reverse order to maintain correct offsets\n const blocksWithResults = file.blocks\n .map((block, i) => ({ block, result: results[i] }))\n .reverse();\n\n for (const { block, result } of blocksWithResults) {\n if (!result) {\n continue;\n }\n\n if (result.passed) {\n continue; // Don't touch passing tests\n }\n\n if (result.error) {\n // Execution error, can't update\n continue;\n }\n\n // Build the new block content\n const newBlockContent = buildUpdatedBlock(block, result);\n\n // Find and replace the block in the file\n const blockStart = content.indexOf(block.rawContent);\n if (blockStart !== -1) {\n content =\n content.slice(0, blockStart) +\n newBlockContent +\n content.slice(blockStart + block.rawContent.length);\n\n changes.push(block.name ?? `Line ${block.lineNumber}`);\n }\n }\n\n if (changes.length > 0) {\n await writeFile(file.path, content);\n }\n\n return { updated: changes.length > 0, changes };\n}\n\n/**\n * Build an updated console block with new expected output.\n */\nfunction buildUpdatedBlock(block: TestBlock, result: TestBlockResult): string {\n // Reconstruct the command line(s)\n const commandLines = block.command.split('\\n').map((line, i) => {\n return i === 0 ? `$ ${line}` : `> ${line}`;\n });\n\n // Build the block\n const lines: string[] = ['```console', ...commandLines];\n\n // Add output if present\n const trimmedOutput = result.actualOutput.trimEnd();\n if (trimmedOutput) {\n lines.push(trimmedOutput);\n }\n\n // Add exit code\n lines.push(`? ${result.actualExitCode}`, '```');\n\n return lines.join('\\n');\n}\n","import { readFile } from 'node:fs/promises';\nimport fg from 'fast-glob';\nimport pc from 'picocolors';\nimport { loadConfig, mergeConfig } from '../../lib/config.js';\nimport { parseTestFile } from '../../lib/parser.js';\nimport { runBlock, createExecutionContext, cleanupExecutionContext } from '../../lib/runner.js';\nimport { matchOutput } from '../../lib/matcher.js';\nimport { createDiff, reportFile, reportSummary } from '../../lib/reporter.js';\nimport { updateTestFile } from '../../lib/updater.js';\nimport type { TestBlockResult, TestFileResult, TestRunSummary } from '../../lib/types.js';\n\ninterface RunOptions {\n update?: boolean;\n diff?: boolean;\n failFast?: boolean;\n filter?: string;\n verbose?: boolean;\n quiet?: boolean;\n}\n\nexport async function runCommand(files: string[], options: RunOptions): Promise<void> {\n const startTime = Date.now();\n\n // Default options\n const opts = {\n diff: options.diff !== false,\n verbose: options.verbose ?? false,\n quiet: options.quiet ?? false,\n update: options.update ?? false,\n failFast: options.failFast ?? false,\n filter: options.filter,\n };\n\n // Find test files (fast-glob respects .gitignore by default)\n const patterns = files.length > 0 ? files : ['**/*.tryscript.md'];\n const testFiles = await fg(patterns, {\n ignore: ['**/node_modules/**', '**/dist/**'],\n absolute: true,\n dot: false,\n });\n\n if (testFiles.length === 0) {\n console.error(pc.yellow('No test files found'));\n process.exit(1);\n }\n\n // Load global config\n const globalConfig = await loadConfig(process.cwd());\n\n // Run tests\n const fileResults: TestFileResult[] = [];\n let shouldStop = false;\n\n for (const filePath of testFiles) {\n if (shouldStop) {\n break;\n }\n\n const content = await readFile(filePath, 'utf-8');\n const testFile = parseTestFile(content, filePath);\n const config = mergeConfig(globalConfig, testFile.config);\n\n // Filter blocks by name if specified\n let blocksToRun = testFile.blocks;\n if (opts.filter) {\n const filterPattern = new RegExp(opts.filter, 'i');\n blocksToRun = blocksToRun.filter((b) => (b.name ? filterPattern.test(b.name) : true));\n }\n\n if (blocksToRun.length === 0) {\n continue;\n }\n\n const ctx = await createExecutionContext(config, filePath);\n const results: TestBlockResult[] = [];\n\n try {\n for (const block of blocksToRun) {\n const result = await runBlock(block, ctx);\n\n // Check if output matches expected\n const matches = matchOutput(\n result.actualOutput,\n block.expectedOutput,\n { root: ctx.tempDir, cwd: ctx.tempDir },\n config.patterns ?? {},\n );\n\n const exitCodeMatches = result.actualExitCode === block.expectedExitCode;\n result.passed = matches && exitCodeMatches && !result.error;\n\n if (!result.passed && opts.diff) {\n result.diff = createDiff(\n block.expectedOutput,\n result.actualOutput,\n `${filePath}:${block.lineNumber}`,\n );\n }\n\n results.push(result);\n\n if (!result.passed && opts.failFast) {\n shouldStop = true;\n break;\n }\n }\n } finally {\n await cleanupExecutionContext(ctx);\n }\n\n const fileResult: TestFileResult = {\n file: testFile,\n results,\n passed: results.every((r) => r.passed),\n duration: results.reduce((sum, r) => sum + r.duration, 0),\n };\n\n fileResults.push(fileResult);\n reportFile(fileResult, opts);\n\n // Update mode\n if (opts.update && !fileResult.passed) {\n const { updated, changes } = await updateTestFile(testFile, results);\n if (updated) {\n console.error(pc.yellow(` ↻ Updated: ${changes.join(', ')}`));\n }\n }\n }\n\n // Summary\n const summary: TestRunSummary = {\n files: fileResults,\n totalPassed: fileResults.reduce((sum, f) => sum + f.results.filter((r) => r.passed).length, 0),\n totalFailed: fileResults.reduce((sum, f) => sum + f.results.filter((r) => !r.passed).length, 0),\n totalBlocks: fileResults.reduce((sum, f) => sum + f.results.length, 0),\n duration: Date.now() - startTime,\n };\n\n reportSummary(summary, opts);\n\n // Exit code\n process.exit(summary.totalFailed > 0 ? 1 : 0);\n}\n","/**\n * Readme command - Display the README documentation.\n *\n * Shows the package README.md, formatted for the terminal when interactive,\n * or as plain text when piped.\n */\n\nimport type { Command } from 'commander';\n\nimport { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport pc from 'picocolors';\n\n/**\n * Get the path to the README.md file.\n * Works both during development and when installed as a package.\n */\nfunction getReadmePath(): string {\n const thisDir = dirname(fileURLToPath(import.meta.url));\n const dirName = thisDir.split(/[/\\\\]/).pop();\n\n if (dirName === 'dist') {\n // Bundled: dist -> package root -> README.md\n return join(dirname(thisDir), 'README.md');\n }\n\n // Development: src/cli/commands -> src/cli -> src -> package root -> README.md\n return join(dirname(dirname(dirname(thisDir))), 'README.md');\n}\n\n/**\n * Load the README content.\n */\nfunction loadReadme(): string {\n const readmePath = getReadmePath();\n try {\n return readFileSync(readmePath, 'utf-8');\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(`Failed to load README from ${readmePath}: ${message}`);\n }\n}\n\n/**\n * Apply basic terminal formatting to markdown content.\n * Colorizes headers, code blocks, and other elements for better readability.\n */\nfunction formatMarkdown(content: string, useColors: boolean): string {\n if (!useColors) {\n return content;\n }\n\n const lines = content.split('\\n');\n const formatted: string[] = [];\n let inCodeBlock = false;\n\n for (const line of lines) {\n // Track code blocks\n if (line.startsWith('```')) {\n inCodeBlock = !inCodeBlock;\n formatted.push(pc.dim(line));\n continue;\n }\n\n if (inCodeBlock) {\n formatted.push(pc.dim(line));\n continue;\n }\n\n // Headers\n if (line.startsWith('# ')) {\n formatted.push(pc.bold(pc.cyan(line)));\n continue;\n }\n if (line.startsWith('## ')) {\n formatted.push(pc.bold(pc.blue(line)));\n continue;\n }\n if (line.startsWith('### ')) {\n formatted.push(pc.bold(line));\n continue;\n }\n\n // Inline code (backticks)\n let formattedLine = line.replace(/`([^`]+)`/g, (_match, code: string) => {\n return pc.yellow(code);\n });\n\n // Bold text\n formattedLine = formattedLine.replace(/\\*\\*([^*]+)\\*\\*/g, (_match, text: string) => {\n return pc.bold(text);\n });\n\n // Links - show text in cyan, URL dimmed\n formattedLine = formattedLine.replace(\n /\\[([^\\]]+)\\]\\(([^)]+)\\)/g,\n (_match, text: string, url: string) => {\n return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;\n },\n );\n\n formatted.push(formattedLine);\n }\n\n return formatted.join('\\n');\n}\n\n/**\n * Check if stdout is an interactive terminal.\n */\nfunction isInteractive(): boolean {\n return process.stdout.isTTY === true;\n}\n\n/**\n * Register the readme command.\n */\nexport function registerReadmeCommand(program: Command): void {\n program\n .command('readme')\n .description('Display README documentation')\n .option('--raw', 'Output raw markdown without formatting')\n .action((options: { raw?: boolean }) => {\n try {\n const readme = loadReadme();\n\n // Determine if we should colorize\n const shouldColorize = !options.raw && isInteractive();\n\n const formatted = formatMarkdown(readme, shouldColorize);\n console.log(formatted);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(pc.red(`Error: ${message}`));\n process.exit(1);\n }\n });\n}\n","/**\n * Docs command - Display the tryscript quick reference.\n *\n * Shows the tryscript-reference.md file, formatted for the terminal when interactive,\n * or as plain text when piped.\n */\n\nimport type { Command } from 'commander';\n\nimport { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport pc from 'picocolors';\n\n/**\n * Get the path to the tryscript-reference.md file.\n * Works both during development and when installed as a package.\n */\nfunction getDocsPath(): string {\n const thisDir = dirname(fileURLToPath(import.meta.url));\n const dirName = thisDir.split(/[/\\\\]/).pop();\n\n if (dirName === 'dist') {\n // Bundled: dist -> package root -> docs/tryscript-reference.md\n return join(dirname(thisDir), 'docs', 'tryscript-reference.md');\n }\n\n // Development: src/cli/commands -> src/cli -> src -> package root -> docs/tryscript-reference.md\n return join(dirname(dirname(dirname(thisDir))), 'docs', 'tryscript-reference.md');\n}\n\n/**\n * Load the docs content.\n */\nfunction loadDocs(): string {\n const docsPath = getDocsPath();\n try {\n return readFileSync(docsPath, 'utf-8');\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(`Failed to load reference docs from ${docsPath}: ${message}`);\n }\n}\n\n/**\n * Apply basic terminal formatting to markdown content.\n * Colorizes headers, code blocks, and other elements for better readability.\n */\nfunction formatMarkdown(content: string, useColors: boolean): string {\n if (!useColors) {\n return content;\n }\n\n const lines = content.split('\\n');\n const formatted: string[] = [];\n let inCodeBlock = false;\n\n for (const line of lines) {\n // Track code blocks\n if (line.startsWith('```')) {\n inCodeBlock = !inCodeBlock;\n formatted.push(pc.dim(line));\n continue;\n }\n\n if (inCodeBlock) {\n formatted.push(pc.dim(line));\n continue;\n }\n\n // Headers\n if (line.startsWith('# ')) {\n formatted.push(pc.bold(pc.cyan(line)));\n continue;\n }\n if (line.startsWith('## ')) {\n formatted.push(pc.bold(pc.blue(line)));\n continue;\n }\n if (line.startsWith('### ')) {\n formatted.push(pc.bold(line));\n continue;\n }\n\n // Inline code (backticks)\n let formattedLine = line.replace(/`([^`]+)`/g, (_match, code: string) => {\n return pc.yellow(code);\n });\n\n // Bold text\n formattedLine = formattedLine.replace(/\\*\\*([^*]+)\\*\\*/g, (_match, text: string) => {\n return pc.bold(text);\n });\n\n // Links - show text in cyan, URL dimmed\n formattedLine = formattedLine.replace(\n /\\[([^\\]]+)\\]\\(([^)]+)\\)/g,\n (_match, text: string, url: string) => {\n return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;\n },\n );\n\n formatted.push(formattedLine);\n }\n\n return formatted.join('\\n');\n}\n\n/**\n * Check if stdout is an interactive terminal.\n */\nfunction isInteractive(): boolean {\n return process.stdout.isTTY === true;\n}\n\n/**\n * Register the docs command.\n */\nexport function registerDocsCommand(program: Command): void {\n program\n .command('docs')\n .description('Display concise syntax reference')\n .option('--raw', 'Output raw markdown without formatting')\n .action((options: { raw?: boolean }) => {\n try {\n const docs = loadDocs();\n\n // Determine if we should colorize\n const shouldColorize = !options.raw && isInteractive();\n\n const formatted = formatMarkdown(docs, shouldColorize);\n console.log(formatted);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(pc.red(`Error: ${message}`));\n process.exit(1);\n }\n });\n}\n","import { Command } from 'commander';\nimport pc from 'picocolors';\nimport { VERSION } from '../index.js';\nimport { runCommand } from './commands/run.js';\nimport { registerReadmeCommand } from './commands/readme.js';\nimport { registerDocsCommand } from './commands/docs.js';\n\nexport function run(argv: string[]): void {\n const program = new Command()\n .name('tryscript')\n .version(VERSION, '--version', 'Show version number')\n .description('Golden testing for CLI applications')\n .showHelpAfterError('(use --help for usage)')\n .argument('[files...]', 'Test files to run (default: **/*.tryscript.md)')\n .option('--update', 'Update golden files with actual output')\n .option('--diff', 'Show diff on failure (default: true)')\n .option('--no-diff', 'Hide diff on failure')\n .option('--fail-fast', 'Stop on first failure')\n .option('--filter <pattern>', 'Filter tests by name pattern')\n .option('--verbose', 'Show detailed output including passing test output')\n .option('--quiet', 'Suppress non-essential output (only show failures)')\n .action(runCommand);\n\n // Register subcommands\n registerReadmeCommand(program);\n registerDocsCommand(program);\n\n program.parseAsync(argv).catch((err: Error) => {\n console.error(pc.red(`Error: ${err.message}`));\n process.exit(2);\n });\n}\n","#!/usr/bin/env node\nimport { run } from './cli/cli.js';\n\nrun(process.argv);\n"],"mappings":"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"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
const require_src = require('./src-CeUA446P.cjs');
|
|
3
|
+
|
|
4
|
+
exports.VERSION = require_src.VERSION;
|
|
5
|
+
exports.cleanupExecutionContext = require_src.cleanupExecutionContext;
|
|
6
|
+
exports.createExecutionContext = require_src.createExecutionContext;
|
|
7
|
+
exports.defineConfig = require_src.defineConfig;
|
|
8
|
+
exports.matchOutput = require_src.matchOutput;
|
|
9
|
+
exports.normalizeOutput = require_src.normalizeOutput;
|
|
10
|
+
exports.parseTestFile = require_src.parseTestFile;
|
|
11
|
+
exports.runBlock = require_src.runBlock;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/lib/types.d.ts
|
|
5
|
+
declare const TestConfigSchema: z.ZodObject<{
|
|
6
|
+
bin: z.ZodOptional<z.ZodString>;
|
|
7
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
8
|
+
timeout: z.ZodOptional<z.ZodNumber>;
|
|
9
|
+
patterns: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodString, z.ZodType<RegExp, z.ZodTypeDef, RegExp>]>>>;
|
|
10
|
+
tests: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
11
|
+
}, "strip", z.ZodTypeAny, {
|
|
12
|
+
bin?: string | undefined;
|
|
13
|
+
env?: Record<string, string> | undefined;
|
|
14
|
+
timeout?: number | undefined;
|
|
15
|
+
patterns?: Record<string, string | RegExp> | undefined;
|
|
16
|
+
tests?: string[] | undefined;
|
|
17
|
+
}, {
|
|
18
|
+
bin?: string | undefined;
|
|
19
|
+
env?: Record<string, string> | undefined;
|
|
20
|
+
timeout?: number | undefined;
|
|
21
|
+
patterns?: Record<string, string | RegExp> | undefined;
|
|
22
|
+
tests?: string[] | undefined;
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* Configuration for a test file or global config.
|
|
26
|
+
*/
|
|
27
|
+
type TestConfig = z.infer<typeof TestConfigSchema>;
|
|
28
|
+
/**
|
|
29
|
+
* A single command block within a test file.
|
|
30
|
+
*/
|
|
31
|
+
interface TestBlock {
|
|
32
|
+
/** Optional test name from preceding markdown heading */
|
|
33
|
+
name?: string;
|
|
34
|
+
/** The command to execute (may span multiple lines with > continuation) */
|
|
35
|
+
command: string;
|
|
36
|
+
/** Expected output (may include elision patterns) */
|
|
37
|
+
expectedOutput: string;
|
|
38
|
+
/** Expected exit code (default: 0) */
|
|
39
|
+
expectedExitCode: number;
|
|
40
|
+
/** Line number where this block starts (1-indexed, for error reporting) */
|
|
41
|
+
lineNumber: number;
|
|
42
|
+
/** Raw content of the block for update mode */
|
|
43
|
+
rawContent: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A parsed test file with all its blocks.
|
|
47
|
+
*/
|
|
48
|
+
interface TestFile {
|
|
49
|
+
/** Absolute path to the test file */
|
|
50
|
+
path: string;
|
|
51
|
+
/** Merged configuration (global + frontmatter) */
|
|
52
|
+
config: TestConfig;
|
|
53
|
+
/** Parsed test blocks in order */
|
|
54
|
+
blocks: TestBlock[];
|
|
55
|
+
/** Raw file content for update mode */
|
|
56
|
+
rawContent: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Result of running a single test block.
|
|
60
|
+
*/
|
|
61
|
+
interface TestBlockResult {
|
|
62
|
+
block: TestBlock;
|
|
63
|
+
passed: boolean;
|
|
64
|
+
actualOutput: string;
|
|
65
|
+
actualExitCode: number;
|
|
66
|
+
/** Diff if test failed (unified diff format) */
|
|
67
|
+
diff?: string;
|
|
68
|
+
/** Duration in milliseconds */
|
|
69
|
+
duration: number;
|
|
70
|
+
/** Error message if execution failed */
|
|
71
|
+
error?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Result of running all blocks in a test file.
|
|
75
|
+
*/
|
|
76
|
+
interface TestFileResult {
|
|
77
|
+
file: TestFile;
|
|
78
|
+
results: TestBlockResult[];
|
|
79
|
+
passed: boolean;
|
|
80
|
+
/** Total duration in milliseconds */
|
|
81
|
+
duration: number;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Summary of running multiple test files.
|
|
85
|
+
*/
|
|
86
|
+
interface TestRunSummary {
|
|
87
|
+
files: TestFileResult[];
|
|
88
|
+
totalPassed: number;
|
|
89
|
+
totalFailed: number;
|
|
90
|
+
totalBlocks: number;
|
|
91
|
+
duration: number;
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/lib/config.d.ts
|
|
95
|
+
interface TryscriptConfig {
|
|
96
|
+
bin?: string;
|
|
97
|
+
env?: Record<string, string>;
|
|
98
|
+
timeout?: number;
|
|
99
|
+
patterns?: Record<string, RegExp | string>;
|
|
100
|
+
tests?: string[];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Helper for typed config files.
|
|
104
|
+
*/
|
|
105
|
+
declare function defineConfig(config: TryscriptConfig): TryscriptConfig;
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/lib/parser.d.ts
|
|
108
|
+
/**
|
|
109
|
+
* Parse a .tryscript.md file into structured test data.
|
|
110
|
+
*/
|
|
111
|
+
declare function parseTestFile(content: string, filePath: string): TestFile;
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/lib/runner.d.ts
|
|
114
|
+
/**
|
|
115
|
+
* Execution context for a test file.
|
|
116
|
+
* Created once per file, contains the temp directory.
|
|
117
|
+
*/
|
|
118
|
+
interface ExecutionContext {
|
|
119
|
+
/** Temporary directory for this test file (resolved, no symlinks) */
|
|
120
|
+
tempDir: string;
|
|
121
|
+
/** Directory containing the test file (for portable test commands) */
|
|
122
|
+
testDir: string;
|
|
123
|
+
/** Resolved binary path */
|
|
124
|
+
binPath: string;
|
|
125
|
+
/** Environment variables */
|
|
126
|
+
env: Record<string, string>;
|
|
127
|
+
/** Timeout per command */
|
|
128
|
+
timeout: number;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Create an execution context for a test file.
|
|
132
|
+
*/
|
|
133
|
+
declare function createExecutionContext(config: TryscriptConfig, testFilePath: string): Promise<ExecutionContext>;
|
|
134
|
+
/**
|
|
135
|
+
* Clean up execution context (remove temp directory).
|
|
136
|
+
*/
|
|
137
|
+
declare function cleanupExecutionContext(ctx: ExecutionContext): Promise<void>;
|
|
138
|
+
/**
|
|
139
|
+
* Run a single test block and return the result.
|
|
140
|
+
*/
|
|
141
|
+
declare function runBlock(block: TestBlock, ctx: ExecutionContext): Promise<TestBlockResult>;
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/lib/matcher.d.ts
|
|
144
|
+
/**
|
|
145
|
+
* Normalize actual output for comparison.
|
|
146
|
+
* - Remove ANSI escape codes (colors, etc.)
|
|
147
|
+
* - Normalize line endings to \n
|
|
148
|
+
* - Normalize paths (Windows backslashes to forward slashes)
|
|
149
|
+
* - Trim trailing whitespace from lines
|
|
150
|
+
* - Ensure single trailing newline
|
|
151
|
+
*/
|
|
152
|
+
declare function normalizeOutput(output: string): string;
|
|
153
|
+
/**
|
|
154
|
+
* Check if actual output matches expected pattern.
|
|
155
|
+
*/
|
|
156
|
+
declare function matchOutput(actual: string, expected: string, context: {
|
|
157
|
+
root: string;
|
|
158
|
+
cwd: string;
|
|
159
|
+
}, customPatterns?: Record<string, string | RegExp>): boolean;
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/index.d.ts
|
|
162
|
+
declare const VERSION: string;
|
|
163
|
+
//#endregion
|
|
164
|
+
export { type ExecutionContext, type TestBlock, type TestBlockResult, type TestConfig, type TestFile, type TestFileResult, type TestRunSummary, type TryscriptConfig, VERSION, cleanupExecutionContext, createExecutionContext, defineConfig, matchOutput, normalizeOutput, parseTestFile, runBlock };
|
|
165
|
+
//# sourceMappingURL=index.d.cts.map
|