@tishlang/tish-format 3.6.0 → 3.7.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/Cargo.toml +1 -0
- package/bin/tish-format +0 -0
- package/crates/tish/Cargo.toml +4 -1
- package/crates/tish/src/cli_help.rs +84 -0
- package/crates/tish/src/main.rs +93 -1
- package/crates/tish/tests/test_runner.rs +673 -0
- package/crates/tish_bytecode/src/compiler.rs +25 -4
- package/crates/tish_compile/src/resolve.rs +45 -12
- package/crates/tish_core/src/lib.rs +103 -11
- package/crates/tish_eval/src/eval.rs +16 -0
- package/crates/tish_eval/src/value_convert.rs +10 -0
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_test/Cargo.toml +47 -0
- package/crates/tish_test/src/assert.rs +435 -0
- package/crates/tish_test/src/config.rs +85 -0
- package/crates/tish_test/src/coverage.rs +237 -0
- package/crates/tish_test/src/deep_equal.rs +261 -0
- package/crates/tish_test/src/discovery.rs +70 -0
- package/crates/tish_test/src/expect.rs +977 -0
- package/crates/tish_test/src/instrument.rs +457 -0
- package/crates/tish_test/src/isolation.rs +14 -0
- package/crates/tish_test/src/lib.rs +22 -0
- package/crates/tish_test/src/load.rs +152 -0
- package/crates/tish_test/src/mocks.rs +270 -0
- package/crates/tish_test/src/module.rs +318 -0
- package/crates/tish_test/src/registry.rs +235 -0
- package/crates/tish_test/src/report.rs +312 -0
- package/crates/tish_test/src/runner.rs +797 -0
- package/crates/tish_test/src/snapshots.rs +197 -0
- package/crates/tish_vm/src/vm.rs +17 -14
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
//! AST instrumentation for line coverage — rewrite programs in `tish_test` only.
|
|
2
|
+
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
use std::sync::Arc;
|
|
5
|
+
|
|
6
|
+
use tishlang_ast::{
|
|
7
|
+
ArrowBody, CallArg, ExportDeclaration, Expr, Literal, Program, Span, Statement,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
use crate::coverage::{self, hit_global_name};
|
|
11
|
+
|
|
12
|
+
/// Instrument every resolved module's AST so execution of any backend records line hits.
|
|
13
|
+
pub fn instrument_program(program: &mut Program, path: &Path) {
|
|
14
|
+
if !coverage::is_enabled() {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
let file_id = coverage::intern_file(path);
|
|
18
|
+
program.statements = instrument_stmts(std::mem::take(&mut program.statements), file_id);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fn instrument_stmts(stmts: Vec<Statement>, file_id: u32) -> Vec<Statement> {
|
|
22
|
+
let mut out = Vec::with_capacity(stmts.len() * 2);
|
|
23
|
+
for stmt in stmts {
|
|
24
|
+
match stmt {
|
|
25
|
+
Statement::TypeAlias { .. }
|
|
26
|
+
| Statement::DeclareVar { .. }
|
|
27
|
+
| Statement::DeclareFun { .. }
|
|
28
|
+
| Statement::Import { .. } => {
|
|
29
|
+
out.push(stmt);
|
|
30
|
+
}
|
|
31
|
+
Statement::Export { declaration, span } => {
|
|
32
|
+
let declaration = Box::new(match *declaration {
|
|
33
|
+
ExportDeclaration::Named(inner) => {
|
|
34
|
+
ExportDeclaration::Named(Box::new(instrument_stmt(*inner, file_id)))
|
|
35
|
+
}
|
|
36
|
+
ExportDeclaration::Default(expr) => {
|
|
37
|
+
ExportDeclaration::Default(instrument_expr(expr, file_id))
|
|
38
|
+
}
|
|
39
|
+
other => other,
|
|
40
|
+
});
|
|
41
|
+
out.push(hit_stmt(file_id, span.start.0 as u32));
|
|
42
|
+
out.push(Statement::Export { declaration, span });
|
|
43
|
+
}
|
|
44
|
+
Statement::Multi { statements, span } => {
|
|
45
|
+
out.push(Statement::Multi {
|
|
46
|
+
statements: instrument_stmts(statements, file_id),
|
|
47
|
+
span,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
Statement::Block { statements, span } => {
|
|
51
|
+
out.push(Statement::Block {
|
|
52
|
+
statements: instrument_stmts(statements, file_id),
|
|
53
|
+
span,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
other => {
|
|
57
|
+
let line = other.span().start.0 as u32;
|
|
58
|
+
out.push(hit_stmt(file_id, line));
|
|
59
|
+
out.push(instrument_stmt(other, file_id));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
out
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn instrument_stmt(stmt: Statement, file_id: u32) -> Statement {
|
|
67
|
+
match stmt {
|
|
68
|
+
Statement::Block { statements, span } => Statement::Block {
|
|
69
|
+
statements: instrument_stmts(statements, file_id),
|
|
70
|
+
span,
|
|
71
|
+
},
|
|
72
|
+
Statement::Multi { statements, span } => Statement::Multi {
|
|
73
|
+
statements: instrument_stmts(statements, file_id),
|
|
74
|
+
span,
|
|
75
|
+
},
|
|
76
|
+
Statement::If {
|
|
77
|
+
cond,
|
|
78
|
+
then_branch,
|
|
79
|
+
else_branch,
|
|
80
|
+
span,
|
|
81
|
+
} => Statement::If {
|
|
82
|
+
cond: instrument_expr(cond, file_id),
|
|
83
|
+
then_branch: Box::new(instrument_stmt(*then_branch, file_id)),
|
|
84
|
+
else_branch: else_branch.map(|b| Box::new(instrument_stmt(*b, file_id))),
|
|
85
|
+
span,
|
|
86
|
+
},
|
|
87
|
+
Statement::While { cond, body, span } => Statement::While {
|
|
88
|
+
cond: instrument_expr(cond, file_id),
|
|
89
|
+
body: Box::new(instrument_stmt(*body, file_id)),
|
|
90
|
+
span,
|
|
91
|
+
},
|
|
92
|
+
Statement::DoWhile { body, cond, span } => Statement::DoWhile {
|
|
93
|
+
body: Box::new(instrument_stmt(*body, file_id)),
|
|
94
|
+
cond: instrument_expr(cond, file_id),
|
|
95
|
+
span,
|
|
96
|
+
},
|
|
97
|
+
Statement::For {
|
|
98
|
+
init,
|
|
99
|
+
cond,
|
|
100
|
+
update,
|
|
101
|
+
body,
|
|
102
|
+
span,
|
|
103
|
+
} => Statement::For {
|
|
104
|
+
init: init.map(|i| Box::new(instrument_stmt(*i, file_id))),
|
|
105
|
+
cond: cond.map(|c| instrument_expr(c, file_id)),
|
|
106
|
+
update: update.map(|u| instrument_expr(u, file_id)),
|
|
107
|
+
body: Box::new(instrument_stmt(*body, file_id)),
|
|
108
|
+
span,
|
|
109
|
+
},
|
|
110
|
+
Statement::ForOf {
|
|
111
|
+
name,
|
|
112
|
+
name_span,
|
|
113
|
+
iterable,
|
|
114
|
+
body,
|
|
115
|
+
span,
|
|
116
|
+
} => Statement::ForOf {
|
|
117
|
+
name,
|
|
118
|
+
name_span,
|
|
119
|
+
iterable: instrument_expr(iterable, file_id),
|
|
120
|
+
body: Box::new(instrument_stmt(*body, file_id)),
|
|
121
|
+
span,
|
|
122
|
+
},
|
|
123
|
+
Statement::ForIn {
|
|
124
|
+
name,
|
|
125
|
+
name_span,
|
|
126
|
+
object,
|
|
127
|
+
body,
|
|
128
|
+
span,
|
|
129
|
+
} => Statement::ForIn {
|
|
130
|
+
name,
|
|
131
|
+
name_span,
|
|
132
|
+
object: instrument_expr(object, file_id),
|
|
133
|
+
body: Box::new(instrument_stmt(*body, file_id)),
|
|
134
|
+
span,
|
|
135
|
+
},
|
|
136
|
+
Statement::FunDecl {
|
|
137
|
+
async_,
|
|
138
|
+
name,
|
|
139
|
+
name_span,
|
|
140
|
+
params,
|
|
141
|
+
rest_param,
|
|
142
|
+
return_type,
|
|
143
|
+
body,
|
|
144
|
+
span,
|
|
145
|
+
} => Statement::FunDecl {
|
|
146
|
+
async_,
|
|
147
|
+
name,
|
|
148
|
+
name_span,
|
|
149
|
+
params,
|
|
150
|
+
rest_param,
|
|
151
|
+
return_type,
|
|
152
|
+
body: Box::new(instrument_stmt(*body, file_id)),
|
|
153
|
+
span,
|
|
154
|
+
},
|
|
155
|
+
Statement::Try {
|
|
156
|
+
body,
|
|
157
|
+
catch_param,
|
|
158
|
+
catch_param_span,
|
|
159
|
+
catch_body,
|
|
160
|
+
finally_body,
|
|
161
|
+
span,
|
|
162
|
+
} => Statement::Try {
|
|
163
|
+
body: Box::new(instrument_stmt(*body, file_id)),
|
|
164
|
+
catch_param,
|
|
165
|
+
catch_param_span,
|
|
166
|
+
catch_body: catch_body.map(|b| Box::new(instrument_stmt(*b, file_id))),
|
|
167
|
+
finally_body: finally_body.map(|b| Box::new(instrument_stmt(*b, file_id))),
|
|
168
|
+
span,
|
|
169
|
+
},
|
|
170
|
+
Statement::Switch {
|
|
171
|
+
expr,
|
|
172
|
+
cases,
|
|
173
|
+
default_body,
|
|
174
|
+
span,
|
|
175
|
+
} => Statement::Switch {
|
|
176
|
+
expr: instrument_expr(expr, file_id),
|
|
177
|
+
cases: cases
|
|
178
|
+
.into_iter()
|
|
179
|
+
.map(|(c, body)| {
|
|
180
|
+
(
|
|
181
|
+
c.map(|e| instrument_expr(e, file_id)),
|
|
182
|
+
instrument_stmts(body, file_id),
|
|
183
|
+
)
|
|
184
|
+
})
|
|
185
|
+
.collect(),
|
|
186
|
+
default_body: default_body.map(|b| instrument_stmts(b, file_id)),
|
|
187
|
+
span,
|
|
188
|
+
},
|
|
189
|
+
Statement::ExprStmt { expr, span } => Statement::ExprStmt {
|
|
190
|
+
expr: instrument_expr(expr, file_id),
|
|
191
|
+
span,
|
|
192
|
+
},
|
|
193
|
+
Statement::VarDecl {
|
|
194
|
+
name,
|
|
195
|
+
name_span,
|
|
196
|
+
mutable,
|
|
197
|
+
type_ann,
|
|
198
|
+
init,
|
|
199
|
+
span,
|
|
200
|
+
} => Statement::VarDecl {
|
|
201
|
+
name,
|
|
202
|
+
name_span,
|
|
203
|
+
mutable,
|
|
204
|
+
type_ann,
|
|
205
|
+
init: init.map(|e| instrument_expr(e, file_id)),
|
|
206
|
+
span,
|
|
207
|
+
},
|
|
208
|
+
Statement::VarDeclDestructure {
|
|
209
|
+
pattern,
|
|
210
|
+
mutable,
|
|
211
|
+
init,
|
|
212
|
+
span,
|
|
213
|
+
} => Statement::VarDeclDestructure {
|
|
214
|
+
pattern,
|
|
215
|
+
mutable,
|
|
216
|
+
init: instrument_expr(init, file_id),
|
|
217
|
+
span,
|
|
218
|
+
},
|
|
219
|
+
Statement::Return { value, span } => Statement::Return {
|
|
220
|
+
value: value.map(|e| instrument_expr(e, file_id)),
|
|
221
|
+
span,
|
|
222
|
+
},
|
|
223
|
+
Statement::Throw { value, span } => Statement::Throw {
|
|
224
|
+
value: instrument_expr(value, file_id),
|
|
225
|
+
span,
|
|
226
|
+
},
|
|
227
|
+
other => other,
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fn instrument_expr(expr: Expr, file_id: u32) -> Expr {
|
|
232
|
+
match expr {
|
|
233
|
+
Expr::ArrowFunction {
|
|
234
|
+
async_,
|
|
235
|
+
params,
|
|
236
|
+
body,
|
|
237
|
+
span,
|
|
238
|
+
} => Expr::ArrowFunction {
|
|
239
|
+
async_,
|
|
240
|
+
params,
|
|
241
|
+
body: match body {
|
|
242
|
+
ArrowBody::Expr(e) => ArrowBody::Expr(Box::new(instrument_expr(*e, file_id))),
|
|
243
|
+
ArrowBody::Block(b) => ArrowBody::Block(Box::new(instrument_stmt(*b, file_id))),
|
|
244
|
+
},
|
|
245
|
+
span,
|
|
246
|
+
},
|
|
247
|
+
Expr::Call { callee, args, span } => Expr::Call {
|
|
248
|
+
callee: Box::new(instrument_expr(*callee, file_id)),
|
|
249
|
+
args: args
|
|
250
|
+
.into_iter()
|
|
251
|
+
.map(|a| match a {
|
|
252
|
+
CallArg::Expr(e) => CallArg::Expr(instrument_expr(e, file_id)),
|
|
253
|
+
CallArg::Spread(e) => CallArg::Spread(instrument_expr(e, file_id)),
|
|
254
|
+
})
|
|
255
|
+
.collect(),
|
|
256
|
+
span,
|
|
257
|
+
},
|
|
258
|
+
Expr::Binary {
|
|
259
|
+
left,
|
|
260
|
+
op,
|
|
261
|
+
right,
|
|
262
|
+
span,
|
|
263
|
+
} => Expr::Binary {
|
|
264
|
+
left: Box::new(instrument_expr(*left, file_id)),
|
|
265
|
+
op,
|
|
266
|
+
right: Box::new(instrument_expr(*right, file_id)),
|
|
267
|
+
span,
|
|
268
|
+
},
|
|
269
|
+
Expr::Unary { op, operand, span } => Expr::Unary {
|
|
270
|
+
op,
|
|
271
|
+
operand: Box::new(instrument_expr(*operand, file_id)),
|
|
272
|
+
span,
|
|
273
|
+
},
|
|
274
|
+
Expr::Member {
|
|
275
|
+
object,
|
|
276
|
+
prop,
|
|
277
|
+
optional,
|
|
278
|
+
span,
|
|
279
|
+
} => Expr::Member {
|
|
280
|
+
object: Box::new(instrument_expr(*object, file_id)),
|
|
281
|
+
prop,
|
|
282
|
+
optional,
|
|
283
|
+
span,
|
|
284
|
+
},
|
|
285
|
+
Expr::Assign { name, value, span } => Expr::Assign {
|
|
286
|
+
name,
|
|
287
|
+
value: Box::new(instrument_expr(*value, file_id)),
|
|
288
|
+
span,
|
|
289
|
+
},
|
|
290
|
+
Expr::Array { elements, span } => Expr::Array {
|
|
291
|
+
elements: elements
|
|
292
|
+
.into_iter()
|
|
293
|
+
.map(|el| match el {
|
|
294
|
+
tishlang_ast::ArrayElement::Expr(e) => {
|
|
295
|
+
tishlang_ast::ArrayElement::Expr(instrument_expr(e, file_id))
|
|
296
|
+
}
|
|
297
|
+
tishlang_ast::ArrayElement::Spread(e) => {
|
|
298
|
+
tishlang_ast::ArrayElement::Spread(instrument_expr(e, file_id))
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
.collect(),
|
|
302
|
+
span,
|
|
303
|
+
},
|
|
304
|
+
Expr::Object { props, span } => Expr::Object {
|
|
305
|
+
props: props
|
|
306
|
+
.into_iter()
|
|
307
|
+
.map(|p| match p {
|
|
308
|
+
tishlang_ast::ObjectProp::KeyValue(k, v, s) => {
|
|
309
|
+
tishlang_ast::ObjectProp::KeyValue(k, instrument_expr(v, file_id), s)
|
|
310
|
+
}
|
|
311
|
+
tishlang_ast::ObjectProp::Spread(e) => {
|
|
312
|
+
tishlang_ast::ObjectProp::Spread(instrument_expr(e, file_id))
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
.collect(),
|
|
316
|
+
span,
|
|
317
|
+
},
|
|
318
|
+
// Every remaining expression form that can nest an arrow function. Missing one does not
|
|
319
|
+
// fail loudly: the arrow's body is simply never marked executable, so it drops out of
|
|
320
|
+
// the denominator and inflates the reported percentage.
|
|
321
|
+
Expr::New { callee, args, span } => Expr::New {
|
|
322
|
+
callee: Box::new(instrument_expr(*callee, file_id)),
|
|
323
|
+
args: args
|
|
324
|
+
.into_iter()
|
|
325
|
+
.map(|a| match a {
|
|
326
|
+
CallArg::Expr(e) => CallArg::Expr(instrument_expr(e, file_id)),
|
|
327
|
+
CallArg::Spread(e) => CallArg::Spread(instrument_expr(e, file_id)),
|
|
328
|
+
})
|
|
329
|
+
.collect(),
|
|
330
|
+
span,
|
|
331
|
+
},
|
|
332
|
+
Expr::Index {
|
|
333
|
+
object,
|
|
334
|
+
index,
|
|
335
|
+
optional,
|
|
336
|
+
span,
|
|
337
|
+
} => Expr::Index {
|
|
338
|
+
object: Box::new(instrument_expr(*object, file_id)),
|
|
339
|
+
index: Box::new(instrument_expr(*index, file_id)),
|
|
340
|
+
optional,
|
|
341
|
+
span,
|
|
342
|
+
},
|
|
343
|
+
Expr::Conditional {
|
|
344
|
+
cond,
|
|
345
|
+
then_branch,
|
|
346
|
+
else_branch,
|
|
347
|
+
span,
|
|
348
|
+
} => Expr::Conditional {
|
|
349
|
+
cond: Box::new(instrument_expr(*cond, file_id)),
|
|
350
|
+
then_branch: Box::new(instrument_expr(*then_branch, file_id)),
|
|
351
|
+
else_branch: Box::new(instrument_expr(*else_branch, file_id)),
|
|
352
|
+
span,
|
|
353
|
+
},
|
|
354
|
+
Expr::NullishCoalesce { left, right, span } => Expr::NullishCoalesce {
|
|
355
|
+
left: Box::new(instrument_expr(*left, file_id)),
|
|
356
|
+
right: Box::new(instrument_expr(*right, file_id)),
|
|
357
|
+
span,
|
|
358
|
+
},
|
|
359
|
+
Expr::Await { operand, span } => Expr::Await {
|
|
360
|
+
operand: Box::new(instrument_expr(*operand, file_id)),
|
|
361
|
+
span,
|
|
362
|
+
},
|
|
363
|
+
Expr::TypeOf { operand, span } => Expr::TypeOf {
|
|
364
|
+
operand: Box::new(instrument_expr(*operand, file_id)),
|
|
365
|
+
span,
|
|
366
|
+
},
|
|
367
|
+
Expr::Delete { target, span } => Expr::Delete {
|
|
368
|
+
target: Box::new(instrument_expr(*target, file_id)),
|
|
369
|
+
span,
|
|
370
|
+
},
|
|
371
|
+
Expr::CompoundAssign {
|
|
372
|
+
name,
|
|
373
|
+
op,
|
|
374
|
+
value,
|
|
375
|
+
span,
|
|
376
|
+
} => Expr::CompoundAssign {
|
|
377
|
+
name,
|
|
378
|
+
op,
|
|
379
|
+
value: Box::new(instrument_expr(*value, file_id)),
|
|
380
|
+
span,
|
|
381
|
+
},
|
|
382
|
+
Expr::LogicalAssign {
|
|
383
|
+
name,
|
|
384
|
+
op,
|
|
385
|
+
value,
|
|
386
|
+
span,
|
|
387
|
+
} => Expr::LogicalAssign {
|
|
388
|
+
name,
|
|
389
|
+
op,
|
|
390
|
+
value: Box::new(instrument_expr(*value, file_id)),
|
|
391
|
+
span,
|
|
392
|
+
},
|
|
393
|
+
Expr::MemberAssign {
|
|
394
|
+
object,
|
|
395
|
+
prop,
|
|
396
|
+
value,
|
|
397
|
+
span,
|
|
398
|
+
} => Expr::MemberAssign {
|
|
399
|
+
object: Box::new(instrument_expr(*object, file_id)),
|
|
400
|
+
prop,
|
|
401
|
+
value: Box::new(instrument_expr(*value, file_id)),
|
|
402
|
+
span,
|
|
403
|
+
},
|
|
404
|
+
Expr::IndexAssign {
|
|
405
|
+
object,
|
|
406
|
+
index,
|
|
407
|
+
value,
|
|
408
|
+
span,
|
|
409
|
+
} => Expr::IndexAssign {
|
|
410
|
+
object: Box::new(instrument_expr(*object, file_id)),
|
|
411
|
+
index: Box::new(instrument_expr(*index, file_id)),
|
|
412
|
+
value: Box::new(instrument_expr(*value, file_id)),
|
|
413
|
+
span,
|
|
414
|
+
},
|
|
415
|
+
Expr::TemplateLiteral {
|
|
416
|
+
quasis,
|
|
417
|
+
exprs,
|
|
418
|
+
span,
|
|
419
|
+
} => Expr::TemplateLiteral {
|
|
420
|
+
quasis,
|
|
421
|
+
exprs: exprs
|
|
422
|
+
.into_iter()
|
|
423
|
+
.map(|e| instrument_expr(e, file_id))
|
|
424
|
+
.collect(),
|
|
425
|
+
span,
|
|
426
|
+
},
|
|
427
|
+
// Literals, identifiers, increments/decrements and JSX carry no nested arrow bodies
|
|
428
|
+
// that statement-level instrumentation would otherwise miss.
|
|
429
|
+
other => other,
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
fn hit_stmt(file_id: u32, line: u32) -> Statement {
|
|
434
|
+
coverage::mark_executable(file_id, line);
|
|
435
|
+
let span = Span {
|
|
436
|
+
start: (line as usize, 1),
|
|
437
|
+
end: (line as usize, 1),
|
|
438
|
+
};
|
|
439
|
+
let name: Arc<str> = Arc::from(hit_global_name());
|
|
440
|
+
Statement::ExprStmt {
|
|
441
|
+
expr: Expr::Call {
|
|
442
|
+
callee: Box::new(Expr::Ident { name, span }),
|
|
443
|
+
args: vec![
|
|
444
|
+
CallArg::Expr(Expr::Literal {
|
|
445
|
+
value: Literal::Number(file_id as f64),
|
|
446
|
+
span,
|
|
447
|
+
}),
|
|
448
|
+
CallArg::Expr(Expr::Literal {
|
|
449
|
+
value: Literal::Number(line as f64),
|
|
450
|
+
span,
|
|
451
|
+
}),
|
|
452
|
+
],
|
|
453
|
+
span,
|
|
454
|
+
},
|
|
455
|
+
span,
|
|
456
|
+
}
|
|
457
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//! Per-file isolation hooks for multi-file `tish test` runs.
|
|
2
|
+
//!
|
|
3
|
+
//! Clears pending throws and the mock registry between files. Timers / HTTP static routes /
|
|
4
|
+
//! sockets are **not** reset in-process — suites that leak host state should use separate
|
|
5
|
+
//! processes.
|
|
6
|
+
|
|
7
|
+
use tishlang_core::take_pending_throw;
|
|
8
|
+
|
|
9
|
+
/// Reset host state between test files in the same process.
|
|
10
|
+
pub fn reset_between_files() {
|
|
11
|
+
let _ = take_pending_throw();
|
|
12
|
+
// Otherwise `restoreAllMocks()` in one file drains handles registered by another.
|
|
13
|
+
crate::mocks::reset_mock_registry();
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//! Native `tish test` runner, Bun/Jest-shaped `tish:test`, and Node-mirrored `tish:assert`.
|
|
2
|
+
|
|
3
|
+
pub mod assert;
|
|
4
|
+
pub mod config;
|
|
5
|
+
pub mod coverage;
|
|
6
|
+
pub mod deep_equal;
|
|
7
|
+
pub mod discovery;
|
|
8
|
+
pub mod expect;
|
|
9
|
+
pub mod instrument;
|
|
10
|
+
pub mod isolation;
|
|
11
|
+
pub mod mocks;
|
|
12
|
+
pub mod module;
|
|
13
|
+
pub mod registry;
|
|
14
|
+
pub mod report;
|
|
15
|
+
pub mod runner;
|
|
16
|
+
pub mod snapshots;
|
|
17
|
+
|
|
18
|
+
#[cfg(feature = "runner")]
|
|
19
|
+
pub mod load;
|
|
20
|
+
|
|
21
|
+
pub use module::{assert_module, test_module};
|
|
22
|
+
pub use runner::{run_tests, run_tests_watch, TestOptions, TestRunResult};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
//! Load a test file into the suite registry (requires `runner` feature).
|
|
2
|
+
|
|
3
|
+
use std::collections::HashSet;
|
|
4
|
+
use std::path::{Path, PathBuf};
|
|
5
|
+
use std::sync::Arc;
|
|
6
|
+
|
|
7
|
+
use crate::coverage::{self, hit_global_name, hit_value};
|
|
8
|
+
use crate::instrument::instrument_program;
|
|
9
|
+
use crate::module::{assert_module, test_module};
|
|
10
|
+
use crate::registry::{begin_collect, end_collect, take_collect_errors, SuiteNode};
|
|
11
|
+
|
|
12
|
+
/// A loaded file plus any errors thrown while collecting its suites.
|
|
13
|
+
pub struct LoadedSuite {
|
|
14
|
+
pub suite: SuiteNode,
|
|
15
|
+
/// Throws that escaped a `describe` body. These silently truncate registration, so the
|
|
16
|
+
/// runner reports them as failures instead of letting the file look green.
|
|
17
|
+
pub collect_errors: Vec<String>,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Resolve, merge, optimize, and execute `path` so `describe`/`test` callbacks register
|
|
21
|
+
/// into the collect-then-run suite tree. Suppresses printing the program's last value.
|
|
22
|
+
///
|
|
23
|
+
/// `preload` modules run first **on the same VM instance**, so anything they define or
|
|
24
|
+
/// register (globals, `beforeEach` hooks) is visible to the test file. `features` restricts VM
|
|
25
|
+
/// capabilities exactly as `tish run --feature` does; an empty list means "everything linked
|
|
26
|
+
/// into this binary".
|
|
27
|
+
pub fn load_and_collect_tests(
|
|
28
|
+
path: &Path,
|
|
29
|
+
preload: &[PathBuf],
|
|
30
|
+
backend: &str,
|
|
31
|
+
no_optimize: bool,
|
|
32
|
+
features: &[String],
|
|
33
|
+
) -> Result<LoadedSuite, String> {
|
|
34
|
+
let path = path
|
|
35
|
+
.canonicalize()
|
|
36
|
+
.map_err(|e| format!("Cannot resolve {}: {}", path.display(), e))?;
|
|
37
|
+
|
|
38
|
+
begin_collect(&path.to_string_lossy());
|
|
39
|
+
|
|
40
|
+
let mut programs = Vec::with_capacity(preload.len() + 1);
|
|
41
|
+
for pre in preload {
|
|
42
|
+
let pre = pre
|
|
43
|
+
.canonicalize()
|
|
44
|
+
.map_err(|e| format!("Cannot resolve preload {}: {}", pre.display(), e))?;
|
|
45
|
+
let program = compile_program(&pre, no_optimize)
|
|
46
|
+
.map_err(|e| format!("Error loading preload {}: {}", pre.display(), e))?;
|
|
47
|
+
programs.push((pre, program));
|
|
48
|
+
}
|
|
49
|
+
programs.push((path.clone(), compile_program(&path, no_optimize)?));
|
|
50
|
+
|
|
51
|
+
let result = match backend {
|
|
52
|
+
"vm" => run_vm(&programs, features),
|
|
53
|
+
other => {
|
|
54
|
+
return Err(format!(
|
|
55
|
+
"unsupported --backend `{other}` for tish test (use `vm`)"
|
|
56
|
+
));
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
let suite = end_collect();
|
|
61
|
+
let collect_errors = take_collect_errors();
|
|
62
|
+
result?;
|
|
63
|
+
Ok(LoadedSuite {
|
|
64
|
+
suite,
|
|
65
|
+
collect_errors,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fn compile_program(path: &Path, no_optimize: bool) -> Result<tishlang_ast::Program, String> {
|
|
70
|
+
let project_root = path.parent().and_then(|p| {
|
|
71
|
+
if p.file_name().and_then(|n| n.to_str()) == Some("src") {
|
|
72
|
+
p.parent()
|
|
73
|
+
} else {
|
|
74
|
+
Some(p)
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
let mut modules = tishlang_compile::resolve_project(path, project_root)?;
|
|
79
|
+
tishlang_compile::detect_cycles(&modules)?;
|
|
80
|
+
|
|
81
|
+
// Instrument each module's AST in this crate so vm / interp / future backends share hits.
|
|
82
|
+
// A module reached from more than one test file must only be instrumented once, or its
|
|
83
|
+
// statements would be double-counted (and re-wrapped) on the second load.
|
|
84
|
+
if coverage::is_enabled() {
|
|
85
|
+
for m in &mut modules {
|
|
86
|
+
if coverage::mark_instrumented(&m.path) {
|
|
87
|
+
instrument_program(&mut m.program, &m.path);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let prog = tishlang_compile::merge_modules(modules)?.program;
|
|
93
|
+
Ok(if no_optimize {
|
|
94
|
+
prog
|
|
95
|
+
} else {
|
|
96
|
+
tishlang_opt::optimize(&prog)
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// VM capability set, matching `tish run`: empty `--feature` means every capability linked
|
|
101
|
+
/// into this binary; otherwise exactly the requested set (`full` expands).
|
|
102
|
+
fn vm_capabilities(features: &[String]) -> HashSet<String> {
|
|
103
|
+
if features.is_empty() {
|
|
104
|
+
return tishlang_vm::all_compiled_capabilities();
|
|
105
|
+
}
|
|
106
|
+
let mut out = HashSet::new();
|
|
107
|
+
for s in features {
|
|
108
|
+
for part in s.split(',').map(str::trim).filter(|p| !p.is_empty()) {
|
|
109
|
+
if part == "full" {
|
|
110
|
+
for name in ["http", "timers", "fs", "process", "regex", "ws", "tty"] {
|
|
111
|
+
out.insert(name.to_string());
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
out.insert(part.to_string());
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
out
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn run_vm(
|
|
122
|
+
programs: &[(PathBuf, tishlang_ast::Program)],
|
|
123
|
+
features: &[String],
|
|
124
|
+
) -> Result<(), String> {
|
|
125
|
+
let mut vm = tishlang_vm::Vm::with_capabilities(vm_capabilities(features));
|
|
126
|
+
// `node:assert` / `assert/strict` are normalized to `tish:assert` during resolution, so
|
|
127
|
+
// only the canonical specs need registering.
|
|
128
|
+
vm.register_native_module("tish:test", test_module());
|
|
129
|
+
vm.register_native_module("tish:assert", assert_module());
|
|
130
|
+
if coverage::is_enabled() {
|
|
131
|
+
vm.set_global(Arc::from(hit_global_name()), hit_value());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (path, program) in programs {
|
|
135
|
+
let source_arc: Arc<str> = Arc::from(path.to_string_lossy().as_ref());
|
|
136
|
+
let mut chunk = tishlang_bytecode::compile_with_source(program, Some(source_arc.clone()))
|
|
137
|
+
.map_err(|e| e.to_string())?;
|
|
138
|
+
chunk.source = Some(source_arc);
|
|
139
|
+
|
|
140
|
+
vm.run_with_options(&chunk, false)
|
|
141
|
+
.map_err(|e| format!("Error loading {}: {}", path.display(), e))?;
|
|
142
|
+
if tishlang_core::has_pending_throw() {
|
|
143
|
+
let err = tishlang_core::take_pending_throw().unwrap_or(tishlang_core::Value::Null);
|
|
144
|
+
return Err(format!(
|
|
145
|
+
"Error loading {}: {}",
|
|
146
|
+
path.display(),
|
|
147
|
+
err.to_display_string()
|
|
148
|
+
));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
Ok(())
|
|
152
|
+
}
|