@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
package/Cargo.toml CHANGED
@@ -33,6 +33,7 @@ members = [
33
33
  "crates/tish_lsp",
34
34
  "crates/tish_resolve",
35
35
  "crates/tishlang_cargo_bindgen",
36
+ "crates/tish_test",
36
37
  ]
37
38
  # Generated native-build projects (`tish build`) land under `target/` when TMPDIR points there;
38
39
  # never enroll anything beneath `target/` as a workspace member.
package/bin/tish-lsp CHANGED
Binary file
@@ -25,7 +25,7 @@ path = "src/main.rs"
25
25
  default = ["full", "fast-alloc"]
26
26
  # Alias: same dependency edges as `default` (kept for `--features full` in CI and docs).
27
27
  # Includes `pg` so `tish run` can resolve `import … from '@tishlang/pg'` (`cargo:tish_pg`) on the bytecode VM.
28
- full = ["http", "fs", "process", "regex", "ws", "timers", "tty", "pg"]
28
+ full = ["http", "fs", "process", "regex", "ws", "timers", "tty", "pg", "test-runner"]
29
29
  # Fast allocator (mimalloc) as the process `#[global_allocator]`. tish's object/array/string workloads
30
30
  # are allocation-bound (a sampling profile of object-heavy code spends ~14%+ in system malloc/free);
31
31
  # mimalloc is markedly faster for the many-small-allocations pattern — the same reason JSC ships bmalloc.
@@ -33,6 +33,8 @@ full = ["http", "fs", "process", "regex", "ws", "timers", "tty", "pg"]
33
33
  fast-alloc = ["dep:mimalloc"]
34
34
  # Opt-out: build without Postgres (`cargo build -p tishlang --no-default-features --features http,...`).
35
35
  pg = ["dep:tishlang_pg"]
36
+ # Native `tish test` runner (`tish:test` / `tish:assert`).
37
+ test-runner = ["dep:tishlang_test"]
36
38
  # Individual capability flags
37
39
  http = ["tishlang_eval/http", "tishlang_runtime/http", "tishlang_compile/http", "tishlang_vm/http"]
38
40
  timers = ["tishlang_eval/timers", "tishlang_compile/timers", "tishlang_vm/timers"]
@@ -58,6 +60,7 @@ tishlang_wasm = { path = "../tish_wasm", version = ">=0.1" }
58
60
  tishlang_runtime = { path = "../tish_runtime", version = ">=0.1" }
59
61
  tishlang_core = { path = "../tish_core", version = ">=0.1" }
60
62
  tishlang_js_to_tish = { path = "../js_to_tish", version = ">=0.1" }
63
+ tishlang_test = { path = "../tish_test", optional = true, features = ["runner", "fs", "regex", "promise", "watch"] }
61
64
  clap = { version = "4.6.0", features = ["derive", "color"] }
62
65
  serde_json = "1"
63
66
  tishlang_pg = { path = "../tish_pg", version = ">=0.1", optional = true }
@@ -631,12 +631,96 @@ pub(crate) struct CompileModuleArgs {
631
631
  pub file: String,
632
632
  }
633
633
 
634
+ #[derive(Parser)]
635
+ pub(crate) struct TestArgs {
636
+ /// File or directory filters (paths / substrings). Omit to discover under the current directory.
637
+ #[arg(num_args = 0.., value_name = "FILTER", help_heading = "Arguments")]
638
+ pub filters: Vec<String>,
639
+ /// Execution backend. Only `vm` runs tests today.
640
+ #[arg(
641
+ long,
642
+ default_value = "vm",
643
+ value_name = "NAME",
644
+ help_heading = "Options"
645
+ )]
646
+ pub backend: String,
647
+ /// Restrict which platform APIs tests may use, as `tish run --feature` does
648
+ /// (omit = all capabilities compiled into this `tish`). Enforced on `--backend vm`.
649
+ #[arg(
650
+ long = "feature",
651
+ value_name = "NAME",
652
+ action = clap::ArgAction::Append,
653
+ help_heading = "Options"
654
+ )]
655
+ pub features: Vec<String>,
656
+ /// Disable AST and bytecode optimizations.
657
+ #[arg(long, help_heading = "Options")]
658
+ pub no_optimize: bool,
659
+ /// Only run tests whose full name matches this regex.
660
+ #[arg(short = 't', long = "test-name-pattern", value_name = "REGEX", help_heading = "Options")]
661
+ pub test_name_pattern: Option<String>,
662
+ /// Per-test timeout in milliseconds (default 5000). Loops are interrupted at the deadline;
663
+ /// a body blocked inside one native call is only caught once it returns.
664
+ #[arg(long, default_value = "5000", value_name = "MS", help_heading = "Options")]
665
+ pub timeout: u64,
666
+ /// Update snapshot files on mismatch (`-u`).
667
+ #[arg(short = 'u', long = "update-snapshots", help_heading = "Options")]
668
+ pub update_snapshots: bool,
669
+ /// Fail on a missing snapshot instead of writing it (also on when `CI` is set).
670
+ #[arg(long, help_heading = "Options")]
671
+ pub ci: bool,
672
+ /// Stop after the first failing test file / suite.
673
+ #[arg(long, help_heading = "Options")]
674
+ pub bail: bool,
675
+ /// Retry failed tests this many times.
676
+ #[arg(long, default_value = "0", value_name = "N", help_heading = "Options")]
677
+ pub retry: u32,
678
+ /// Only run tests marked `.only`.
679
+ #[arg(long, help_heading = "Options")]
680
+ pub only: bool,
681
+ /// Reporter: `console` (default) or `dots`.
682
+ #[arg(long, default_value = "console", value_name = "NAME", help_heading = "Options")]
683
+ pub reporter: String,
684
+ /// Write a JUnit XML report to this path.
685
+ #[arg(long = "reporter-outfile", value_name = "PATH", help_heading = "Options")]
686
+ pub reporter_outfile: Option<String>,
687
+ /// Re-run tests when files under discovery roots change (requires notify).
688
+ #[arg(long, help_heading = "Options")]
689
+ pub watch: bool,
690
+ /// Shuffle test file order.
691
+ #[arg(long, help_heading = "Options")]
692
+ pub randomize: bool,
693
+ /// Seed for `--randomize` (implies randomize).
694
+ #[arg(long, value_name = "N", help_heading = "Options")]
695
+ pub seed: Option<u64>,
696
+ /// Run the suite this many times; any failing pass fails the command (`--rerun-each`).
697
+ #[arg(long = "rerun-each", default_value = "1", value_name = "N", help_heading = "Options")]
698
+ pub rerun_each: u32,
699
+ /// Shard files as `i/n` (1-based index).
700
+ #[arg(long, value_name = "I/N", help_heading = "Options")]
701
+ pub shard: Option<String>,
702
+ /// Only run tests that include this tag (repeatable).
703
+ #[arg(long = "tag", value_name = "TAG", action = clap::ArgAction::Append, help_heading = "Options")]
704
+ pub tags: Vec<String>,
705
+ /// Preload module paths (evaluated before each test file, on the same backend instance).
706
+ #[arg(long = "preload", value_name = "FILE", action = clap::ArgAction::Append, help_heading = "Options")]
707
+ pub preload: Vec<String>,
708
+ /// Collect line coverage via AST instrumentation.
709
+ #[arg(long, help_heading = "Options")]
710
+ pub coverage: bool,
711
+ /// Write `lcov.info` under this directory (implies `--coverage`). Default: `coverage/`.
712
+ #[arg(long = "coverage-dir", value_name = "DIR", help_heading = "Options")]
713
+ pub coverage_dir: Option<String>,
714
+ }
715
+
634
716
  #[derive(Subcommand)]
635
717
  pub(crate) enum Commands {
636
718
  /// Run a Tish file (interpret)
637
719
  Run(RunArgs),
638
720
  /// Interactive REPL
639
721
  Repl(ReplArgs),
722
+ /// Run tests (`*.test.tish`, `*.spec.tish`, …)
723
+ Test(TestArgs),
640
724
  /// Build native binary, wasm, wasi, or JavaScript output
641
725
  Build(BuildArgs),
642
726
  /// Compile a single `.tish` module to one ES module (for Vite dev / HMR); prints to stdout
@@ -74,7 +74,7 @@ fn argv_with_implicit_run(mut argv: Vec<String>) -> Vec<String> {
74
74
  if argv.len() >= 2 {
75
75
  let first = argv[1].as_str();
76
76
  const SUBCOMMANDS: &[&str] =
77
- &["run", "repl", "build", "compile-module", "resolve-id", "dump-ast"];
77
+ &["run", "repl", "test", "build", "compile-module", "resolve-id", "dump-ast"];
78
78
  let looks_like_file = !first.starts_with('-') && !SUBCOMMANDS.contains(&first);
79
79
  if looks_like_file {
80
80
  argv.insert(1, "run".to_string());
@@ -120,6 +120,98 @@ fn main() {
120
120
  tishlang_core::set_process_argv(argv);
121
121
  run_file(&a.file, &a.backend, &a.features, a.no_optimize || no_opt_env)
122
122
  }
123
+ Some(Commands::Test(a)) => {
124
+ #[cfg(feature = "test-runner")]
125
+ {
126
+ use tishlang_test::{run_tests, TestOptions};
127
+ use tishlang_test::report::ReporterKind;
128
+
129
+ let reporter = match a.reporter.as_str() {
130
+ "dots" => ReporterKind::Dots,
131
+ _ => ReporterKind::Console,
132
+ };
133
+ let (roots, filters) = if a.filters.is_empty() {
134
+ (vec![std::path::PathBuf::from(".")], Vec::new())
135
+ } else {
136
+ // Path-like args that exist become discovery roots; others are substring filters.
137
+ let mut roots = Vec::new();
138
+ let mut filters = Vec::new();
139
+ for f in &a.filters {
140
+ let p = std::path::PathBuf::from(f);
141
+ if p.exists() {
142
+ roots.push(p);
143
+ } else {
144
+ filters.push(f.clone());
145
+ }
146
+ }
147
+ if roots.is_empty() {
148
+ roots.push(std::path::PathBuf::from("."));
149
+ }
150
+ (roots, filters)
151
+ };
152
+ let shard = a.shard.as_ref().and_then(|s| {
153
+ let parts: Vec<_> = s.split('/').collect();
154
+ if parts.len() == 2 {
155
+ let i = parts[0].parse().ok()?;
156
+ let n = parts[1].parse().ok()?;
157
+ Some((i, n))
158
+ } else {
159
+ None
160
+ }
161
+ });
162
+ let backend = a.backend.as_str();
163
+ if backend != "vm" {
164
+ // The interpreter cannot host test callbacks: its closures are bound to the
165
+ // `Evaluator`, which is dropped before the collected suite runs. `--backend
166
+ // interp` used to be accepted and then fail on the simplest test file.
167
+ let hint = if backend == "interp" {
168
+ "`--backend interp` cannot run tests: interpreter closures are bound to the evaluator and cannot be called back after collection"
169
+ } else {
170
+ "`--backend native` is not implemented yet"
171
+ };
172
+ eprintln!("error: unsupported --backend `{backend}` for tish test (use `vm`); {hint}");
173
+ std::process::exit(2);
174
+ }
175
+ let opts = TestOptions {
176
+ roots,
177
+ filters,
178
+ name_pattern: a.test_name_pattern.clone(),
179
+ timeout_ms: a.timeout,
180
+ bail: a.bail,
181
+ retry: a.retry,
182
+ update_snapshots: a.update_snapshots,
183
+ reporter,
184
+ junit_path: a.reporter_outfile.as_ref().map(std::path::PathBuf::from),
185
+ only: a.only,
186
+ backend: a.backend.clone(),
187
+ no_optimize: a.no_optimize || no_opt_env,
188
+ features: a.features.clone(),
189
+ shard,
190
+ randomize: a.randomize || a.seed.is_some(),
191
+ seed: a.seed,
192
+ rerun_each: a.rerun_each,
193
+ tags: a.tags.clone(),
194
+ preload: a.preload.iter().map(std::path::PathBuf::from).collect(),
195
+ ci: a.ci,
196
+ coverage: a.coverage || a.coverage_dir.is_some(),
197
+ coverage_dir: a.coverage_dir.as_ref().map(std::path::PathBuf::from),
198
+ };
199
+ if a.watch {
200
+ let code = tishlang_test::run_tests_watch(opts);
201
+ std::process::exit(code);
202
+ }
203
+ let result = run_tests(opts);
204
+ std::process::exit(result.exit_code);
205
+ }
206
+ #[cfg(not(feature = "test-runner"))]
207
+ {
208
+ let _ = a;
209
+ Err(
210
+ "tish test requires the test-runner feature (included in --features full)"
211
+ .into(),
212
+ )
213
+ }
214
+ }
123
215
  Some(Commands::Repl(a)) => run_repl(&a.backend, a.no_optimize || no_opt_env, &a.features),
124
216
  Some(Commands::Build(a)) => {
125
217
  // `--check warn|error` drives the gradual type checker via the same channel as the