@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.
Files changed (36) hide show
  1. package/Cargo.toml +1 -0
  2. package/bin/tish-format +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_fmt/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-fmt +0 -0
  33. package/platform/darwin-x64/tish-fmt +0 -0
  34. package/platform/linux-arm64/tish-fmt +0 -0
  35. package/platform/linux-x64/tish-fmt +0 -0
  36. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -0,0 +1,197 @@
1
+ //! Snapshot matchers — `__snapshots__/<testfile>.<name>.snap` relative to the test file.
2
+
3
+ use std::cell::RefCell;
4
+ use std::fs;
5
+ use std::path::{Path, PathBuf};
6
+
7
+ use tishlang_core::{set_pending_throw, Value};
8
+
9
+ use crate::assert::assertion_error;
10
+
11
+ thread_local! {
12
+ static CURRENT_FILE: RefCell<String> = const { RefCell::new(String::new()) };
13
+ /// Full name of the running test. Snapshots are keyed by it so two unnamed
14
+ /// `toMatchSnapshot()` calls in different tests cannot collide on one file.
15
+ static CURRENT_TEST: RefCell<String> = const { RefCell::new(String::new()) };
16
+ static UPDATE: RefCell<bool> = const { RefCell::new(false) };
17
+ static CI: RefCell<bool> = const { RefCell::new(false) };
18
+ static COUNTERS: RefCell<std::collections::HashMap<String, usize>> =
19
+ RefCell::new(std::collections::HashMap::new());
20
+ }
21
+
22
+ /// Set the active test file (used to resolve `__snapshots__/`).
23
+ pub fn set_current_file(path: &str) {
24
+ CURRENT_FILE.with(|f| *f.borrow_mut() = path.to_string());
25
+ COUNTERS.with(|c| c.borrow_mut().clear());
26
+ }
27
+
28
+ /// Set the running test's full name, and reset its per-test snapshot counter.
29
+ pub fn set_current_test(full_name: &str) {
30
+ CURRENT_TEST.with(|t| *t.borrow_mut() = full_name.to_string());
31
+ COUNTERS.with(|c| {
32
+ c.borrow_mut().remove(full_name);
33
+ });
34
+ }
35
+
36
+ pub fn set_update_snapshots(update: bool) {
37
+ UPDATE.with(|u| *u.borrow_mut() = update);
38
+ }
39
+
40
+ /// In CI a *missing* snapshot is a failure, not something to write and pass. Otherwise a
41
+ /// deleted `.snap` file makes the suite go green on the very run that should catch it.
42
+ pub fn set_ci_mode(ci: bool) {
43
+ CI.with(|c| *c.borrow_mut() = ci);
44
+ }
45
+
46
+ fn ci_mode() -> bool {
47
+ CI.with(|c| *c.borrow())
48
+ || std::env::var("CI")
49
+ .ok()
50
+ .is_some_and(|v| v != "0" && v != "false")
51
+ }
52
+
53
+ pub fn update_snapshots() -> bool {
54
+ UPDATE.with(|u| *u.borrow())
55
+ || std::env::var("TISH_UPDATE_SNAPSHOTS").ok().as_deref() == Some("1")
56
+ || std::env::var("UPDATE_SNAPSHOTS").ok().as_deref() == Some("1")
57
+ }
58
+
59
+ fn snapshot_dir_for(test_file: &str) -> PathBuf {
60
+ let p = Path::new(test_file);
61
+ let parent = p.parent().unwrap_or_else(|| Path::new("."));
62
+ parent.join("__snapshots__")
63
+ }
64
+
65
+ fn sanitize(name: &str) -> String {
66
+ let s: String = name
67
+ .chars()
68
+ .map(|c| {
69
+ if c.is_alphanumeric() || c == '_' || c == '-' {
70
+ c
71
+ } else {
72
+ '_'
73
+ }
74
+ })
75
+ .collect();
76
+ // Keep file names bounded; a long describe > it chain would otherwise blow past NAME_MAX.
77
+ if s.len() > 120 {
78
+ let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
79
+ for b in name.as_bytes() {
80
+ hash ^= *b as u64;
81
+ hash = hash.wrapping_mul(0x100_0000_01b3);
82
+ }
83
+ format!("{}_{hash:x}", &s[..120])
84
+ } else {
85
+ s
86
+ }
87
+ }
88
+
89
+ fn snap_path(test_file: &str, name: &str) -> PathBuf {
90
+ let stem = Path::new(test_file)
91
+ .file_stem()
92
+ .and_then(|s| s.to_str())
93
+ .unwrap_or("test");
94
+ snapshot_dir_for(test_file).join(format!("{stem}.{}.snap", sanitize(name)))
95
+ }
96
+
97
+ /// Default snapshot key: `<test full name> <n>`, mirroring Jest. Keyed by test rather than by
98
+ /// file so unnamed snapshots in sibling tests get distinct files.
99
+ fn next_anon_name() -> String {
100
+ let test = CURRENT_TEST.with(|t| t.borrow().clone());
101
+ let key = if test.is_empty() {
102
+ CURRENT_FILE.with(|f| f.borrow().clone())
103
+ } else {
104
+ test
105
+ };
106
+ COUNTERS.with(|c| {
107
+ let mut map = c.borrow_mut();
108
+ let n = map.entry(key.clone()).or_insert(0);
109
+ *n += 1;
110
+ format!("{key} {}", *n)
111
+ })
112
+ }
113
+
114
+ fn serialize(v: &Value) -> String {
115
+ tishlang_core::json_stringify(v)
116
+ }
117
+
118
+ /// Compare `received` against the on-disk snapshot (or write/update it).
119
+ pub fn match_snapshot(received: &Value, hint: Option<&str>) -> Value {
120
+ let file = CURRENT_FILE.with(|f| f.borrow().clone());
121
+ if file.is_empty() {
122
+ set_pending_throw(assertion_error(
123
+ "toMatchSnapshot: no current test file (runner must call set_current_file)",
124
+ Some(received.clone()),
125
+ None,
126
+ "toMatchSnapshot",
127
+ true,
128
+ ));
129
+ return Value::Null;
130
+ }
131
+ let name = hint.map(|s| s.to_string()).unwrap_or_else(next_anon_name);
132
+ let path = snap_path(&file, &name);
133
+ let actual = serialize(received);
134
+ let update = update_snapshots();
135
+ let exists = path.exists();
136
+
137
+ if !exists && ci_mode() && !update {
138
+ set_pending_throw(assertion_error(
139
+ format!(
140
+ "expect(received).toMatchSnapshot()\n\nSnapshot missing (CI mode): {}",
141
+ path.display()
142
+ ),
143
+ Some(received.clone()),
144
+ None,
145
+ "toMatchSnapshot",
146
+ true,
147
+ ));
148
+ return Value::Null;
149
+ }
150
+
151
+ if update || !exists {
152
+ if let Some(parent) = path.parent() {
153
+ let _ = fs::create_dir_all(parent);
154
+ }
155
+ if let Err(e) = fs::write(&path, format!("{}\n", actual)) {
156
+ set_pending_throw(assertion_error(
157
+ format!("toMatchSnapshot: failed to write {}: {}", path.display(), e),
158
+ Some(received.clone()),
159
+ None,
160
+ "toMatchSnapshot",
161
+ true,
162
+ ));
163
+ return Value::Null;
164
+ }
165
+ return Value::Null;
166
+ }
167
+
168
+ let expected = match fs::read_to_string(&path) {
169
+ Ok(s) => s.trim_end_matches('\n').to_string(),
170
+ Err(e) => {
171
+ set_pending_throw(assertion_error(
172
+ format!("toMatchSnapshot: failed to read {}: {}", path.display(), e),
173
+ Some(received.clone()),
174
+ None,
175
+ "toMatchSnapshot",
176
+ true,
177
+ ));
178
+ return Value::Null;
179
+ }
180
+ };
181
+
182
+ if actual != expected {
183
+ set_pending_throw(assertion_error(
184
+ format!(
185
+ "expect(received).toMatchSnapshot()\n\nSnapshot: {}\n\n- Expected\n+ Received\n\n- {}\n+ {}",
186
+ path.display(),
187
+ expected,
188
+ actual
189
+ ),
190
+ Some(received.clone()),
191
+ Some(Value::String(expected.as_str().into())),
192
+ "toMatchSnapshot",
193
+ true,
194
+ ));
195
+ }
196
+ Value::Null
197
+ }
@@ -2178,6 +2178,11 @@ impl Vm {
2178
2178
  }
2179
2179
  }
2180
2180
  Opcode::JumpBack => {
2181
+ // Cooperative timeout poll: a runaway loop is otherwise uninterruptible.
2182
+ // Disarmed (every path but `tish test --timeout`) this is one relaxed load.
2183
+ if tishlang_core::execution_deadline_exceeded() {
2184
+ return Some(Err(tishlang_core::DEADLINE_ERROR.to_string()));
2185
+ }
2181
2186
  let dist = Self::read_u16(code, &mut ip) as usize;
2182
2187
  // #190 OSR: `ip` now points just past the JumpBack (region end); the loop header is
2183
2188
  // `region_end - dist`. Once a loop is hot, try to run its remaining iterations in
@@ -3092,6 +3097,10 @@ impl Vm {
3092
3097
  }
3093
3098
  }
3094
3099
  Opcode::JumpBack => {
3100
+ // Cooperative timeout poll — see the frame VM's JumpBack.
3101
+ if tishlang_core::execution_deadline_exceeded() {
3102
+ return Err(tishlang_core::DEADLINE_ERROR.to_string());
3103
+ }
3095
3104
  let dist = Self::read_u16(code, &mut ip) as usize;
3096
3105
  // #190 OSR: on a hot back-edge, try to finish the loop natively. Slot-based frames
3097
3106
  // only (`slot_locals` is the live frame); non-slot / non-numeric loops fail the
@@ -3686,20 +3695,14 @@ impl Vm {
3686
3695
  return Err("LoadNativeExport: export_name constant out of bounds or not string".to_string());
3687
3696
  }
3688
3697
  };
3689
- // Phase-2 item 11: consult externally registered native
3690
- // modules (populated via `Vm::register_native_module`)
3691
- // before falling through to the built-in lookup. Embedders
3692
- // on the cranelift / llvm backends that want to expose
3693
- // `cargo:…` Rust crates should register the module's
3694
- // exports map before calling `vm.run(chunk)`.
3695
- let from_registry: Option<Value> =
3696
- if spec.starts_with("cargo:") || spec.starts_with("ffi:") {
3697
- let regs = self.native_modules.borrow();
3698
- regs.get(spec)
3699
- .and_then(|m| m.borrow().get(&Arc::from(export_name)).cloned())
3700
- } else {
3701
- None
3702
- };
3698
+ // Consult modules registered via `Vm::register_native_module` before
3699
+ // built-in lookup (used by `tish test` for `tish:test` / `tish:assert`,
3700
+ // and by embedders for `cargo:` / `ffi:`).
3701
+ let from_registry: Option<Value> = {
3702
+ let regs = self.native_modules.borrow();
3703
+ regs.get(spec)
3704
+ .and_then(|m| m.borrow().get(&Arc::from(export_name)).cloned())
3705
+ };
3703
3706
  let v = from_registry
3704
3707
  .or_else(|| get_builtin_export(self.capabilities.as_ref(), spec, export_name))
3705
3708
  .ok_or_else(|| {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tishlang/tish-format",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "Tish code formatter",
5
5
  "license": "PIF",
6
6
  "repository": {
Binary file
Binary file
Binary file
Binary file
Binary file