@tishlang/tish-lsp 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.
Files changed (36) hide show
  1. package/Cargo.toml +1 -0
  2. package/bin/tish-lsp +0 -0
  3. package/crates/tish/Cargo.toml +4 -1
  4. package/crates/tish/src/cli_help.rs +84 -0
  5. package/crates/tish/src/main.rs +93 -1
  6. package/crates/tish/tests/test_runner.rs +673 -0
  7. package/crates/tish_bytecode/src/compiler.rs +25 -4
  8. package/crates/tish_compile/src/resolve.rs +45 -12
  9. package/crates/tish_core/src/lib.rs +103 -11
  10. package/crates/tish_eval/src/eval.rs +16 -0
  11. package/crates/tish_eval/src/value_convert.rs +10 -0
  12. package/crates/tish_lsp/Cargo.toml +1 -1
  13. package/crates/tish_test/Cargo.toml +47 -0
  14. package/crates/tish_test/src/assert.rs +435 -0
  15. package/crates/tish_test/src/config.rs +85 -0
  16. package/crates/tish_test/src/coverage.rs +237 -0
  17. package/crates/tish_test/src/deep_equal.rs +261 -0
  18. package/crates/tish_test/src/discovery.rs +70 -0
  19. package/crates/tish_test/src/expect.rs +977 -0
  20. package/crates/tish_test/src/instrument.rs +457 -0
  21. package/crates/tish_test/src/isolation.rs +14 -0
  22. package/crates/tish_test/src/lib.rs +22 -0
  23. package/crates/tish_test/src/load.rs +152 -0
  24. package/crates/tish_test/src/mocks.rs +270 -0
  25. package/crates/tish_test/src/module.rs +318 -0
  26. package/crates/tish_test/src/registry.rs +235 -0
  27. package/crates/tish_test/src/report.rs +312 -0
  28. package/crates/tish_test/src/runner.rs +797 -0
  29. package/crates/tish_test/src/snapshots.rs +197 -0
  30. package/crates/tish_vm/src/vm.rs +17 -14
  31. package/package.json +1 -1
  32. package/platform/darwin-arm64/tish-lsp +0 -0
  33. package/platform/darwin-x64/tish-lsp +0 -0
  34. package/platform/linux-arm64/tish-lsp +0 -0
  35. package/platform/linux-x64/tish-lsp +0 -0
  36. package/platform/win32-x64/tish-lsp.exe +0 -0
@@ -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(&regexp, &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(&regexp, &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
+ }