@tishlang/tish-lsp 3.3.3 → 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-lsp +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/scope_merge.rs +90 -0
- package/crates/tish/tests/test_runner.rs +673 -0
- package/crates/tish_bytecode/src/compiler.rs +25 -4
- package/crates/tish_compile/src/check.rs +178 -0
- package/crates/tish_compile/src/codegen.rs +383 -63
- package/crates/tish_compile/src/lib.rs +33 -1
- package/crates/tish_compile/src/resolve.rs +121 -19
- package/crates/tish_compile/src/types.rs +35 -1
- package/crates/tish_compile/tests/call_arity_641.rs +104 -0
- 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_lsp/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-lsp +0 -0
- package/platform/darwin-x64/tish-lsp +0 -0
- package/platform/linux-arm64/tish-lsp +0 -0
- package/platform/linux-x64/tish-lsp +0 -0
- package/platform/win32-x64/tish-lsp.exe +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "tishlang_test"
|
|
3
|
+
version = "0.0.0-dev"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
description = "Native tish test runner, expect, and Node-compatible assert"
|
|
6
|
+
license-file = { workspace = true }
|
|
7
|
+
repository = { workspace = true }
|
|
8
|
+
|
|
9
|
+
[features]
|
|
10
|
+
default = ["fs", "regex", "promise"]
|
|
11
|
+
# Fan features into runtime + optional eval/vm so send-values / regex stay consistent.
|
|
12
|
+
fs = ["tishlang_runtime/fs", "tishlang_eval/fs", "tishlang_vm/fs"]
|
|
13
|
+
regex = [
|
|
14
|
+
"tishlang_core/regex",
|
|
15
|
+
"tishlang_runtime/regex",
|
|
16
|
+
"tishlang_eval/regex",
|
|
17
|
+
"tishlang_vm/regex",
|
|
18
|
+
]
|
|
19
|
+
promise = ["tishlang_runtime/promise", "tishlang_vm/promise"]
|
|
20
|
+
# Full runner needs compile + VM to load files
|
|
21
|
+
runner = [
|
|
22
|
+
"dep:tishlang_compile",
|
|
23
|
+
"dep:tishlang_opt",
|
|
24
|
+
"dep:tishlang_bytecode",
|
|
25
|
+
"dep:tishlang_vm",
|
|
26
|
+
"dep:tishlang_ast",
|
|
27
|
+
"dep:tishlang_eval",
|
|
28
|
+
]
|
|
29
|
+
watch = ["dep:notify"]
|
|
30
|
+
|
|
31
|
+
[dependencies]
|
|
32
|
+
tishlang_core = { path = "../tish_core", version = ">=0.1" }
|
|
33
|
+
tishlang_builtins = { path = "../tish_builtins", version = ">=0.1" }
|
|
34
|
+
tishlang_runtime = { path = "../tish_runtime", version = ">=0.1" }
|
|
35
|
+
tishlang_ast = { path = "../tish_ast", version = ">=0.1", optional = true }
|
|
36
|
+
tishlang_compile = { path = "../tish_compile", version = ">=0.1", optional = true }
|
|
37
|
+
tishlang_opt = { path = "../tish_opt", version = ">=0.1", optional = true }
|
|
38
|
+
tishlang_bytecode = { path = "../tish_bytecode", version = ">=0.1", optional = true }
|
|
39
|
+
tishlang_vm = { path = "../tish_vm", version = ">=0.1", optional = true }
|
|
40
|
+
tishlang_eval = { path = "../tish_eval", version = ">=0.1", optional = true }
|
|
41
|
+
serde_json = "1"
|
|
42
|
+
walkdir = "2"
|
|
43
|
+
regex = "1"
|
|
44
|
+
notify = { version = "7", optional = true }
|
|
45
|
+
|
|
46
|
+
[dev-dependencies]
|
|
47
|
+
tempfile = "3"
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
//! Node-compatible `assert` / `tish:assert` surface.
|
|
2
|
+
|
|
3
|
+
use tishlang_core::{
|
|
4
|
+
has_pending_throw, set_pending_throw, take_pending_throw, value_call, Arc, ObjectMap, Value,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
use crate::deep_equal::{deep_strict_equal, partial_deep_strict_equal};
|
|
8
|
+
|
|
9
|
+
pub fn assertion_error(
|
|
10
|
+
message: impl Into<String>,
|
|
11
|
+
actual: Option<Value>,
|
|
12
|
+
expected: Option<Value>,
|
|
13
|
+
operator: &str,
|
|
14
|
+
generated_message: bool,
|
|
15
|
+
) -> Value {
|
|
16
|
+
let msg = message.into();
|
|
17
|
+
let mut m = ObjectMap::default();
|
|
18
|
+
m.insert(Arc::from("name"), Value::String("AssertionError".into()));
|
|
19
|
+
m.insert(Arc::from("message"), Value::String(msg.into()));
|
|
20
|
+
m.insert(Arc::from("code"), Value::String("ERR_ASSERTION".into()));
|
|
21
|
+
m.insert(Arc::from("operator"), Value::String(operator.into()));
|
|
22
|
+
m.insert(
|
|
23
|
+
Arc::from("generatedMessage"),
|
|
24
|
+
Value::Bool(generated_message),
|
|
25
|
+
);
|
|
26
|
+
if let Some(a) = actual {
|
|
27
|
+
m.insert(Arc::from("actual"), a);
|
|
28
|
+
}
|
|
29
|
+
if let Some(e) = expected {
|
|
30
|
+
m.insert(Arc::from("expected"), e);
|
|
31
|
+
}
|
|
32
|
+
Value::object(m)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn throw_err(
|
|
36
|
+
message: impl Into<String>,
|
|
37
|
+
actual: Option<Value>,
|
|
38
|
+
expected: Option<Value>,
|
|
39
|
+
operator: &str,
|
|
40
|
+
) -> Value {
|
|
41
|
+
set_pending_throw(assertion_error(message, actual, expected, operator, true));
|
|
42
|
+
Value::Null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn is_truthy(v: &Value) -> bool {
|
|
46
|
+
match v {
|
|
47
|
+
Value::Null => false,
|
|
48
|
+
Value::Bool(b) => *b,
|
|
49
|
+
Value::Number(n) => *n != 0.0 && !n.is_nan(),
|
|
50
|
+
Value::String(s) => !s.is_empty(),
|
|
51
|
+
_ => true,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn ok(args: &[Value]) -> Value {
|
|
56
|
+
let v = args.first().cloned().unwrap_or(Value::Null);
|
|
57
|
+
let msg = args
|
|
58
|
+
.get(1)
|
|
59
|
+
.map(|m| m.to_display_string())
|
|
60
|
+
.unwrap_or_else(|| "The expression evaluated to a falsy value".into());
|
|
61
|
+
if !is_truthy(&v) {
|
|
62
|
+
return throw_err(msg, Some(v), Some(Value::Bool(true)), "==");
|
|
63
|
+
}
|
|
64
|
+
Value::Null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fn strict_equal(args: &[Value]) -> Value {
|
|
68
|
+
let actual = args.first().cloned().unwrap_or(Value::Null);
|
|
69
|
+
let expected = args.get(1).cloned().unwrap_or(Value::Null);
|
|
70
|
+
let msg = args.get(2).map(|m| m.to_display_string());
|
|
71
|
+
if !actual.strict_eq(&expected) {
|
|
72
|
+
let m = msg.unwrap_or_else(|| {
|
|
73
|
+
format!(
|
|
74
|
+
"Expected values to be strictly equal:\n+ actual: {}\n- expected: {}",
|
|
75
|
+
actual.to_display_string(),
|
|
76
|
+
expected.to_display_string()
|
|
77
|
+
)
|
|
78
|
+
});
|
|
79
|
+
return throw_err(m, Some(actual), Some(expected), "strictEqual");
|
|
80
|
+
}
|
|
81
|
+
Value::Null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fn not_strict_equal(args: &[Value]) -> Value {
|
|
85
|
+
let actual = args.first().cloned().unwrap_or(Value::Null);
|
|
86
|
+
let expected = args.get(1).cloned().unwrap_or(Value::Null);
|
|
87
|
+
let msg = args.get(2).map(|m| m.to_display_string());
|
|
88
|
+
if actual.strict_eq(&expected) {
|
|
89
|
+
let m = msg.unwrap_or_else(|| {
|
|
90
|
+
format!(
|
|
91
|
+
"Expected values to be strictly unequal:\nActual: {}",
|
|
92
|
+
actual.to_display_string()
|
|
93
|
+
)
|
|
94
|
+
});
|
|
95
|
+
return throw_err(m, Some(actual), Some(expected), "notStrictEqual");
|
|
96
|
+
}
|
|
97
|
+
Value::Null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn deep_strict(args: &[Value]) -> Value {
|
|
101
|
+
let actual = args.first().cloned().unwrap_or(Value::Null);
|
|
102
|
+
let expected = args.get(1).cloned().unwrap_or(Value::Null);
|
|
103
|
+
let msg = args.get(2).map(|m| m.to_display_string());
|
|
104
|
+
if !deep_strict_equal(&actual, &expected) {
|
|
105
|
+
let m = msg.unwrap_or_else(|| {
|
|
106
|
+
format!(
|
|
107
|
+
"Expected values to be strictly deep-equal:\n+ actual: {}\n- expected: {}",
|
|
108
|
+
actual.to_display_string(),
|
|
109
|
+
expected.to_display_string()
|
|
110
|
+
)
|
|
111
|
+
});
|
|
112
|
+
return throw_err(m, Some(actual), Some(expected), "deepStrictEqual");
|
|
113
|
+
}
|
|
114
|
+
Value::Null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn not_deep_strict(args: &[Value]) -> Value {
|
|
118
|
+
let actual = args.first().cloned().unwrap_or(Value::Null);
|
|
119
|
+
let expected = args.get(1).cloned().unwrap_or(Value::Null);
|
|
120
|
+
let msg = args.get(2).map(|m| m.to_display_string());
|
|
121
|
+
if deep_strict_equal(&actual, &expected) {
|
|
122
|
+
let m = msg.unwrap_or_else(|| {
|
|
123
|
+
format!(
|
|
124
|
+
"Expected values not to be strictly deep-equal:\nActual: {}",
|
|
125
|
+
actual.to_display_string()
|
|
126
|
+
)
|
|
127
|
+
});
|
|
128
|
+
return throw_err(m, Some(actual), Some(expected), "notDeepStrictEqual");
|
|
129
|
+
}
|
|
130
|
+
Value::Null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fn partial_deep(args: &[Value]) -> Value {
|
|
134
|
+
let actual = args.first().cloned().unwrap_or(Value::Null);
|
|
135
|
+
let expected = args.get(1).cloned().unwrap_or(Value::Null);
|
|
136
|
+
let msg = args.get(2).map(|m| m.to_display_string());
|
|
137
|
+
if !partial_deep_strict_equal(&actual, &expected) {
|
|
138
|
+
let m = msg.unwrap_or_else(|| {
|
|
139
|
+
format!(
|
|
140
|
+
"Expected object to match partial:\n+ actual: {}\n- expected: {}",
|
|
141
|
+
actual.to_display_string(),
|
|
142
|
+
expected.to_display_string()
|
|
143
|
+
)
|
|
144
|
+
});
|
|
145
|
+
return throw_err(m, Some(actual), Some(expected), "partialDeepStrictEqual");
|
|
146
|
+
}
|
|
147
|
+
Value::Null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn match_fn(args: &[Value]) -> Value {
|
|
151
|
+
let string = args
|
|
152
|
+
.first()
|
|
153
|
+
.map(|v| v.to_display_string())
|
|
154
|
+
.unwrap_or_default();
|
|
155
|
+
let regexp = args.get(1).cloned().unwrap_or(Value::Null);
|
|
156
|
+
let msg = args.get(2).map(|m| m.to_display_string());
|
|
157
|
+
let ok = regexp_test(®exp, &string);
|
|
158
|
+
if !ok {
|
|
159
|
+
let m = msg.unwrap_or_else(|| {
|
|
160
|
+
format!(
|
|
161
|
+
"The input did not match the regular expression {}:\n{}",
|
|
162
|
+
regexp.to_display_string(),
|
|
163
|
+
string
|
|
164
|
+
)
|
|
165
|
+
});
|
|
166
|
+
return throw_err(m, Some(Value::String(string.into())), Some(regexp), "match");
|
|
167
|
+
}
|
|
168
|
+
Value::Null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fn does_not_match(args: &[Value]) -> Value {
|
|
172
|
+
let string = args
|
|
173
|
+
.first()
|
|
174
|
+
.map(|v| v.to_display_string())
|
|
175
|
+
.unwrap_or_default();
|
|
176
|
+
let regexp = args.get(1).cloned().unwrap_or(Value::Null);
|
|
177
|
+
let msg = args.get(2).map(|m| m.to_display_string());
|
|
178
|
+
if regexp_test(®exp, &string) {
|
|
179
|
+
let m = msg.unwrap_or_else(|| {
|
|
180
|
+
format!(
|
|
181
|
+
"The input was expected to not match {}:\n{}",
|
|
182
|
+
regexp.to_display_string(),
|
|
183
|
+
string
|
|
184
|
+
)
|
|
185
|
+
});
|
|
186
|
+
return throw_err(
|
|
187
|
+
m,
|
|
188
|
+
Some(Value::String(string.into())),
|
|
189
|
+
Some(regexp),
|
|
190
|
+
"doesNotMatch",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
Value::Null
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fn regexp_test(re: &Value, s: &str) -> bool {
|
|
197
|
+
#[cfg(feature = "regex")]
|
|
198
|
+
{
|
|
199
|
+
if let Value::RegExp(r) = re {
|
|
200
|
+
return r.borrow_mut().test(s);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
let pat = re.to_display_string();
|
|
204
|
+
regex::Regex::new(&pat)
|
|
205
|
+
.map(|r| r.is_match(s))
|
|
206
|
+
.unwrap_or(false)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// Node's `assert.throws(fn[, error][, message])`: `error` is a validator (regex / message
|
|
210
|
+
/// substring / `Error`-shaped object), `message` is the failure text.
|
|
211
|
+
fn split_error_and_message(args: &[Value]) -> (Option<Value>, Option<String>) {
|
|
212
|
+
match (args.get(1), args.get(2)) {
|
|
213
|
+
(Some(e), Some(m)) => (Some(e.clone()), Some(m.to_display_string())),
|
|
214
|
+
(Some(e), None) => (Some(e.clone()), None),
|
|
215
|
+
_ => (None, None),
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
fn throws(args: &[Value]) -> Value {
|
|
220
|
+
let fn_v = args.first().cloned().unwrap_or(Value::Null);
|
|
221
|
+
let (expected, message) = split_error_and_message(args);
|
|
222
|
+
if !crate::expect::is_callable(&fn_v) {
|
|
223
|
+
return throw_err(
|
|
224
|
+
format!(
|
|
225
|
+
"assert.throws expects a function, got {}",
|
|
226
|
+
fn_v.to_display_string()
|
|
227
|
+
),
|
|
228
|
+
Some(fn_v),
|
|
229
|
+
None,
|
|
230
|
+
"throws",
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
let _ = take_pending_throw();
|
|
234
|
+
let _ = value_call(&fn_v, &[]);
|
|
235
|
+
if !has_pending_throw() {
|
|
236
|
+
let msg = message.unwrap_or_else(|| "Missing expected exception".into());
|
|
237
|
+
return throw_err(msg, None, expected, "throws");
|
|
238
|
+
}
|
|
239
|
+
let err = take_pending_throw().unwrap_or(Value::Null);
|
|
240
|
+
if let Some(expected) = expected {
|
|
241
|
+
// Previously the second argument was treated as the failure message, so ANY throw
|
|
242
|
+
// satisfied `assert.throws(fn, /pattern/)`. Validate it.
|
|
243
|
+
if !crate::expect::error_matches(&err, &expected) {
|
|
244
|
+
let msg = message.unwrap_or_else(|| {
|
|
245
|
+
format!(
|
|
246
|
+
"The error did not match the expected pattern:\n+ actual: {}\n- expected: {}",
|
|
247
|
+
err.to_display_string(),
|
|
248
|
+
expected.to_display_string()
|
|
249
|
+
)
|
|
250
|
+
});
|
|
251
|
+
return throw_err(msg, Some(err), Some(expected), "throws");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
Value::Null
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
fn does_not_throw(args: &[Value]) -> Value {
|
|
258
|
+
let fn_v = args.first().cloned().unwrap_or(Value::Null);
|
|
259
|
+
if !crate::expect::is_callable(&fn_v) {
|
|
260
|
+
return throw_err(
|
|
261
|
+
format!(
|
|
262
|
+
"assert.doesNotThrow expects a function, got {}",
|
|
263
|
+
fn_v.to_display_string()
|
|
264
|
+
),
|
|
265
|
+
Some(fn_v),
|
|
266
|
+
None,
|
|
267
|
+
"doesNotThrow",
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
let _ = take_pending_throw();
|
|
271
|
+
let _ = value_call(&fn_v, &[]);
|
|
272
|
+
if has_pending_throw() {
|
|
273
|
+
let err = take_pending_throw().unwrap_or(Value::Null);
|
|
274
|
+
let msg = args
|
|
275
|
+
.get(1)
|
|
276
|
+
.map(|m| m.to_display_string())
|
|
277
|
+
.unwrap_or_else(|| format!("Got unwanted exception: {}", err.to_display_string()));
|
|
278
|
+
return throw_err(msg, Some(err), None, "doesNotThrow");
|
|
279
|
+
}
|
|
280
|
+
Value::Null
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/// Settle `assert.rejects` / `doesNotReject` input. Node accepts a promise **or a function
|
|
284
|
+
/// returning one**; the function form used to be treated as an already-resolved value, so
|
|
285
|
+
/// `assert.rejects(async () => { throw … })` never ran the body.
|
|
286
|
+
#[cfg(feature = "promise")]
|
|
287
|
+
fn settle_awaitable(v: Value) -> Result<Value, Value> {
|
|
288
|
+
let v = if crate::expect::is_callable(&v) {
|
|
289
|
+
let _ = take_pending_throw();
|
|
290
|
+
let produced = value_call(&v, &[]);
|
|
291
|
+
if has_pending_throw() {
|
|
292
|
+
// A synchronous throw from the function counts as a rejection, as it does in Node.
|
|
293
|
+
return Err(take_pending_throw().unwrap_or(Value::Null));
|
|
294
|
+
}
|
|
295
|
+
produced
|
|
296
|
+
} else {
|
|
297
|
+
v
|
|
298
|
+
};
|
|
299
|
+
match v {
|
|
300
|
+
Value::Promise(pr) => pr.block_until_settled(),
|
|
301
|
+
other => Ok(other),
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
fn rejects(args: &[Value]) -> Value {
|
|
306
|
+
let p = args.first().cloned().unwrap_or(Value::Null);
|
|
307
|
+
#[cfg(feature = "promise")]
|
|
308
|
+
{
|
|
309
|
+
let (expected, message) = split_error_and_message(args);
|
|
310
|
+
match settle_awaitable(p) {
|
|
311
|
+
Err(err) => {
|
|
312
|
+
if let Some(expected) = expected {
|
|
313
|
+
if !crate::expect::error_matches(&err, &expected) {
|
|
314
|
+
let msg = message.unwrap_or_else(|| {
|
|
315
|
+
format!(
|
|
316
|
+
"The rejection did not match the expected pattern:\n+ actual: {}\n- expected: {}",
|
|
317
|
+
err.to_display_string(),
|
|
318
|
+
expected.to_display_string()
|
|
319
|
+
)
|
|
320
|
+
});
|
|
321
|
+
return throw_err(msg, Some(err), Some(expected), "rejects");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
Value::Null
|
|
325
|
+
}
|
|
326
|
+
Ok(_) => {
|
|
327
|
+
let msg = message.unwrap_or_else(|| "Missing expected rejection".into());
|
|
328
|
+
throw_err(msg, None, expected, "rejects")
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
#[cfg(not(feature = "promise"))]
|
|
333
|
+
{
|
|
334
|
+
let _ = p;
|
|
335
|
+
throw_err(
|
|
336
|
+
"assert.rejects requires the promise feature",
|
|
337
|
+
None,
|
|
338
|
+
None,
|
|
339
|
+
"rejects",
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
fn does_not_reject(args: &[Value]) -> Value {
|
|
345
|
+
let p = args.first().cloned().unwrap_or(Value::Null);
|
|
346
|
+
#[cfg(feature = "promise")]
|
|
347
|
+
{
|
|
348
|
+
match settle_awaitable(p) {
|
|
349
|
+
Ok(_) => Value::Null,
|
|
350
|
+
Err(err) => {
|
|
351
|
+
let msg = args
|
|
352
|
+
.get(1)
|
|
353
|
+
.map(|m| m.to_display_string())
|
|
354
|
+
.unwrap_or_else(|| {
|
|
355
|
+
format!("Got unwanted rejection: {}", err.to_display_string())
|
|
356
|
+
});
|
|
357
|
+
throw_err(msg, Some(err), None, "doesNotReject")
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
#[cfg(not(feature = "promise"))]
|
|
362
|
+
{
|
|
363
|
+
let _ = p;
|
|
364
|
+
throw_err(
|
|
365
|
+
"assert.doesNotReject requires the promise feature",
|
|
366
|
+
None,
|
|
367
|
+
None,
|
|
368
|
+
"doesNotReject",
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
fn fail(args: &[Value]) -> Value {
|
|
374
|
+
let msg = args
|
|
375
|
+
.first()
|
|
376
|
+
.map(|m| m.to_display_string())
|
|
377
|
+
.unwrap_or_else(|| "Failed".into());
|
|
378
|
+
throw_err(msg, None, None, "fail")
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
fn if_error(args: &[Value]) -> Value {
|
|
382
|
+
let v = args.first().cloned().unwrap_or(Value::Null);
|
|
383
|
+
if matches!(v, Value::Null) {
|
|
384
|
+
return Value::Null;
|
|
385
|
+
}
|
|
386
|
+
// Treat non-null as error
|
|
387
|
+
throw_err(
|
|
388
|
+
format!("ifError got unwanted exception: {}", v.to_display_string()),
|
|
389
|
+
Some(v),
|
|
390
|
+
Some(Value::Null),
|
|
391
|
+
"ifError",
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/// Build the callable `assert` object (`assert(x)` + methods).
|
|
396
|
+
pub fn assert_object() -> Value {
|
|
397
|
+
let call = Value::native(ok);
|
|
398
|
+
let mut m = ObjectMap::default();
|
|
399
|
+
m.insert(Arc::from("__call"), call);
|
|
400
|
+
m.insert(Arc::from("ok"), Value::native(ok));
|
|
401
|
+
m.insert(Arc::from("equal"), Value::native(strict_equal));
|
|
402
|
+
m.insert(Arc::from("notEqual"), Value::native(not_strict_equal));
|
|
403
|
+
m.insert(Arc::from("strictEqual"), Value::native(strict_equal));
|
|
404
|
+
m.insert(Arc::from("notStrictEqual"), Value::native(not_strict_equal));
|
|
405
|
+
m.insert(Arc::from("deepEqual"), Value::native(deep_strict));
|
|
406
|
+
m.insert(Arc::from("notDeepEqual"), Value::native(not_deep_strict));
|
|
407
|
+
m.insert(Arc::from("deepStrictEqual"), Value::native(deep_strict));
|
|
408
|
+
m.insert(
|
|
409
|
+
Arc::from("notDeepStrictEqual"),
|
|
410
|
+
Value::native(not_deep_strict),
|
|
411
|
+
);
|
|
412
|
+
m.insert(
|
|
413
|
+
Arc::from("partialDeepStrictEqual"),
|
|
414
|
+
Value::native(partial_deep),
|
|
415
|
+
);
|
|
416
|
+
m.insert(Arc::from("match"), Value::native(match_fn));
|
|
417
|
+
m.insert(Arc::from("doesNotMatch"), Value::native(does_not_match));
|
|
418
|
+
m.insert(Arc::from("throws"), Value::native(throws));
|
|
419
|
+
m.insert(Arc::from("doesNotThrow"), Value::native(does_not_throw));
|
|
420
|
+
m.insert(Arc::from("rejects"), Value::native(rejects));
|
|
421
|
+
m.insert(Arc::from("doesNotReject"), Value::native(does_not_reject));
|
|
422
|
+
m.insert(Arc::from("fail"), Value::native(fail));
|
|
423
|
+
m.insert(Arc::from("ifError"), Value::native(if_error));
|
|
424
|
+
m.insert(
|
|
425
|
+
Arc::from("AssertionError"),
|
|
426
|
+
Value::native(|args: &[Value]| {
|
|
427
|
+
let msg = args
|
|
428
|
+
.first()
|
|
429
|
+
.map(|v| v.to_display_string())
|
|
430
|
+
.unwrap_or_default();
|
|
431
|
+
assertion_error(msg, None, None, "fail", false)
|
|
432
|
+
}),
|
|
433
|
+
);
|
|
434
|
+
Value::object(m)
|
|
435
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
//! Load `package.json` → `tish.test` config for the runner.
|
|
2
|
+
|
|
3
|
+
use std::fs;
|
|
4
|
+
use std::path::{Path, PathBuf};
|
|
5
|
+
|
|
6
|
+
use serde_json::Value as Json;
|
|
7
|
+
|
|
8
|
+
#[derive(Clone, Debug, Default)]
|
|
9
|
+
pub struct TishTestConfig {
|
|
10
|
+
/// Discovery roots (relative to package root).
|
|
11
|
+
pub root: Vec<PathBuf>,
|
|
12
|
+
/// Extra preload modules run before each test file (paths relative to package root).
|
|
13
|
+
pub preload: Vec<PathBuf>,
|
|
14
|
+
/// Default timeout ms.
|
|
15
|
+
pub timeout_ms: Option<u64>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Read `package.json` beside `cwd` (or ancestors) and parse `tish.test`.
|
|
19
|
+
pub fn load_tish_test_config(start: &Path) -> TishTestConfig {
|
|
20
|
+
let Some(pkg) = find_package_json(start) else {
|
|
21
|
+
return TishTestConfig::default();
|
|
22
|
+
};
|
|
23
|
+
let Ok(text) = fs::read_to_string(&pkg) else {
|
|
24
|
+
return TishTestConfig::default();
|
|
25
|
+
};
|
|
26
|
+
let Ok(json) = serde_json::from_str::<Json>(&text) else {
|
|
27
|
+
return TishTestConfig::default();
|
|
28
|
+
};
|
|
29
|
+
let Some(test) = json
|
|
30
|
+
.get("tish")
|
|
31
|
+
.and_then(|t| t.get("test"))
|
|
32
|
+
.or_else(|| json.get("tish.test"))
|
|
33
|
+
else {
|
|
34
|
+
// Also accept top-level "tish.test" object via nested tish.test already tried;
|
|
35
|
+
// try `tishTest` camelCase as a convenience.
|
|
36
|
+
if let Some(t) = json.get("tishTest") {
|
|
37
|
+
return parse_config(t, pkg.parent().unwrap_or(start));
|
|
38
|
+
}
|
|
39
|
+
return TishTestConfig::default();
|
|
40
|
+
};
|
|
41
|
+
parse_config(test, pkg.parent().unwrap_or(start))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn find_package_json(start: &Path) -> Option<PathBuf> {
|
|
45
|
+
let mut cur = if start.is_file() {
|
|
46
|
+
start.parent()?.to_path_buf()
|
|
47
|
+
} else {
|
|
48
|
+
start.to_path_buf()
|
|
49
|
+
};
|
|
50
|
+
loop {
|
|
51
|
+
let candidate = cur.join("package.json");
|
|
52
|
+
if candidate.is_file() {
|
|
53
|
+
return Some(candidate);
|
|
54
|
+
}
|
|
55
|
+
if !cur.pop() {
|
|
56
|
+
return None;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn parse_config(test: &Json, base: &Path) -> TishTestConfig {
|
|
62
|
+
let mut cfg = TishTestConfig::default();
|
|
63
|
+
if let Some(root) = test.get("root") {
|
|
64
|
+
if let Some(s) = root.as_str() {
|
|
65
|
+
cfg.root.push(base.join(s));
|
|
66
|
+
} else if let Some(arr) = root.as_array() {
|
|
67
|
+
for v in arr {
|
|
68
|
+
if let Some(s) = v.as_str() {
|
|
69
|
+
cfg.root.push(base.join(s));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if let Some(arr) = test.get("preload").and_then(|v| v.as_array()) {
|
|
75
|
+
for v in arr {
|
|
76
|
+
if let Some(s) = v.as_str() {
|
|
77
|
+
cfg.preload.push(base.join(s));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if let Some(n) = test.get("timeout").and_then(|v| v.as_u64()) {
|
|
82
|
+
cfg.timeout_ms = Some(n);
|
|
83
|
+
}
|
|
84
|
+
cfg
|
|
85
|
+
}
|