tryscript 0.0.1 → 0.1.0
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/LICENSE +21 -0
- package/README.md +38 -188
- package/dist/bin.cjs +121 -32
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.mjs +120 -31
- package/dist/bin.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +72 -8
- package/dist/index.d.mts +72 -8
- package/dist/index.mjs +1 -1
- package/dist/{src-CeUA446P.cjs → src-CP4-Q-U5.cjs} +117 -15
- package/dist/src-CP4-Q-U5.cjs.map +1 -0
- package/dist/{src-UjaSQrqA.mjs → src-Ca6X7ul-.mjs} +114 -18
- package/dist/src-Ca6X7ul-.mjs.map +1 -0
- package/docs/tryscript-reference.md +362 -84
- package/package.json +17 -16
- package/dist/src-CeUA446P.cjs.map +0 -1
- package/dist/src-UjaSQrqA.mjs.map +0 -1
|
@@ -1,163 +1,441 @@
|
|
|
1
|
-
# tryscript
|
|
1
|
+
# tryscript Reference
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Complete reference for writing tryscript golden tests. This document covers all syntax,
|
|
4
|
+
configuration, and patterns needed to write accurate CLI tests on the first try.
|
|
4
5
|
|
|
5
|
-
##
|
|
6
|
+
## Overview
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
Tryscript is a markdown-based CLI golden testing format. Test files are markdown documents
|
|
9
|
+
with embedded console code blocks specifying commands and expected output.
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
**Design Philosophy:**
|
|
12
|
+
- **Shell delegation**: Commands run in a real shell with full shell features
|
|
13
|
+
- **Markdown-first**: Test files are valid markdown, readable as documentation
|
|
14
|
+
- **Output matching**: Patterns like `[..]` match variable output; they're not for commands
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
$ command
|
|
14
|
-
expected output
|
|
15
|
-
? exit_code
|
|
16
|
-
\`\`\`
|
|
17
|
-
```
|
|
16
|
+
## Quick Start Example
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
````markdown
|
|
19
|
+
---
|
|
20
|
+
sandbox: true
|
|
21
|
+
env:
|
|
22
|
+
NO_COLOR: "1"
|
|
23
|
+
---
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
# Test: Echo command
|
|
25
|
+
# Test: Basic echo
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
```console
|
|
25
28
|
$ echo "hello world"
|
|
26
29
|
hello world
|
|
27
30
|
? 0
|
|
28
|
-
\`\`\`
|
|
29
31
|
```
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
# Test: Command with variable output
|
|
34
|
+
|
|
35
|
+
```console
|
|
36
|
+
$ date +%Y
|
|
37
|
+
[..]
|
|
38
|
+
? 0
|
|
39
|
+
```
|
|
40
|
+
````
|
|
41
|
+
|
|
42
|
+
## Test File Structure
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
┌──────────────────────────────────────┐
|
|
46
|
+
│ --- │ YAML Frontmatter (optional)
|
|
47
|
+
│ env: │ - Configuration
|
|
48
|
+
│ MY_VAR: value │ - Environment variables
|
|
49
|
+
│ sandbox: true │ - Patterns
|
|
50
|
+
│ --- │
|
|
51
|
+
├──────────────────────────────────────┤
|
|
52
|
+
│ # Test: Description │ Test heading (# or ##)
|
|
53
|
+
│ │
|
|
54
|
+
│ ```console │ Test block
|
|
55
|
+
│ $ command --flag │ - Command starts with $
|
|
56
|
+
│ expected output │ - Expected stdout follows
|
|
57
|
+
│ ? 0 │ - Exit code (optional, default 0)
|
|
58
|
+
│ ``` │
|
|
59
|
+
└──────────────────────────────────────┘
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Command Block Syntax
|
|
32
63
|
|
|
33
|
-
|
|
64
|
+
```
|
|
65
|
+
$ command [arguments...] # Command to execute (required)
|
|
66
|
+
> continuation line # Multi-line command continuation
|
|
67
|
+
expected output # Expected stdout (line by line)
|
|
68
|
+
! stderr line # Expected stderr (when separating streams)
|
|
69
|
+
? exit_code # Expected exit code (default: 0)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Examples
|
|
34
73
|
|
|
74
|
+
**Simple command:**
|
|
75
|
+
```console
|
|
76
|
+
$ echo "hello"
|
|
77
|
+
hello
|
|
78
|
+
? 0
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Non-zero exit code:**
|
|
35
82
|
```console
|
|
36
83
|
$ exit 42
|
|
37
84
|
? 42
|
|
38
85
|
```
|
|
39
86
|
|
|
87
|
+
**Multi-line command:**
|
|
88
|
+
```console
|
|
89
|
+
$ ls -la | \
|
|
90
|
+
> grep ".md" | \
|
|
91
|
+
> wc -l
|
|
92
|
+
5
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Stderr handling:**
|
|
96
|
+
```console
|
|
97
|
+
$ cat nonexistent 2>&1
|
|
98
|
+
cat: nonexistent: No such file or directory
|
|
99
|
+
? 1
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Separate stderr assertion:**
|
|
103
|
+
```console
|
|
104
|
+
$ ./script.sh
|
|
105
|
+
stdout line
|
|
106
|
+
! stderr line
|
|
107
|
+
? 0
|
|
108
|
+
```
|
|
109
|
+
|
|
40
110
|
## Elision Patterns
|
|
41
111
|
|
|
42
|
-
|
|
43
|
-
| -------- | -------------------------------- | --------------------- |
|
|
44
|
-
| `[..]` | Any characters on current line | `Built in [..]ms` |
|
|
45
|
-
| `...` | Zero or more complete lines | `...\nDone` |
|
|
46
|
-
| `[EXE]` | `.exe` on Windows, empty on Unix | `my-cli[EXE]` |
|
|
47
|
-
| `[ROOT]` | Test root directory | `[ROOT]/output.txt` |
|
|
48
|
-
| `[CWD]` | Current working directory | `[CWD]/file.txt` |
|
|
112
|
+
Patterns in expected output match variable content:
|
|
49
113
|
|
|
50
|
-
|
|
114
|
+
| Pattern | Matches | Example |
|
|
115
|
+
|---------|---------|---------|
|
|
116
|
+
| `[..]` | Any text on a single line | `Built in [..]ms` |
|
|
117
|
+
| `...` | Zero or more complete lines | `...\nDone` |
|
|
118
|
+
| `[CWD]` | Current working directory | `[CWD]/output.txt` |
|
|
119
|
+
| `[ROOT]` | Test file directory | `[ROOT]/fixtures/` |
|
|
120
|
+
| `[EXE]` | `.exe` on Windows, empty otherwise | `my-cli[EXE]` |
|
|
121
|
+
| `[PATTERN]` | Custom pattern from config | User-defined regex |
|
|
51
122
|
|
|
52
|
-
|
|
123
|
+
### Pattern Examples
|
|
124
|
+
|
|
125
|
+
**Single-line wildcard:**
|
|
126
|
+
```console
|
|
127
|
+
$ date
|
|
128
|
+
[..]
|
|
129
|
+
? 0
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Multi-line wildcard:**
|
|
133
|
+
```console
|
|
134
|
+
$ ls -la
|
|
135
|
+
total [..]
|
|
136
|
+
...
|
|
137
|
+
-rw-r--r-- 1 user user [..] README.md
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Custom pattern:**
|
|
141
|
+
```yaml
|
|
142
|
+
patterns:
|
|
143
|
+
VERSION: '\d+\.\d+\.\d+'
|
|
144
|
+
```
|
|
145
|
+
```console
|
|
146
|
+
$ my-cli --version
|
|
147
|
+
my-cli version [VERSION]
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Configuration (Frontmatter)
|
|
151
|
+
|
|
152
|
+
All options are optional. Place at the top of the file:
|
|
53
153
|
|
|
54
154
|
```yaml
|
|
55
155
|
---
|
|
56
|
-
|
|
57
|
-
|
|
156
|
+
cwd: ./subdir # Working directory (relative to test file)
|
|
157
|
+
sandbox: true # Run in isolated temp directory
|
|
158
|
+
env: # Environment variables
|
|
58
159
|
NO_COLOR: "1"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
160
|
+
MY_VAR: value
|
|
161
|
+
timeout: 5000 # Command timeout in milliseconds
|
|
162
|
+
patterns: # Custom elision patterns
|
|
163
|
+
UUID: '[0-9a-f]{8}-...'
|
|
164
|
+
fixtures: # Files to copy to sandbox
|
|
165
|
+
- data/input.txt
|
|
166
|
+
before: npm run build # Run before first test
|
|
167
|
+
after: rm -rf ./cache # Run after all tests
|
|
62
168
|
---
|
|
63
169
|
```
|
|
64
170
|
|
|
65
|
-
### Config Options
|
|
171
|
+
### Config Options Reference
|
|
172
|
+
|
|
173
|
+
| Option | Type | Default | Description |
|
|
174
|
+
|--------|------|---------|-------------|
|
|
175
|
+
| `cwd` | path | `"."` | Working directory (relative to test file) |
|
|
176
|
+
| `sandbox` | `boolean \| path` | `false` | Run in isolated temp directory |
|
|
177
|
+
| `env` | `object` | `{}` | Environment variables passed to shell |
|
|
178
|
+
| `timeout` | `number` | `30000` | Command timeout in milliseconds |
|
|
179
|
+
| `patterns` | `object` | `{}` | Custom regex patterns for `[NAME]` |
|
|
180
|
+
| `fixtures` | `array` | `[]` | Files to copy to sandbox |
|
|
181
|
+
| `before` | `string` | - | Shell command before first test |
|
|
182
|
+
| `after` | `string` | - | Shell command after all tests |
|
|
183
|
+
|
|
184
|
+
## Sandbox Mode
|
|
66
185
|
|
|
67
|
-
|
|
68
|
-
| ---------- | -------- | ------------------------------------- |
|
|
69
|
-
| `bin` | string | Path to CLI binary |
|
|
70
|
-
| `env` | object | Environment variables |
|
|
71
|
-
| `timeout` | number | Command timeout in milliseconds |
|
|
72
|
-
| `patterns` | object | Custom regex patterns for `[NAME]` |
|
|
186
|
+
Sandbox provides test isolation by running commands in a temporary directory:
|
|
73
187
|
|
|
74
|
-
|
|
188
|
+
| Configuration | Behavior |
|
|
189
|
+
|--------------|----------|
|
|
190
|
+
| `sandbox: false` (default) | Commands run in `cwd` (test file dir) |
|
|
191
|
+
| `sandbox: true` | Creates empty temp dir, commands run there |
|
|
192
|
+
| `sandbox: ./fixtures` | Copies `./fixtures/` to temp dir, runs there |
|
|
75
193
|
|
|
76
|
-
|
|
194
|
+
**When sandbox is enabled:**
|
|
195
|
+
- Fresh temp directory created for each test file
|
|
196
|
+
- Fixtures are copied before tests run
|
|
197
|
+
- `[CWD]` matches the sandbox directory
|
|
198
|
+
- Files created by tests don't pollute source
|
|
199
|
+
|
|
200
|
+
### Sandbox with Fixtures
|
|
77
201
|
|
|
78
202
|
```yaml
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
203
|
+
---
|
|
204
|
+
sandbox: true
|
|
205
|
+
fixtures:
|
|
206
|
+
- data/input.txt # Copies to sandbox/input.txt
|
|
207
|
+
- source: config/settings.json # Copies to sandbox/custom.json
|
|
208
|
+
dest: custom.json
|
|
209
|
+
---
|
|
82
210
|
```
|
|
83
211
|
|
|
84
|
-
|
|
212
|
+
## Environment Variables
|
|
213
|
+
|
|
214
|
+
Use `env` to set variables. The **shell** handles `$VAR` expansion:
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
env:
|
|
218
|
+
CLI: ./dist/cli.mjs
|
|
219
|
+
DEBUG: "true"
|
|
220
|
+
```
|
|
85
221
|
|
|
86
222
|
```console
|
|
87
|
-
$
|
|
223
|
+
$ $CLI --version
|
|
224
|
+
1.0.0
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Important:** Variables are for the shell, not for output matching.
|
|
228
|
+
|
|
229
|
+
## Test Annotations
|
|
230
|
+
|
|
231
|
+
Control test execution with HTML comments:
|
|
232
|
+
|
|
233
|
+
```markdown
|
|
234
|
+
## This test is skipped <!-- skip -->
|
|
235
|
+
|
|
236
|
+
## Only run this test <!-- only -->
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
| Annotation | Effect |
|
|
240
|
+
|------------|--------|
|
|
241
|
+
| `<!-- skip -->` | Test is skipped, marked as passed |
|
|
242
|
+
| `<!-- only -->` | Only tests with this annotation run |
|
|
243
|
+
|
|
244
|
+
## Complete Example
|
|
245
|
+
|
|
246
|
+
Here's a complete test file demonstrating all features:
|
|
247
|
+
|
|
248
|
+
````markdown
|
|
249
|
+
---
|
|
250
|
+
sandbox: true
|
|
251
|
+
env:
|
|
252
|
+
NO_COLOR: "1"
|
|
253
|
+
CLI: ./dist/my-cli.mjs
|
|
254
|
+
timeout: 5000
|
|
255
|
+
patterns:
|
|
256
|
+
VERSION: '\d+\.\d+\.\d+'
|
|
257
|
+
TIMESTAMP: '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}'
|
|
258
|
+
fixtures:
|
|
259
|
+
- test-data/config.json
|
|
260
|
+
before: echo "Setup complete"
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
# CLI Golden Tests
|
|
264
|
+
|
|
265
|
+
These tests validate the my-cli command-line tool.
|
|
266
|
+
|
|
267
|
+
## Basic Commands
|
|
268
|
+
|
|
269
|
+
# Test: Show version
|
|
270
|
+
|
|
271
|
+
```console
|
|
272
|
+
$ $CLI --version
|
|
88
273
|
my-cli version [VERSION]
|
|
89
274
|
? 0
|
|
90
275
|
```
|
|
91
276
|
|
|
92
|
-
|
|
277
|
+
# Test: Show help
|
|
93
278
|
|
|
94
|
-
|
|
279
|
+
```console
|
|
280
|
+
$ $CLI --help
|
|
281
|
+
Usage: my-cli [options] [command]
|
|
95
282
|
|
|
96
|
-
|
|
97
|
-
|
|
283
|
+
Options:
|
|
284
|
+
--version Show version
|
|
285
|
+
--help Show help
|
|
286
|
+
...
|
|
287
|
+
? 0
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Error Handling
|
|
291
|
+
|
|
292
|
+
# Test: Missing required argument
|
|
293
|
+
|
|
294
|
+
```console
|
|
295
|
+
$ $CLI process
|
|
296
|
+
Error: missing required argument 'file'
|
|
297
|
+
? 1
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
# Test: File not found
|
|
301
|
+
|
|
302
|
+
```console
|
|
303
|
+
$ $CLI process nonexistent.txt 2>&1
|
|
304
|
+
Error: file not found: nonexistent.txt
|
|
305
|
+
? 1
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Feature Tests
|
|
309
|
+
|
|
310
|
+
# Test: Process config file
|
|
98
311
|
|
|
99
|
-
|
|
100
|
-
$
|
|
101
|
-
|
|
312
|
+
```console
|
|
313
|
+
$ $CLI process config.json
|
|
314
|
+
Processing: config.json
|
|
315
|
+
Done at [TIMESTAMP][..]
|
|
102
316
|
? 0
|
|
103
|
-
|
|
317
|
+
```
|
|
104
318
|
|
|
105
|
-
# Test:
|
|
319
|
+
# Test: Verbose output <!-- skip -->
|
|
106
320
|
|
|
107
|
-
|
|
108
|
-
$
|
|
109
|
-
|
|
321
|
+
```console
|
|
322
|
+
$ $CLI --verbose process config.json
|
|
323
|
+
[DEBUG] Loading config.json
|
|
324
|
+
...
|
|
325
|
+
Done
|
|
110
326
|
? 0
|
|
111
|
-
\`\`\`
|
|
112
327
|
```
|
|
328
|
+
````
|
|
113
329
|
|
|
114
330
|
## CLI Usage
|
|
115
331
|
|
|
116
332
|
```bash
|
|
117
|
-
#
|
|
118
|
-
|
|
333
|
+
tryscript # Show help (same as --help)
|
|
334
|
+
tryscript run [files...] # Run golden tests
|
|
335
|
+
tryscript docs # Show this reference
|
|
336
|
+
tryscript readme # Show README
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Run Options
|
|
340
|
+
|
|
341
|
+
| Option | Description |
|
|
342
|
+
|--------|-------------|
|
|
343
|
+
| `--update` | Update test files with actual output |
|
|
344
|
+
| `--diff` / `--no-diff` | Show/hide diff on failure |
|
|
345
|
+
| `--fail-fast` | Stop on first failure |
|
|
346
|
+
| `--filter <pattern>` | Filter tests by name |
|
|
347
|
+
| `--verbose` | Show detailed output |
|
|
348
|
+
| `--quiet` | Suppress non-essential output |
|
|
119
349
|
|
|
120
|
-
|
|
121
|
-
|
|
350
|
+
## Best Practices
|
|
351
|
+
|
|
352
|
+
### DO: Use shell features directly
|
|
353
|
+
|
|
354
|
+
```console
|
|
355
|
+
$ echo "hello" | tr 'a-z' 'A-Z'
|
|
356
|
+
HELLO
|
|
357
|
+
|
|
358
|
+
$ cat file.txt 2>/dev/null || echo "not found"
|
|
359
|
+
not found
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### DO: Use env for CLI paths
|
|
363
|
+
|
|
364
|
+
```yaml
|
|
365
|
+
env:
|
|
366
|
+
BIN: ./dist/cli.mjs
|
|
367
|
+
```
|
|
368
|
+
```console
|
|
369
|
+
$ $BIN --version
|
|
370
|
+
1.0.0
|
|
371
|
+
```
|
|
122
372
|
|
|
123
|
-
|
|
124
|
-
npx tryscript --update
|
|
373
|
+
### DO: Use sandbox for file operations
|
|
125
374
|
|
|
126
|
-
|
|
127
|
-
|
|
375
|
+
```yaml
|
|
376
|
+
sandbox: true
|
|
377
|
+
```
|
|
378
|
+
```console
|
|
379
|
+
$ echo "test" > output.txt
|
|
380
|
+
$ cat output.txt
|
|
381
|
+
test
|
|
382
|
+
```
|
|
128
383
|
|
|
129
|
-
|
|
130
|
-
npx tryscript --fail-fast
|
|
384
|
+
### DON'T: Use patterns in commands
|
|
131
385
|
|
|
132
|
-
|
|
133
|
-
|
|
386
|
+
```console
|
|
387
|
+
# ❌ WRONG: Patterns are for output matching only
|
|
388
|
+
$ cat [CWD]/file.txt
|
|
134
389
|
```
|
|
135
390
|
|
|
136
|
-
|
|
391
|
+
### DON'T: Rely on exact timestamps or paths
|
|
137
392
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
393
|
+
```console
|
|
394
|
+
# ❌ WRONG: Exact match will fail
|
|
395
|
+
$ date
|
|
396
|
+
Mon Jan 3 12:34:56 UTC 2026
|
|
397
|
+
|
|
398
|
+
# ✓ RIGHT: Use elision
|
|
399
|
+
$ date
|
|
400
|
+
[..]
|
|
401
|
+
```
|
|
147
402
|
|
|
148
403
|
## Config File
|
|
149
404
|
|
|
150
|
-
|
|
405
|
+
For project-wide settings, create `tryscript.config.ts`:
|
|
151
406
|
|
|
152
407
|
```typescript
|
|
153
408
|
import { defineConfig } from 'tryscript';
|
|
154
409
|
|
|
155
410
|
export default defineConfig({
|
|
156
|
-
bin: './dist/cli.js',
|
|
157
411
|
env: { NO_COLOR: '1' },
|
|
158
412
|
timeout: 30000,
|
|
159
413
|
patterns: {
|
|
160
414
|
VERSION: '\\d+\\.\\d+\\.\\d+',
|
|
415
|
+
UUID: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
|
|
161
416
|
},
|
|
162
417
|
});
|
|
163
418
|
```
|
|
419
|
+
|
|
420
|
+
## Execution Model
|
|
421
|
+
|
|
422
|
+
```
|
|
423
|
+
Test File → Parse YAML + Blocks → Create Execution Context
|
|
424
|
+
│
|
|
425
|
+
┌────────────────────┴────────────────────┐
|
|
426
|
+
│ │
|
|
427
|
+
sandbox: false sandbox: true
|
|
428
|
+
cwd = testDir/config.cwd cwd = /tmp/tryscript-xxx/
|
|
429
|
+
│ │
|
|
430
|
+
└────────────────────┬────────────────────┘
|
|
431
|
+
│
|
|
432
|
+
spawn(command, { shell: true, cwd, env })
|
|
433
|
+
│
|
|
434
|
+
Capture stdout + stderr → Match against expected
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
**Key points:**
|
|
438
|
+
1. Commands run in a real shell (`shell: true`)
|
|
439
|
+
2. Shell handles all variable expansion (`$VAR`)
|
|
440
|
+
3. Patterns (`[..]`, `[CWD]`) only apply to output matching
|
|
441
|
+
4. Sandbox creates isolated temp directory per test file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tryscript",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Golden testing for CLI applications - TypeScript port of trycmd",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Joshua Levy",
|
|
@@ -37,20 +37,6 @@
|
|
|
37
37
|
"engines": {
|
|
38
38
|
"node": ">=20"
|
|
39
39
|
},
|
|
40
|
-
"scripts": {
|
|
41
|
-
"build": "tsdown",
|
|
42
|
-
"dev": "tsdown --watch",
|
|
43
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
44
|
-
"test": "vitest run",
|
|
45
|
-
"test:watch": "vitest",
|
|
46
|
-
"test:golden": "node dist/bin.mjs 'tests/**/*.tryscript.md'",
|
|
47
|
-
"test:golden:coverage": "c8 --src src --all --include 'dist/**' --reporter text --reporter html --reports-dir coverage-golden node dist/bin.mjs 'tests/**/*.tryscript.md'",
|
|
48
|
-
"test:coverage": "vitest run --coverage",
|
|
49
|
-
"test:all:coverage": "pnpm test:golden:coverage && pnpm test:coverage",
|
|
50
|
-
"publint": "publint",
|
|
51
|
-
"prepack": "pnpm build",
|
|
52
|
-
"tryscript": "tsx src/bin.ts"
|
|
53
|
-
},
|
|
54
40
|
"dependencies": {
|
|
55
41
|
"atomically": "^2.0.0",
|
|
56
42
|
"commander": "^14.0.0",
|
|
@@ -72,5 +58,20 @@
|
|
|
72
58
|
"tsx": "^4.21.0",
|
|
73
59
|
"typescript": "^5.0.0",
|
|
74
60
|
"vitest": "^4.0.0"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"copy-docs": "cp ../../README.md . && mkdir -p docs && cp ../../docs/tryscript-reference.md docs/",
|
|
64
|
+
"build": "pnpm copy-docs && tsdown",
|
|
65
|
+
"dev": "tsdown --watch",
|
|
66
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
67
|
+
"test": "vitest run",
|
|
68
|
+
"test:watch": "vitest",
|
|
69
|
+
"test:self": "tsx src/bin.ts",
|
|
70
|
+
"test:golden": "node dist/bin.mjs run 'tests/**/*.tryscript.md'",
|
|
71
|
+
"test:golden:coverage": "c8 --src src --all --include 'dist/**' --reporter text --reporter html --reports-dir coverage-golden node dist/bin.mjs run 'tests/**/*.tryscript.md'",
|
|
72
|
+
"test:coverage": "vitest run --coverage",
|
|
73
|
+
"test:all:coverage": "pnpm test:golden:coverage && pnpm test:coverage",
|
|
74
|
+
"publint": "publint",
|
|
75
|
+
"tryscript": "tsx src/bin.ts"
|
|
75
76
|
}
|
|
76
|
-
}
|
|
77
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"src-CeUA446P.cjs","names":["module","config: TestConfig","blocks: TestBlock[]","match: RegExpExecArray | null","commandLines: string[]","outputLines: string[]","chunks: Buffer[]","VERSION: string"],"sources":["../src/lib/config.ts","../src/lib/parser.ts","../src/lib/runner.ts","../src/lib/matcher.ts","../src/index.ts"],"sourcesContent":["import { pathToFileURL } from 'node:url';\nimport { existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport type { TestConfig } from './types.js';\n\nexport interface TryscriptConfig {\n bin?: string;\n env?: Record<string, string>;\n timeout?: number;\n patterns?: Record<string, RegExp | string>;\n tests?: string[];\n}\n\nconst CONFIG_FILES = ['tryscript.config.ts', 'tryscript.config.js', 'tryscript.config.mjs'];\n\n/**\n * Load config file using dynamic import.\n * Supports both TypeScript (via tsx/ts-node) and JavaScript configs.\n */\nexport async function loadConfig(baseDir: string): Promise<TryscriptConfig> {\n for (const filename of CONFIG_FILES) {\n const configPath = resolve(baseDir, filename);\n if (existsSync(configPath)) {\n const configUrl = pathToFileURL(configPath).href;\n const module = (await import(configUrl)) as { default?: TryscriptConfig } | TryscriptConfig;\n return (module as { default?: TryscriptConfig }).default ?? (module as TryscriptConfig);\n }\n }\n return {};\n}\n\n/**\n * Merge config with frontmatter overrides.\n * Frontmatter takes precedence over config file.\n */\nexport function mergeConfig(base: TryscriptConfig, frontmatter: TestConfig): TryscriptConfig {\n return {\n ...base,\n ...frontmatter,\n env: { ...base.env, ...frontmatter.env },\n patterns: { ...base.patterns, ...frontmatter.patterns },\n };\n}\n\n/**\n * Helper for typed config files.\n */\nexport function defineConfig(config: TryscriptConfig): TryscriptConfig {\n return config;\n}\n","import { parse as parseYaml } from 'yaml';\nimport type { TestConfig, TestBlock, TestFile } from './types.js';\n\n/** Regex to match YAML frontmatter at the start of a file */\nconst FRONTMATTER_REGEX = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n/;\n\n/** Regex to match fenced code blocks with console/bash info string */\nconst CODE_BLOCK_REGEX = /```(console|bash)\\r?\\n([\\s\\S]*?)```/g;\n\n/** Regex to match markdown headings (for test names) */\nconst HEADING_REGEX = /^#+\\s+(?:Test:\\s*)?(.+)$/m;\n\n/**\n * Parse a .tryscript.md file into structured test data.\n */\nexport function parseTestFile(content: string, filePath: string): TestFile {\n const rawContent = content;\n let config: TestConfig = {};\n let body = content;\n\n // Extract frontmatter if present\n const frontmatterMatch = FRONTMATTER_REGEX.exec(content);\n if (frontmatterMatch) {\n const yamlContent = frontmatterMatch[1] ?? '';\n config = parseYaml(yamlContent) as TestConfig;\n body = content.slice(frontmatterMatch[0].length);\n }\n\n // Parse all console blocks\n const blocks: TestBlock[] = [];\n\n // Reset regex lastIndex\n CODE_BLOCK_REGEX.lastIndex = 0;\n\n let match: RegExpExecArray | null;\n while ((match = CODE_BLOCK_REGEX.exec(body)) !== null) {\n const blockContent = match[2] ?? '';\n const blockStart = match.index;\n\n // Find the line number (1-indexed)\n const precedingContent = content.slice(0, content.indexOf(match[0]));\n const lineNumber = precedingContent.split('\\n').length;\n\n // Look for a heading before this block (for test name)\n const contentBefore = body.slice(0, blockStart);\n const lastHeadingMatch = [\n ...contentBefore.matchAll(new RegExp(HEADING_REGEX.source, 'gm')),\n ].pop();\n const name = lastHeadingMatch?.[1]?.trim();\n\n // Parse the block content\n const parsed = parseBlockContent(blockContent);\n if (parsed) {\n blocks.push({\n name,\n command: parsed.command,\n expectedOutput: parsed.expectedOutput,\n expectedExitCode: parsed.expectedExitCode,\n lineNumber,\n rawContent: match[0],\n });\n }\n }\n\n return { path: filePath, config, blocks, rawContent };\n}\n\n/**\n * Parse the content of a single console block.\n */\nfunction parseBlockContent(content: string): {\n command: string;\n expectedOutput: string;\n expectedExitCode: number;\n} | null {\n const lines = content.split('\\n');\n const commandLines: string[] = [];\n const outputLines: string[] = [];\n let expectedExitCode = 0;\n let inCommand = false;\n\n for (const line of lines) {\n if (line.startsWith('$ ')) {\n // Start of a command\n inCommand = true;\n commandLines.push(line.slice(2));\n } else if (line.startsWith('> ') && inCommand) {\n // Continuation of a multi-line command\n commandLines.push(line.slice(2));\n } else if (line.startsWith('? ')) {\n // Exit code specification\n inCommand = false;\n expectedExitCode = parseInt(line.slice(2).trim(), 10);\n } else {\n // Output line\n inCommand = false;\n outputLines.push(line);\n }\n }\n\n if (commandLines.length === 0) {\n return null;\n }\n\n // Join command lines, handling shell continuations\n let command = '';\n for (let i = 0; i < commandLines.length; i++) {\n const line = commandLines[i] ?? '';\n if (line.endsWith('\\\\')) {\n command += line.slice(0, -1) + ' ';\n } else {\n command += line;\n if (i < commandLines.length - 1) {\n command += ' ';\n }\n }\n }\n\n // Join output lines, preserving blank lines but trimming trailing empty lines\n let expectedOutput = outputLines.join('\\n');\n expectedOutput = expectedOutput.replace(/\\n+$/, '');\n if (expectedOutput) {\n expectedOutput += '\\n';\n }\n\n return { command: command.trim(), expectedOutput, expectedExitCode };\n}\n","import { spawn } from 'node:child_process';\nimport { mkdtemp, realpath, rm } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join, dirname, resolve } from 'node:path';\nimport treeKill from 'tree-kill';\nimport type { TestBlock, TestBlockResult } from './types.js';\nimport type { TryscriptConfig } from './config.js';\n\n/** Default timeout in milliseconds */\nconst DEFAULT_TIMEOUT = 30_000;\n\n/**\n * Execution context for a test file.\n * Created once per file, contains the temp directory.\n */\nexport interface ExecutionContext {\n /** Temporary directory for this test file (resolved, no symlinks) */\n tempDir: string;\n /** Directory containing the test file (for portable test commands) */\n testDir: string;\n /** Resolved binary path */\n binPath: string;\n /** Environment variables */\n env: Record<string, string>;\n /** Timeout per command */\n timeout: number;\n}\n\n/**\n * Create an execution context for a test file.\n */\nexport async function createExecutionContext(\n config: TryscriptConfig,\n testFilePath: string,\n): Promise<ExecutionContext> {\n // Create temp directory and resolve symlinks (e.g., /var -> /private/var on macOS)\n // This ensures [CWD] and [ROOT] patterns match pwd output\n const rawTempDir = await mkdtemp(join(tmpdir(), 'tryscript-'));\n const tempDir = await realpath(rawTempDir);\n\n // Resolve test file directory for portable test commands\n const testDir = resolve(dirname(testFilePath));\n\n // Resolve binary path relative to test file directory\n let binPath = config.bin ?? '';\n if (binPath && !binPath.startsWith('/')) {\n binPath = join(testDir, binPath);\n }\n\n return {\n tempDir,\n testDir,\n binPath,\n env: {\n ...process.env,\n ...config.env,\n // Disable colors by default for deterministic output\n NO_COLOR: config.env?.NO_COLOR ?? '1',\n FORCE_COLOR: '0',\n // Provide test directory for portable test commands\n TRYSCRIPT_TEST_DIR: testDir,\n } as Record<string, string>,\n timeout: config.timeout ?? DEFAULT_TIMEOUT,\n };\n}\n\n/**\n * Clean up execution context (remove temp directory).\n */\nexport async function cleanupExecutionContext(ctx: ExecutionContext): Promise<void> {\n await rm(ctx.tempDir, { recursive: true, force: true });\n}\n\n/**\n * Run a single test block and return the result.\n */\nexport async function runBlock(block: TestBlock, ctx: ExecutionContext): Promise<TestBlockResult> {\n const startTime = Date.now();\n\n try {\n const { output, exitCode } = await executeCommand(block.command, ctx);\n\n const duration = Date.now() - startTime;\n\n return {\n block,\n passed: true, // Matching handled separately\n actualOutput: output,\n actualExitCode: exitCode,\n duration,\n };\n } catch (error) {\n const duration = Date.now() - startTime;\n const message = error instanceof Error ? error.message : String(error);\n\n return {\n block,\n passed: false,\n actualOutput: '',\n actualExitCode: -1,\n duration,\n error: message,\n };\n }\n}\n\n/**\n * Execute a command and capture output.\n */\nasync function executeCommand(\n command: string,\n ctx: ExecutionContext,\n): Promise<{ output: string; exitCode: number }> {\n return new Promise((resolve, reject) => {\n const proc = spawn(command, {\n shell: true,\n cwd: ctx.tempDir,\n env: ctx.env as NodeJS.ProcessEnv,\n // Pipe both to capture\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n const chunks: Buffer[] = [];\n\n // Capture data as it comes in to preserve order\n proc.stdout.on('data', (data: Buffer) => chunks.push(data));\n proc.stderr.on('data', (data: Buffer) => chunks.push(data));\n\n const timeoutId = setTimeout(() => {\n if (proc.pid) {\n treeKill(proc.pid, 'SIGKILL');\n }\n reject(new Error(`Command timed out after ${ctx.timeout}ms`));\n }, ctx.timeout);\n\n proc.on('close', (code) => {\n clearTimeout(timeoutId);\n const output = Buffer.concat(chunks).toString('utf-8');\n resolve({\n output,\n exitCode: code ?? 0,\n });\n });\n\n proc.on('error', (err) => {\n clearTimeout(timeoutId);\n reject(err);\n });\n });\n}\n","import stripAnsi from 'strip-ansi';\n\n/**\n * Escape special regex characters in a string.\n */\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n// Marker prefix for patterns (uses Unicode private use chars that won't appear in normal output)\nconst MARKER = '\\uE000';\n\n/**\n * Convert expected output with elision patterns to a regex.\n *\n * Handles (matching trycmd):\n * - [..] — matches any characters on the same line (trycmd: [^\\n]*?)\n * - ... — matches zero or more complete lines (trycmd: \\n(([^\\n]*\\n)*)?)\n * - [EXE] — matches .exe on Windows, empty otherwise\n * - [ROOT] — replaced with test root directory (pre-processed)\n * - [CWD] — replaced with current working directory (pre-processed)\n * - Custom [NAME] patterns from config (trycmd: TestCases::insert_var)\n */\nfunction patternToRegex(\n expected: string,\n customPatterns: Record<string, string | RegExp> = {},\n): RegExp {\n // Build a map of markers to their regex replacements\n const replacements = new Map<string, string>();\n let markerIndex = 0;\n\n const getMarker = (): string => {\n return `${MARKER}${markerIndex++}${MARKER}`;\n };\n\n let processed = expected;\n\n // Replace [..] with marker\n const dotdotMarker = getMarker();\n replacements.set(dotdotMarker, '[^\\\\n]*');\n processed = processed.replaceAll('[..]', dotdotMarker);\n\n // Replace ... (followed by newline) with marker\n const ellipsisMarker = getMarker();\n replacements.set(ellipsisMarker, '(?:[^\\\\n]*\\\\n)*');\n processed = processed.replace(/\\.\\.\\.\\n/g, ellipsisMarker);\n\n // Replace [EXE] with marker\n const exeMarker = getMarker();\n const exe = process.platform === 'win32' ? '\\\\.exe' : '';\n replacements.set(exeMarker, exe);\n processed = processed.replaceAll('[EXE]', exeMarker);\n\n // Replace custom patterns with markers\n for (const [name, pattern] of Object.entries(customPatterns)) {\n const placeholder = `[${name}]`;\n const patternStr = pattern instanceof RegExp ? pattern.source : pattern;\n const marker = getMarker();\n replacements.set(marker, `(${patternStr})`);\n processed = processed.replaceAll(placeholder, marker);\n }\n\n // Escape special regex characters\n let regex = escapeRegex(processed);\n\n // Restore markers to their regex replacements\n for (const [marker, replacement] of replacements) {\n regex = regex.replaceAll(escapeRegex(marker), replacement);\n }\n\n // Match the entire string (dotall mode for . to match newlines if needed)\n return new RegExp(`^${regex}$`, 's');\n}\n\n/**\n * Pre-process expected output to replace path placeholders with actual paths.\n * This happens BEFORE pattern matching.\n */\nfunction preprocessPaths(expected: string, context: { root: string; cwd: string }): string {\n let result = expected;\n // Normalize paths for comparison (use forward slashes)\n const normalizedRoot = context.root.replace(/\\\\/g, '/');\n const normalizedCwd = context.cwd.replace(/\\\\/g, '/');\n result = result.replaceAll('[ROOT]', normalizedRoot);\n result = result.replaceAll('[CWD]', normalizedCwd);\n return result;\n}\n\n/**\n * Normalize actual output for comparison.\n * - Remove ANSI escape codes (colors, etc.)\n * - Normalize line endings to \\n\n * - Normalize paths (Windows backslashes to forward slashes)\n * - Trim trailing whitespace from lines\n * - Ensure single trailing newline\n */\nexport function normalizeOutput(output: string): string {\n // Remove ANSI escape codes first\n let normalized = stripAnsi(output);\n\n normalized = normalized\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\r/g, '\\n')\n .split('\\n')\n .map((line) => line.trimEnd())\n .join('\\n')\n .replace(/\\n+$/, '\\n');\n\n // Handle empty output\n if (normalized === '\\n') {\n normalized = '';\n }\n\n return normalized;\n}\n\n/**\n * Check if actual output matches expected pattern.\n */\nexport function matchOutput(\n actual: string,\n expected: string,\n context: { root: string; cwd: string },\n customPatterns: Record<string, string | RegExp> = {},\n): boolean {\n const normalizedActual = normalizeOutput(actual);\n const normalizedExpected = normalizeOutput(expected);\n\n // Empty expected matches empty actual\n if (normalizedExpected === '' && normalizedActual === '') {\n return true;\n }\n\n const preprocessed = preprocessPaths(normalizedExpected, context);\n const regex = patternToRegex(preprocessed, customPatterns);\n return regex.test(normalizedActual);\n}\n","// Public API exports\n\n// Version constant (injected at build time)\ndeclare const __VERSION__: string;\nexport const VERSION: string = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'development';\n\n// Config helper\nexport { defineConfig } from './lib/config.js';\nexport type { TryscriptConfig } from './lib/config.js';\n\n// Types\nexport type {\n TestConfig,\n TestBlock,\n TestFile,\n TestBlockResult,\n TestFileResult,\n TestRunSummary,\n} from './lib/types.js';\n\n// Core functions (for programmatic use)\nexport { parseTestFile } from './lib/parser.js';\nexport { runBlock, createExecutionContext, cleanupExecutionContext } from './lib/runner.js';\nexport type { ExecutionContext } from './lib/runner.js';\nexport { matchOutput, normalizeOutput } from './lib/matcher.js';\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"src-UjaSQrqA.mjs","names":["config: TestConfig","parseYaml","blocks: TestBlock[]","match: RegExpExecArray | null","commandLines: string[]","outputLines: string[]","chunks: Buffer[]","VERSION: string"],"sources":["../src/lib/config.ts","../src/lib/parser.ts","../src/lib/runner.ts","../src/lib/matcher.ts","../src/index.ts"],"sourcesContent":["import { pathToFileURL } from 'node:url';\nimport { existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport type { TestConfig } from './types.js';\n\nexport interface TryscriptConfig {\n bin?: string;\n env?: Record<string, string>;\n timeout?: number;\n patterns?: Record<string, RegExp | string>;\n tests?: string[];\n}\n\nconst CONFIG_FILES = ['tryscript.config.ts', 'tryscript.config.js', 'tryscript.config.mjs'];\n\n/**\n * Load config file using dynamic import.\n * Supports both TypeScript (via tsx/ts-node) and JavaScript configs.\n */\nexport async function loadConfig(baseDir: string): Promise<TryscriptConfig> {\n for (const filename of CONFIG_FILES) {\n const configPath = resolve(baseDir, filename);\n if (existsSync(configPath)) {\n const configUrl = pathToFileURL(configPath).href;\n const module = (await import(configUrl)) as { default?: TryscriptConfig } | TryscriptConfig;\n return (module as { default?: TryscriptConfig }).default ?? (module as TryscriptConfig);\n }\n }\n return {};\n}\n\n/**\n * Merge config with frontmatter overrides.\n * Frontmatter takes precedence over config file.\n */\nexport function mergeConfig(base: TryscriptConfig, frontmatter: TestConfig): TryscriptConfig {\n return {\n ...base,\n ...frontmatter,\n env: { ...base.env, ...frontmatter.env },\n patterns: { ...base.patterns, ...frontmatter.patterns },\n };\n}\n\n/**\n * Helper for typed config files.\n */\nexport function defineConfig(config: TryscriptConfig): TryscriptConfig {\n return config;\n}\n","import { parse as parseYaml } from 'yaml';\nimport type { TestConfig, TestBlock, TestFile } from './types.js';\n\n/** Regex to match YAML frontmatter at the start of a file */\nconst FRONTMATTER_REGEX = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n/;\n\n/** Regex to match fenced code blocks with console/bash info string */\nconst CODE_BLOCK_REGEX = /```(console|bash)\\r?\\n([\\s\\S]*?)```/g;\n\n/** Regex to match markdown headings (for test names) */\nconst HEADING_REGEX = /^#+\\s+(?:Test:\\s*)?(.+)$/m;\n\n/**\n * Parse a .tryscript.md file into structured test data.\n */\nexport function parseTestFile(content: string, filePath: string): TestFile {\n const rawContent = content;\n let config: TestConfig = {};\n let body = content;\n\n // Extract frontmatter if present\n const frontmatterMatch = FRONTMATTER_REGEX.exec(content);\n if (frontmatterMatch) {\n const yamlContent = frontmatterMatch[1] ?? '';\n config = parseYaml(yamlContent) as TestConfig;\n body = content.slice(frontmatterMatch[0].length);\n }\n\n // Parse all console blocks\n const blocks: TestBlock[] = [];\n\n // Reset regex lastIndex\n CODE_BLOCK_REGEX.lastIndex = 0;\n\n let match: RegExpExecArray | null;\n while ((match = CODE_BLOCK_REGEX.exec(body)) !== null) {\n const blockContent = match[2] ?? '';\n const blockStart = match.index;\n\n // Find the line number (1-indexed)\n const precedingContent = content.slice(0, content.indexOf(match[0]));\n const lineNumber = precedingContent.split('\\n').length;\n\n // Look for a heading before this block (for test name)\n const contentBefore = body.slice(0, blockStart);\n const lastHeadingMatch = [\n ...contentBefore.matchAll(new RegExp(HEADING_REGEX.source, 'gm')),\n ].pop();\n const name = lastHeadingMatch?.[1]?.trim();\n\n // Parse the block content\n const parsed = parseBlockContent(blockContent);\n if (parsed) {\n blocks.push({\n name,\n command: parsed.command,\n expectedOutput: parsed.expectedOutput,\n expectedExitCode: parsed.expectedExitCode,\n lineNumber,\n rawContent: match[0],\n });\n }\n }\n\n return { path: filePath, config, blocks, rawContent };\n}\n\n/**\n * Parse the content of a single console block.\n */\nfunction parseBlockContent(content: string): {\n command: string;\n expectedOutput: string;\n expectedExitCode: number;\n} | null {\n const lines = content.split('\\n');\n const commandLines: string[] = [];\n const outputLines: string[] = [];\n let expectedExitCode = 0;\n let inCommand = false;\n\n for (const line of lines) {\n if (line.startsWith('$ ')) {\n // Start of a command\n inCommand = true;\n commandLines.push(line.slice(2));\n } else if (line.startsWith('> ') && inCommand) {\n // Continuation of a multi-line command\n commandLines.push(line.slice(2));\n } else if (line.startsWith('? ')) {\n // Exit code specification\n inCommand = false;\n expectedExitCode = parseInt(line.slice(2).trim(), 10);\n } else {\n // Output line\n inCommand = false;\n outputLines.push(line);\n }\n }\n\n if (commandLines.length === 0) {\n return null;\n }\n\n // Join command lines, handling shell continuations\n let command = '';\n for (let i = 0; i < commandLines.length; i++) {\n const line = commandLines[i] ?? '';\n if (line.endsWith('\\\\')) {\n command += line.slice(0, -1) + ' ';\n } else {\n command += line;\n if (i < commandLines.length - 1) {\n command += ' ';\n }\n }\n }\n\n // Join output lines, preserving blank lines but trimming trailing empty lines\n let expectedOutput = outputLines.join('\\n');\n expectedOutput = expectedOutput.replace(/\\n+$/, '');\n if (expectedOutput) {\n expectedOutput += '\\n';\n }\n\n return { command: command.trim(), expectedOutput, expectedExitCode };\n}\n","import { spawn } from 'node:child_process';\nimport { mkdtemp, realpath, rm } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join, dirname, resolve } from 'node:path';\nimport treeKill from 'tree-kill';\nimport type { TestBlock, TestBlockResult } from './types.js';\nimport type { TryscriptConfig } from './config.js';\n\n/** Default timeout in milliseconds */\nconst DEFAULT_TIMEOUT = 30_000;\n\n/**\n * Execution context for a test file.\n * Created once per file, contains the temp directory.\n */\nexport interface ExecutionContext {\n /** Temporary directory for this test file (resolved, no symlinks) */\n tempDir: string;\n /** Directory containing the test file (for portable test commands) */\n testDir: string;\n /** Resolved binary path */\n binPath: string;\n /** Environment variables */\n env: Record<string, string>;\n /** Timeout per command */\n timeout: number;\n}\n\n/**\n * Create an execution context for a test file.\n */\nexport async function createExecutionContext(\n config: TryscriptConfig,\n testFilePath: string,\n): Promise<ExecutionContext> {\n // Create temp directory and resolve symlinks (e.g., /var -> /private/var on macOS)\n // This ensures [CWD] and [ROOT] patterns match pwd output\n const rawTempDir = await mkdtemp(join(tmpdir(), 'tryscript-'));\n const tempDir = await realpath(rawTempDir);\n\n // Resolve test file directory for portable test commands\n const testDir = resolve(dirname(testFilePath));\n\n // Resolve binary path relative to test file directory\n let binPath = config.bin ?? '';\n if (binPath && !binPath.startsWith('/')) {\n binPath = join(testDir, binPath);\n }\n\n return {\n tempDir,\n testDir,\n binPath,\n env: {\n ...process.env,\n ...config.env,\n // Disable colors by default for deterministic output\n NO_COLOR: config.env?.NO_COLOR ?? '1',\n FORCE_COLOR: '0',\n // Provide test directory for portable test commands\n TRYSCRIPT_TEST_DIR: testDir,\n } as Record<string, string>,\n timeout: config.timeout ?? DEFAULT_TIMEOUT,\n };\n}\n\n/**\n * Clean up execution context (remove temp directory).\n */\nexport async function cleanupExecutionContext(ctx: ExecutionContext): Promise<void> {\n await rm(ctx.tempDir, { recursive: true, force: true });\n}\n\n/**\n * Run a single test block and return the result.\n */\nexport async function runBlock(block: TestBlock, ctx: ExecutionContext): Promise<TestBlockResult> {\n const startTime = Date.now();\n\n try {\n const { output, exitCode } = await executeCommand(block.command, ctx);\n\n const duration = Date.now() - startTime;\n\n return {\n block,\n passed: true, // Matching handled separately\n actualOutput: output,\n actualExitCode: exitCode,\n duration,\n };\n } catch (error) {\n const duration = Date.now() - startTime;\n const message = error instanceof Error ? error.message : String(error);\n\n return {\n block,\n passed: false,\n actualOutput: '',\n actualExitCode: -1,\n duration,\n error: message,\n };\n }\n}\n\n/**\n * Execute a command and capture output.\n */\nasync function executeCommand(\n command: string,\n ctx: ExecutionContext,\n): Promise<{ output: string; exitCode: number }> {\n return new Promise((resolve, reject) => {\n const proc = spawn(command, {\n shell: true,\n cwd: ctx.tempDir,\n env: ctx.env as NodeJS.ProcessEnv,\n // Pipe both to capture\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n const chunks: Buffer[] = [];\n\n // Capture data as it comes in to preserve order\n proc.stdout.on('data', (data: Buffer) => chunks.push(data));\n proc.stderr.on('data', (data: Buffer) => chunks.push(data));\n\n const timeoutId = setTimeout(() => {\n if (proc.pid) {\n treeKill(proc.pid, 'SIGKILL');\n }\n reject(new Error(`Command timed out after ${ctx.timeout}ms`));\n }, ctx.timeout);\n\n proc.on('close', (code) => {\n clearTimeout(timeoutId);\n const output = Buffer.concat(chunks).toString('utf-8');\n resolve({\n output,\n exitCode: code ?? 0,\n });\n });\n\n proc.on('error', (err) => {\n clearTimeout(timeoutId);\n reject(err);\n });\n });\n}\n","import stripAnsi from 'strip-ansi';\n\n/**\n * Escape special regex characters in a string.\n */\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n// Marker prefix for patterns (uses Unicode private use chars that won't appear in normal output)\nconst MARKER = '\\uE000';\n\n/**\n * Convert expected output with elision patterns to a regex.\n *\n * Handles (matching trycmd):\n * - [..] — matches any characters on the same line (trycmd: [^\\n]*?)\n * - ... — matches zero or more complete lines (trycmd: \\n(([^\\n]*\\n)*)?)\n * - [EXE] — matches .exe on Windows, empty otherwise\n * - [ROOT] — replaced with test root directory (pre-processed)\n * - [CWD] — replaced with current working directory (pre-processed)\n * - Custom [NAME] patterns from config (trycmd: TestCases::insert_var)\n */\nfunction patternToRegex(\n expected: string,\n customPatterns: Record<string, string | RegExp> = {},\n): RegExp {\n // Build a map of markers to their regex replacements\n const replacements = new Map<string, string>();\n let markerIndex = 0;\n\n const getMarker = (): string => {\n return `${MARKER}${markerIndex++}${MARKER}`;\n };\n\n let processed = expected;\n\n // Replace [..] with marker\n const dotdotMarker = getMarker();\n replacements.set(dotdotMarker, '[^\\\\n]*');\n processed = processed.replaceAll('[..]', dotdotMarker);\n\n // Replace ... (followed by newline) with marker\n const ellipsisMarker = getMarker();\n replacements.set(ellipsisMarker, '(?:[^\\\\n]*\\\\n)*');\n processed = processed.replace(/\\.\\.\\.\\n/g, ellipsisMarker);\n\n // Replace [EXE] with marker\n const exeMarker = getMarker();\n const exe = process.platform === 'win32' ? '\\\\.exe' : '';\n replacements.set(exeMarker, exe);\n processed = processed.replaceAll('[EXE]', exeMarker);\n\n // Replace custom patterns with markers\n for (const [name, pattern] of Object.entries(customPatterns)) {\n const placeholder = `[${name}]`;\n const patternStr = pattern instanceof RegExp ? pattern.source : pattern;\n const marker = getMarker();\n replacements.set(marker, `(${patternStr})`);\n processed = processed.replaceAll(placeholder, marker);\n }\n\n // Escape special regex characters\n let regex = escapeRegex(processed);\n\n // Restore markers to their regex replacements\n for (const [marker, replacement] of replacements) {\n regex = regex.replaceAll(escapeRegex(marker), replacement);\n }\n\n // Match the entire string (dotall mode for . to match newlines if needed)\n return new RegExp(`^${regex}$`, 's');\n}\n\n/**\n * Pre-process expected output to replace path placeholders with actual paths.\n * This happens BEFORE pattern matching.\n */\nfunction preprocessPaths(expected: string, context: { root: string; cwd: string }): string {\n let result = expected;\n // Normalize paths for comparison (use forward slashes)\n const normalizedRoot = context.root.replace(/\\\\/g, '/');\n const normalizedCwd = context.cwd.replace(/\\\\/g, '/');\n result = result.replaceAll('[ROOT]', normalizedRoot);\n result = result.replaceAll('[CWD]', normalizedCwd);\n return result;\n}\n\n/**\n * Normalize actual output for comparison.\n * - Remove ANSI escape codes (colors, etc.)\n * - Normalize line endings to \\n\n * - Normalize paths (Windows backslashes to forward slashes)\n * - Trim trailing whitespace from lines\n * - Ensure single trailing newline\n */\nexport function normalizeOutput(output: string): string {\n // Remove ANSI escape codes first\n let normalized = stripAnsi(output);\n\n normalized = normalized\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\r/g, '\\n')\n .split('\\n')\n .map((line) => line.trimEnd())\n .join('\\n')\n .replace(/\\n+$/, '\\n');\n\n // Handle empty output\n if (normalized === '\\n') {\n normalized = '';\n }\n\n return normalized;\n}\n\n/**\n * Check if actual output matches expected pattern.\n */\nexport function matchOutput(\n actual: string,\n expected: string,\n context: { root: string; cwd: string },\n customPatterns: Record<string, string | RegExp> = {},\n): boolean {\n const normalizedActual = normalizeOutput(actual);\n const normalizedExpected = normalizeOutput(expected);\n\n // Empty expected matches empty actual\n if (normalizedExpected === '' && normalizedActual === '') {\n return true;\n }\n\n const preprocessed = preprocessPaths(normalizedExpected, context);\n const regex = patternToRegex(preprocessed, customPatterns);\n return regex.test(normalizedActual);\n}\n","// Public API exports\n\n// Version constant (injected at build time)\ndeclare const __VERSION__: string;\nexport const VERSION: string = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'development';\n\n// Config helper\nexport { defineConfig } from './lib/config.js';\nexport type { TryscriptConfig } from './lib/config.js';\n\n// Types\nexport type {\n TestConfig,\n TestBlock,\n TestFile,\n TestBlockResult,\n TestFileResult,\n TestRunSummary,\n} from './lib/types.js';\n\n// Core functions (for programmatic use)\nexport { parseTestFile } from './lib/parser.js';\nexport { runBlock, createExecutionContext, cleanupExecutionContext } from './lib/runner.js';\nexport type { ExecutionContext } from './lib/runner.js';\nexport { matchOutput, normalizeOutput } from './lib/matcher.js';\n"],"mappings":"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"}
|