@tishlang/tish-format 3.6.0 → 3.7.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/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,270 @@
|
|
|
1
|
+
//! `mock` / `spyOn` with call tracking.
|
|
2
|
+
|
|
3
|
+
use tishlang_core::{value_call, Arc, ObjectMap, Value, VmRef};
|
|
4
|
+
|
|
5
|
+
use std::cell::RefCell;
|
|
6
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
7
|
+
|
|
8
|
+
thread_local! {
|
|
9
|
+
static MOCKS: RefCell<Vec<MockHandle>> = const { RefCell::new(Vec::new()) };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
struct MockHandle {
|
|
13
|
+
meta: Value,
|
|
14
|
+
restore: Option<(Value, String, Value)>,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static MOCK_SEQ: AtomicUsize = AtomicUsize::new(1);
|
|
18
|
+
|
|
19
|
+
fn next_mock_id() -> usize {
|
|
20
|
+
MOCK_SEQ.fetch_add(1, Ordering::SeqCst)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fn empty_calls_array() -> Value {
|
|
24
|
+
Value::Array(VmRef::new(Vec::new()))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn bump_calls(meta: &Value, args: &[Value]) {
|
|
28
|
+
let Value::Object(o) = meta else {
|
|
29
|
+
return;
|
|
30
|
+
};
|
|
31
|
+
let mut b = o.borrow_mut();
|
|
32
|
+
if !b.strings.contains_key("calls") {
|
|
33
|
+
b.strings.insert(Arc::from("calls"), empty_calls_array());
|
|
34
|
+
}
|
|
35
|
+
if let Some(Value::Array(arr)) = b.strings.get("calls") {
|
|
36
|
+
arr.borrow_mut()
|
|
37
|
+
.push(Value::Array(VmRef::new(args.to_vec())));
|
|
38
|
+
}
|
|
39
|
+
let count = match b.strings.get("calls") {
|
|
40
|
+
Some(Value::Array(a)) => a.borrow().len() as f64,
|
|
41
|
+
_ => 0.0,
|
|
42
|
+
};
|
|
43
|
+
b.strings
|
|
44
|
+
.insert(Arc::from("callCount"), Value::Number(count));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fn make_mock_meta(impl_fn: Value) -> Value {
|
|
48
|
+
let id = next_mock_id();
|
|
49
|
+
let mut m = ObjectMap::default();
|
|
50
|
+
m.insert(Arc::from("mockId"), Value::Number(id as f64));
|
|
51
|
+
m.insert(Arc::from("calls"), empty_calls_array());
|
|
52
|
+
m.insert(Arc::from("callCount"), Value::Number(0.0));
|
|
53
|
+
m.insert(Arc::from("implementation"), impl_fn.clone());
|
|
54
|
+
let meta = Value::object(m);
|
|
55
|
+
if let Value::Object(o) = &meta {
|
|
56
|
+
let o_clear = o.clone();
|
|
57
|
+
let o_reset = o.clone();
|
|
58
|
+
let impl_reset = impl_fn;
|
|
59
|
+
o.borrow_mut().strings.insert(
|
|
60
|
+
Arc::from("mockClear"),
|
|
61
|
+
Value::native(move |_args: &[Value]| {
|
|
62
|
+
let mut b = o_clear.borrow_mut();
|
|
63
|
+
b.strings.insert(Arc::from("calls"), empty_calls_array());
|
|
64
|
+
b.strings.insert(Arc::from("callCount"), Value::Number(0.0));
|
|
65
|
+
Value::Null
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
o.borrow_mut().strings.insert(
|
|
69
|
+
Arc::from("mockReset"),
|
|
70
|
+
Value::native(move |_args: &[Value]| {
|
|
71
|
+
let mut b = o_reset.borrow_mut();
|
|
72
|
+
b.strings.insert(Arc::from("calls"), empty_calls_array());
|
|
73
|
+
b.strings.insert(Arc::from("callCount"), Value::Number(0.0));
|
|
74
|
+
b.strings
|
|
75
|
+
.insert(Arc::from("implementation"), impl_reset.clone());
|
|
76
|
+
Value::Null
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
meta
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fn wrap_mock_fn(meta: Value) -> Value {
|
|
84
|
+
let meta_call = meta.clone();
|
|
85
|
+
Value::native(move |args: &[Value]| {
|
|
86
|
+
bump_calls(&meta_call, args);
|
|
87
|
+
let impl_fn = match &meta_call {
|
|
88
|
+
Value::Object(o) => o
|
|
89
|
+
.borrow()
|
|
90
|
+
.strings
|
|
91
|
+
.get("implementation")
|
|
92
|
+
.cloned()
|
|
93
|
+
.unwrap_or(Value::Null),
|
|
94
|
+
_ => Value::Null,
|
|
95
|
+
};
|
|
96
|
+
value_call(&impl_fn, args)
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Set `implementation` on a mock's metadata object.
|
|
101
|
+
fn set_implementation(meta: &Value, f: Value) {
|
|
102
|
+
if let Value::Object(o) = meta {
|
|
103
|
+
o.borrow_mut()
|
|
104
|
+
.strings
|
|
105
|
+
.insert(Arc::from("implementation"), f);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Jest/Bun put `mockClear` / `mockImplementation` / `mockReturnValue` on the mock function
|
|
110
|
+
/// itself, not on `.mock`. Attach both so either spelling works.
|
|
111
|
+
fn attach_mock_controls(outer: &mut ObjectMap, meta: &Value) {
|
|
112
|
+
if let Value::Object(o) = meta {
|
|
113
|
+
for name in ["mockClear", "mockReset"] {
|
|
114
|
+
let existing = o.borrow().strings.get(name).cloned();
|
|
115
|
+
if let Some(f) = existing {
|
|
116
|
+
outer.insert(Arc::from(name), f);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
let m_impl = meta.clone();
|
|
121
|
+
outer.insert(
|
|
122
|
+
Arc::from("mockImplementation"),
|
|
123
|
+
Value::native(move |args: &[Value]| {
|
|
124
|
+
set_implementation(&m_impl, args.first().cloned().unwrap_or(Value::Null));
|
|
125
|
+
Value::Null
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
let m_once = meta.clone();
|
|
129
|
+
outer.insert(
|
|
130
|
+
Arc::from("mockImplementationOnce"),
|
|
131
|
+
Value::native(move |args: &[Value]| {
|
|
132
|
+
set_implementation(&m_once, args.first().cloned().unwrap_or(Value::Null));
|
|
133
|
+
Value::Null
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
let m_ret = meta.clone();
|
|
137
|
+
outer.insert(
|
|
138
|
+
Arc::from("mockReturnValue"),
|
|
139
|
+
Value::native(move |args: &[Value]| {
|
|
140
|
+
let v = args.first().cloned().unwrap_or(Value::Null);
|
|
141
|
+
set_implementation(&m_ret, Value::native(move |_a: &[Value]| v.clone()));
|
|
142
|
+
Value::Null
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
let m_res = meta.clone();
|
|
146
|
+
outer.insert(
|
|
147
|
+
Arc::from("mockResolvedValue"),
|
|
148
|
+
Value::native(move |args: &[Value]| {
|
|
149
|
+
let v = args.first().cloned().unwrap_or(Value::Null);
|
|
150
|
+
// No promise construction here: a resolved value is what `await` yields anyway.
|
|
151
|
+
set_implementation(&m_res, Value::native(move |_a: &[Value]| v.clone()));
|
|
152
|
+
Value::Null
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// `mock(fn?)` — callable mock object with `.mock.calls` / `.mockClear`.
|
|
158
|
+
pub fn mock(args: &[Value]) -> Value {
|
|
159
|
+
let impl_fn = args
|
|
160
|
+
.first()
|
|
161
|
+
.cloned()
|
|
162
|
+
.unwrap_or_else(|| Value::native(|_a: &[Value]| Value::Null));
|
|
163
|
+
let meta = make_mock_meta(impl_fn);
|
|
164
|
+
let call = wrap_mock_fn(meta.clone());
|
|
165
|
+
let mut outer = ObjectMap::default();
|
|
166
|
+
outer.insert(Arc::from("__call"), call);
|
|
167
|
+
outer.insert(Arc::from("mock"), meta.clone());
|
|
168
|
+
attach_mock_controls(&mut outer, &meta);
|
|
169
|
+
let obj = Value::object(outer);
|
|
170
|
+
MOCKS.with(|m| {
|
|
171
|
+
m.borrow_mut().push(MockHandle {
|
|
172
|
+
meta,
|
|
173
|
+
restore: None,
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
obj
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// `spyOn(obj, methodName)` — wraps a method; restore via `restoreAllMocks`.
|
|
180
|
+
pub fn spy_on(args: &[Value]) -> Value {
|
|
181
|
+
let target = args.first().cloned().unwrap_or(Value::Null);
|
|
182
|
+
let prop = args
|
|
183
|
+
.get(1)
|
|
184
|
+
.map(|v| v.to_display_string())
|
|
185
|
+
.unwrap_or_default();
|
|
186
|
+
let original = match &target {
|
|
187
|
+
Value::Object(o) => o
|
|
188
|
+
.borrow()
|
|
189
|
+
.strings
|
|
190
|
+
.get(prop.as_str())
|
|
191
|
+
.cloned()
|
|
192
|
+
.unwrap_or(Value::Null),
|
|
193
|
+
_ => Value::Null,
|
|
194
|
+
};
|
|
195
|
+
let meta = make_mock_meta(original.clone());
|
|
196
|
+
let call = wrap_mock_fn(meta.clone());
|
|
197
|
+
let mut outer = ObjectMap::default();
|
|
198
|
+
outer.insert(Arc::from("__call"), call);
|
|
199
|
+
outer.insert(Arc::from("mock"), meta.clone());
|
|
200
|
+
attach_mock_controls(&mut outer, &meta);
|
|
201
|
+
let spy = Value::object(outer);
|
|
202
|
+
if !matches!(target, Value::Object(_)) {
|
|
203
|
+
// Silently returning an unattached spy made the whole test a no-op.
|
|
204
|
+
tishlang_core::set_pending_throw(crate::assert::assertion_error(
|
|
205
|
+
format!(
|
|
206
|
+
"spyOn(target, {prop:?}): target is not an object ({})",
|
|
207
|
+
target.to_display_string()
|
|
208
|
+
),
|
|
209
|
+
Some(target.clone()),
|
|
210
|
+
None,
|
|
211
|
+
"spyOn",
|
|
212
|
+
true,
|
|
213
|
+
));
|
|
214
|
+
return Value::Null;
|
|
215
|
+
}
|
|
216
|
+
if let Value::Object(o) = &target {
|
|
217
|
+
o.borrow_mut()
|
|
218
|
+
.strings
|
|
219
|
+
.insert(Arc::from(prop.as_str()), spy.clone());
|
|
220
|
+
}
|
|
221
|
+
MOCKS.with(|m| {
|
|
222
|
+
m.borrow_mut().push(MockHandle {
|
|
223
|
+
meta,
|
|
224
|
+
restore: Some((target, prop, original)),
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
spy
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
pub fn clear_all_mocks(_args: &[Value]) -> Value {
|
|
231
|
+
MOCKS.with(|m| {
|
|
232
|
+
for h in m.borrow().iter() {
|
|
233
|
+
if let Value::Object(o) = &h.meta {
|
|
234
|
+
let mut b = o.borrow_mut();
|
|
235
|
+
b.strings.insert(Arc::from("calls"), empty_calls_array());
|
|
236
|
+
b.strings.insert(Arc::from("callCount"), Value::Number(0.0));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
Value::Null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
pub fn restore_all_mocks(_args: &[Value]) -> Value {
|
|
244
|
+
MOCKS.with(|m| {
|
|
245
|
+
let mut list = m.borrow_mut();
|
|
246
|
+
// Restore newest-first: spying twice on one method must land back on the ORIGINAL,
|
|
247
|
+
// not on the first spy.
|
|
248
|
+
for h in list.drain(..).rev() {
|
|
249
|
+
if let Some((Value::Object(o), prop, original)) = h.restore {
|
|
250
|
+
o.borrow_mut()
|
|
251
|
+
.strings
|
|
252
|
+
.insert(Arc::from(prop.as_str()), original);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
Value::Null
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/// Drop all mock/spy handles between test files (restore spies first).
|
|
260
|
+
pub fn reset_mock_registry() {
|
|
261
|
+
let _ = restore_all_mocks(&[]);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
pub fn mock_native(args: &[Value]) -> Value {
|
|
265
|
+
mock(args)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
pub fn spy_on_native(args: &[Value]) -> Value {
|
|
269
|
+
spy_on(args)
|
|
270
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
//! `tish:test` and `tish:assert` module export maps.
|
|
2
|
+
|
|
3
|
+
use tishlang_core::{value_call, Arc, ObjectMap, Value};
|
|
4
|
+
|
|
5
|
+
use crate::assert::assert_object;
|
|
6
|
+
use crate::expect::expect_object;
|
|
7
|
+
use crate::mocks::{clear_all_mocks, mock, restore_all_mocks, spy_on};
|
|
8
|
+
use crate::registry::{add_hook, add_test, push_describe, TestMode};
|
|
9
|
+
|
|
10
|
+
fn name_and_fn(args: &[Value]) -> (String, Value) {
|
|
11
|
+
match args.len() {
|
|
12
|
+
0 => (String::new(), Value::Null),
|
|
13
|
+
1 => {
|
|
14
|
+
if matches!(args[0], Value::Function(_) | Value::Object(_)) {
|
|
15
|
+
("<anonymous>".into(), args[0].clone())
|
|
16
|
+
} else {
|
|
17
|
+
(args[0].to_display_string(), Value::Null)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
_ => {
|
|
21
|
+
let name = args[0].to_display_string();
|
|
22
|
+
let body = args.last().cloned().unwrap_or(Value::Null);
|
|
23
|
+
(name, body)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fn opts_object(args: &[Value]) -> Option<Value> {
|
|
29
|
+
if args.len() >= 3 {
|
|
30
|
+
if let Value::Object(_) = &args[1] {
|
|
31
|
+
return Some(args[1].clone());
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
None
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn timeout_from_args(args: &[Value]) -> Option<u64> {
|
|
38
|
+
let v = opts_object(args)?;
|
|
39
|
+
let Value::Object(o) = v else {
|
|
40
|
+
return None;
|
|
41
|
+
};
|
|
42
|
+
let n = {
|
|
43
|
+
let b = o.borrow();
|
|
44
|
+
b.strings
|
|
45
|
+
.get("timeout")
|
|
46
|
+
.and_then(|v| v.as_number())
|
|
47
|
+
.filter(|n| n.is_finite() && *n >= 0.0)
|
|
48
|
+
}?;
|
|
49
|
+
Some(n as u64)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn tags_from_args(args: &[Value]) -> Vec<String> {
|
|
53
|
+
let Some(Value::Object(o)) = opts_object(args) else {
|
|
54
|
+
return Vec::new();
|
|
55
|
+
};
|
|
56
|
+
let b = o.borrow();
|
|
57
|
+
match b.strings.get("tags") {
|
|
58
|
+
Some(Value::Array(a)) => a.borrow().iter().map(|v| v.to_display_string()).collect(),
|
|
59
|
+
Some(Value::String(s)) => vec![s.to_string()],
|
|
60
|
+
_ => Vec::new(),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn is_truthy(v: &Value) -> bool {
|
|
65
|
+
match v {
|
|
66
|
+
Value::Null => false,
|
|
67
|
+
Value::Bool(b) => *b,
|
|
68
|
+
Value::Number(n) => *n != 0.0 && !n.is_nan(),
|
|
69
|
+
Value::String(s) => !s.is_empty(),
|
|
70
|
+
_ => true,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fn row_args(row: &Value) -> Vec<Value> {
|
|
75
|
+
match row {
|
|
76
|
+
Value::Array(a) => a.borrow().clone(),
|
|
77
|
+
Value::NumberArray(a) => a.borrow().to_values(),
|
|
78
|
+
other => vec![other.clone()],
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn format_each_name(template: &str, row: &Value, index: usize) -> String {
|
|
83
|
+
if !template.contains('%') {
|
|
84
|
+
return format!("{template} [{index}]");
|
|
85
|
+
}
|
|
86
|
+
let args = row_args(row);
|
|
87
|
+
let mut out = String::new();
|
|
88
|
+
let mut chars = template.chars().peekable();
|
|
89
|
+
let mut ai = 0usize;
|
|
90
|
+
while let Some(c) = chars.next() {
|
|
91
|
+
if c == '%' {
|
|
92
|
+
match chars.peek().copied() {
|
|
93
|
+
Some('#') => {
|
|
94
|
+
chars.next();
|
|
95
|
+
out.push_str(&index.to_string());
|
|
96
|
+
}
|
|
97
|
+
Some('s') | Some('i') | Some('d') | Some('p') | Some('j') => {
|
|
98
|
+
chars.next();
|
|
99
|
+
let v = args.get(ai).cloned().unwrap_or(Value::Null);
|
|
100
|
+
ai += 1;
|
|
101
|
+
out.push_str(&v.to_display_string());
|
|
102
|
+
}
|
|
103
|
+
Some('%') => {
|
|
104
|
+
chars.next();
|
|
105
|
+
out.push('%');
|
|
106
|
+
}
|
|
107
|
+
_ => out.push('%'),
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
out.push(c);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
out
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fn make_test_call(mode: TestMode) -> Value {
|
|
117
|
+
Value::native(move |args: &[Value]| {
|
|
118
|
+
let (name, body) = name_and_fn(args);
|
|
119
|
+
let timeout = timeout_from_args(args);
|
|
120
|
+
let tags = tags_from_args(args);
|
|
121
|
+
// Parallel `test.concurrent` is deferred (single-threaded VM); `{ concurrent: true }`
|
|
122
|
+
// is accepted and ignored so Bun/Jest suites port without edits.
|
|
123
|
+
add_test(&name, mode, body, timeout, tags);
|
|
124
|
+
Value::Null
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn make_each_call(mode: TestMode) -> Value {
|
|
129
|
+
Value::native(move |args: &[Value]| {
|
|
130
|
+
let table = args.first().cloned().unwrap_or(Value::Null);
|
|
131
|
+
Value::native(move |inner: &[Value]| {
|
|
132
|
+
let (name_tmpl, body) = name_and_fn(inner);
|
|
133
|
+
let timeout = timeout_from_args(inner);
|
|
134
|
+
let rows: Vec<Value> = match &table {
|
|
135
|
+
Value::Array(a) => a.borrow().clone(),
|
|
136
|
+
Value::NumberArray(a) => a.borrow().to_values(),
|
|
137
|
+
other => vec![other.clone()],
|
|
138
|
+
};
|
|
139
|
+
for (i, row) in rows.into_iter().enumerate() {
|
|
140
|
+
let name = format_each_name(&name_tmpl, &row, i);
|
|
141
|
+
let body = body.clone();
|
|
142
|
+
let row_for_call = row;
|
|
143
|
+
let wrapped = Value::native(move |_a: &[Value]| {
|
|
144
|
+
let call_args = row_args(&row_for_call);
|
|
145
|
+
value_call(&body, &call_args)
|
|
146
|
+
});
|
|
147
|
+
add_test(&name, mode, wrapped, timeout, Vec::new());
|
|
148
|
+
}
|
|
149
|
+
Value::Null
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn attach_each(target: &mut ObjectMap, mode: TestMode) {
|
|
155
|
+
target.insert(Arc::from("each"), make_each_call(mode));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn test_object() -> Value {
|
|
159
|
+
let mut m = ObjectMap::default();
|
|
160
|
+
m.insert(Arc::from("__call"), make_test_call(TestMode::Normal));
|
|
161
|
+
attach_each(&mut m, TestMode::Normal);
|
|
162
|
+
m.insert(Arc::from("skipIf"), {
|
|
163
|
+
Value::native(|args: &[Value]| {
|
|
164
|
+
let skip = args.first().map(is_truthy).unwrap_or(false);
|
|
165
|
+
make_test_call(if skip {
|
|
166
|
+
TestMode::Skip
|
|
167
|
+
} else {
|
|
168
|
+
TestMode::Normal
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
});
|
|
172
|
+
m.insert(Arc::from("skip"), {
|
|
173
|
+
let mut o = ObjectMap::default();
|
|
174
|
+
o.insert(Arc::from("__call"), make_test_call(TestMode::Skip));
|
|
175
|
+
attach_each(&mut o, TestMode::Skip);
|
|
176
|
+
Value::object(o)
|
|
177
|
+
});
|
|
178
|
+
m.insert(Arc::from("only"), {
|
|
179
|
+
let mut o = ObjectMap::default();
|
|
180
|
+
o.insert(Arc::from("__call"), make_test_call(TestMode::Only));
|
|
181
|
+
attach_each(&mut o, TestMode::Only);
|
|
182
|
+
Value::object(o)
|
|
183
|
+
});
|
|
184
|
+
m.insert(Arc::from("todo"), {
|
|
185
|
+
let mut o = ObjectMap::default();
|
|
186
|
+
o.insert(
|
|
187
|
+
Arc::from("__call"),
|
|
188
|
+
Value::native(|args: &[Value]| {
|
|
189
|
+
let name = args
|
|
190
|
+
.first()
|
|
191
|
+
.map(|v| v.to_display_string())
|
|
192
|
+
.unwrap_or_else(|| "<todo>".into());
|
|
193
|
+
add_test(&name, TestMode::Todo, Value::Null, None, Vec::new());
|
|
194
|
+
Value::Null
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
Value::object(o)
|
|
198
|
+
});
|
|
199
|
+
m.insert(Arc::from("failing"), {
|
|
200
|
+
let mut o = ObjectMap::default();
|
|
201
|
+
o.insert(Arc::from("__call"), make_test_call(TestMode::Failing));
|
|
202
|
+
attach_each(&mut o, TestMode::Failing);
|
|
203
|
+
Value::object(o)
|
|
204
|
+
});
|
|
205
|
+
Value::object(m)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
fn make_describe_call(mode: TestMode) -> Value {
|
|
209
|
+
Value::native(move |args: &[Value]| {
|
|
210
|
+
let name = args
|
|
211
|
+
.first()
|
|
212
|
+
.map(|v| v.to_display_string())
|
|
213
|
+
.unwrap_or_default();
|
|
214
|
+
let body = args.get(1).cloned().unwrap_or(Value::Null);
|
|
215
|
+
push_describe(&name, mode, &body);
|
|
216
|
+
Value::Null
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fn describe_object() -> Value {
|
|
221
|
+
let mut m = ObjectMap::default();
|
|
222
|
+
m.insert(Arc::from("__call"), make_describe_call(TestMode::Normal));
|
|
223
|
+
m.insert(Arc::from("skip"), {
|
|
224
|
+
let mut o = ObjectMap::default();
|
|
225
|
+
o.insert(Arc::from("__call"), make_describe_call(TestMode::Skip));
|
|
226
|
+
Value::object(o)
|
|
227
|
+
});
|
|
228
|
+
m.insert(Arc::from("only"), {
|
|
229
|
+
let mut o = ObjectMap::default();
|
|
230
|
+
o.insert(Arc::from("__call"), make_describe_call(TestMode::Only));
|
|
231
|
+
Value::object(o)
|
|
232
|
+
});
|
|
233
|
+
// `describe.each` — table-driven suites (same table shape as `test.each`).
|
|
234
|
+
m.insert(
|
|
235
|
+
Arc::from("each"),
|
|
236
|
+
Value::native(move |args: &[Value]| {
|
|
237
|
+
let table = args.first().cloned().unwrap_or(Value::Null);
|
|
238
|
+
Value::native(move |inner: &[Value]| {
|
|
239
|
+
let name_tmpl = inner
|
|
240
|
+
.first()
|
|
241
|
+
.map(|v| v.to_display_string())
|
|
242
|
+
.unwrap_or_default();
|
|
243
|
+
let body = inner.get(1).cloned().unwrap_or(Value::Null);
|
|
244
|
+
let rows: Vec<Value> = match &table {
|
|
245
|
+
Value::Array(a) => a.borrow().clone(),
|
|
246
|
+
Value::NumberArray(a) => a.borrow().to_values(),
|
|
247
|
+
other => vec![other.clone()],
|
|
248
|
+
};
|
|
249
|
+
for (i, row) in rows.into_iter().enumerate() {
|
|
250
|
+
let name = format_each_name(&name_tmpl, &row, i);
|
|
251
|
+
let body = body.clone();
|
|
252
|
+
let row_for_call = row;
|
|
253
|
+
let wrapped = Value::native(move |_a: &[Value]| {
|
|
254
|
+
let call_args = row_args(&row_for_call);
|
|
255
|
+
value_call(&body, &call_args)
|
|
256
|
+
});
|
|
257
|
+
push_describe(&name, TestMode::Normal, &wrapped);
|
|
258
|
+
}
|
|
259
|
+
Value::Null
|
|
260
|
+
})
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
Value::object(m)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
fn hook(kind: &'static str) -> Value {
|
|
267
|
+
Value::native(move |args: &[Value]| {
|
|
268
|
+
let body = args.first().cloned().unwrap_or(Value::Null);
|
|
269
|
+
add_hook(kind, body);
|
|
270
|
+
Value::Null
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// Named exports for `import { … } from "tish:test"`.
|
|
275
|
+
pub fn test_module() -> ObjectMap {
|
|
276
|
+
let test = test_object();
|
|
277
|
+
let describe = describe_object();
|
|
278
|
+
let expect = expect_object();
|
|
279
|
+
|
|
280
|
+
let mut m = ObjectMap::default();
|
|
281
|
+
m.insert(Arc::from("describe"), describe.clone());
|
|
282
|
+
m.insert(Arc::from("suite"), describe);
|
|
283
|
+
m.insert(Arc::from("test"), test.clone());
|
|
284
|
+
m.insert(Arc::from("it"), test);
|
|
285
|
+
m.insert(Arc::from("expect"), expect);
|
|
286
|
+
m.insert(Arc::from("beforeAll"), hook("beforeAll"));
|
|
287
|
+
m.insert(Arc::from("afterAll"), hook("afterAll"));
|
|
288
|
+
m.insert(Arc::from("beforeEach"), hook("beforeEach"));
|
|
289
|
+
m.insert(Arc::from("afterEach"), hook("afterEach"));
|
|
290
|
+
m.insert(Arc::from("before"), hook("before"));
|
|
291
|
+
m.insert(Arc::from("after"), hook("after"));
|
|
292
|
+
m.insert(Arc::from("mock"), Value::native(mock));
|
|
293
|
+
m.insert(Arc::from("spyOn"), Value::native(spy_on));
|
|
294
|
+
m.insert(Arc::from("clearAllMocks"), Value::native(clear_all_mocks));
|
|
295
|
+
m.insert(
|
|
296
|
+
Arc::from("restoreAllMocks"),
|
|
297
|
+
Value::native(restore_all_mocks),
|
|
298
|
+
);
|
|
299
|
+
let default_test = m.get("test").cloned().unwrap_or(Value::Null);
|
|
300
|
+
m.insert(Arc::from("default"), default_test);
|
|
301
|
+
m
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/// Named + default exports for `tish:assert` / `node:assert`.
|
|
305
|
+
pub fn assert_module() -> ObjectMap {
|
|
306
|
+
let assert = assert_object();
|
|
307
|
+
let mut m = ObjectMap::default();
|
|
308
|
+
if let Value::Object(o) = &assert {
|
|
309
|
+
for (k, v) in o.borrow().strings.iter() {
|
|
310
|
+
if k.as_ref() != "__call" {
|
|
311
|
+
m.insert(Arc::clone(k), v.clone());
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
m.insert(Arc::from("assert"), assert.clone());
|
|
316
|
+
m.insert(Arc::from("default"), assert);
|
|
317
|
+
m
|
|
318
|
+
}
|