From 609ccd98c604753ddce18e17436ae2154557814b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=BCtke?= Date: Thu, 13 Aug 2026 20:32:59 +0000 Subject: [PATCH 1/3] test: lint with MRI and Spinel, require both implementations Warn (don't fail) when spinel is missing. ruby -c always; spinel -c try.rb when present. AGENTS.md now says try must run on MRI and Spinel. --- AGENTS.md | 15 +++++++++++++-- Rakefile | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 887df17..d6c6609 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,11 +23,21 @@ - Emitted commands use absolute, quoted paths and are shell-neutral (bash/zsh/fish). Only the wrapper function generated by `init` differs per shell. ## Build, Test, and Development Commands +- `rake` / `rake test`: lint (MRI + Spinel if present), unit tests, spec runner. +- `rake lint`: `ruby -c` always; `spinel -c try.rb` when Spinel is available. - `nix run`: Run the packaged CLI (e.g., `nix run . -- --help`). - `nix build`: Build the binary derivation; output at `./result/bin/try`. - `./try.rb init ~/src/tries`: Emit shell function for your shell config. - `./try.rb cd`: Launch interactive selector; prints `cd` script to stdout. - `./try.rb clone [name]`: Clone into date-prefixed directory. +- `make native SPINEL=/path/to/spinel`: AOT a native `dist/try`. + +## Dual implementation (MRI + Spinel) +`try` must keep working on **both**: +- MRI: `ruby try.rb` / the `try-cli` gem +- Spinel AOT: `spinel try.rb -o dist/try` (see `make native`) + +A construct that only one of them can run is a bug. Prefer stdlib and Spinel-friendly Ruby (no `eval`, `method_missing`, `FileUtils`, `IO#raw`/`IO.console`, etc.) or keep an MRI-compatible shim like `TryCompat`. `rake lint` syntax-checks with `ruby -c` always, and with `spinel -c` when Spinel is on `PATH` (or `SPINEL=`). If Spinel is missing, lint **warns** and continues. ## Coding Style & Naming Conventions - Ruby, 2-space indent, standard library only; keep it single-file unless necessary. @@ -47,8 +57,9 @@ **Important**: Specs must reflect the full feature set of `try`. They serve as the canonical reference for behavior, enabling new implementations (in any language) to be validated against the same test suite. When adding or changing features, update both the relevant spec markdown and add corresponding tests. ## Testing Guidelines -- Primary testing is via the spec system: `./spec/tests/runner.sh ./try.rb` -- Native binary (optional, Spinel): `make native-test` / `bash spec/tests/runner.sh dist/try` +- `rake` runs lint + unit + spec. Lint is `ruby -c` on `try.rb` and `lib/**/*.rb`, plus `spinel -c try.rb` when Spinel is installed (`SPINEL=/path/to/spinel` to point at a local build). Missing Spinel is a warning, not a hard fail. +- Primary spec run: `./spec/tests/runner.sh ./try.rb` (MRI). +- Native spec run: `make native-test` / `bash spec/tests/runner.sh dist/try` (Spinel). Behavior must match; the spec runner is language-agnostic. - Manual flows for exploratory testing: - `TRY_PATH=$(mktemp -d) ./try.rb cd` then create/select directories. - Validate delete confirmation and scoring by changing `mtime`/`ctime`. diff --git a/Rakefile b/Rakefile index c63d5e0..e65def6 100644 --- a/Rakefile +++ b/Rakefile @@ -1,16 +1,45 @@ require 'rake/testtask' +require 'tempfile' + +RUBY_SOURCES = FileList['try.rb', 'lib/**/*.rb'] + +def spinel_cmd + ENV.fetch('SPINEL', 'spinel') +end + +def spinel_available? + system('sh', '-c', 'command -v "$1" >/dev/null', '--', spinel_cmd) +end Rake::TestTask.new(:unit) do |t| t.libs << 'lib' << 'test' t.pattern = 'test/**/*_test.rb' end +desc "Check syntax with MRI and Spinel (warns if Spinel is missing)" +task :lint do + RUBY_SOURCES.each do |file| + sh 'ruby', '-c', file + end + + unless spinel_available? + warn "warning: spinel not found (#{spinel_cmd}); skipping Spinel syntax check" + warn "warning: try must parse and run on both MRI Ruby and Spinel" + next + end + + # try.rb require_relatives lib/; compiling the entrypoint type-checks the whole program. + Tempfile.create(['try-spinel-syntax', '.c']) do |tmp| + sh spinel_cmd, '-c', 'try.rb', '-o', tmp.path + end +end + desc "Run shell spec compliance tests" task :spec do sh "bash spec/tests/runner.sh ./try.rb" end -desc "Run all tests (unit + spec)" -task test: [:unit, :spec] +desc "Run all tests (lint + unit + spec)" +task test: [:lint, :unit, :spec] task default: :test From 3b3e4259a39563c2a8978b697cec00d80125c3f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=BCtke?= Date: Thu, 13 Aug 2026 20:34:34 +0000 Subject: [PATCH 2/3] test: spinel -c every app Ruby file, not just try.rb lib/fuzzy.rb and lib/tui.rb compile standalone. Minitest files do not (and are not Spinel programs). --- AGENTS.md | 6 +++--- Rakefile | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d6c6609..a4d4f76 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,7 @@ ## Build, Test, and Development Commands - `rake` / `rake test`: lint (MRI + Spinel if present), unit tests, spec runner. -- `rake lint`: `ruby -c` always; `spinel -c try.rb` when Spinel is available. +- `rake lint`: `ruby -c` always; `spinel -c` on `try.rb` and `lib/**/*.rb` when Spinel is available. - `nix run`: Run the packaged CLI (e.g., `nix run . -- --help`). - `nix build`: Build the binary derivation; output at `./result/bin/try`. - `./try.rb init ~/src/tries`: Emit shell function for your shell config. @@ -37,7 +37,7 @@ - MRI: `ruby try.rb` / the `try-cli` gem - Spinel AOT: `spinel try.rb -o dist/try` (see `make native`) -A construct that only one of them can run is a bug. Prefer stdlib and Spinel-friendly Ruby (no `eval`, `method_missing`, `FileUtils`, `IO#raw`/`IO.console`, etc.) or keep an MRI-compatible shim like `TryCompat`. `rake lint` syntax-checks with `ruby -c` always, and with `spinel -c` when Spinel is on `PATH` (or `SPINEL=`). If Spinel is missing, lint **warns** and continues. +A construct that only one of them can run is a bug. Prefer stdlib and Spinel-friendly Ruby (no `eval`, `method_missing`, `FileUtils`, `IO#raw`/`IO.console`, etc.) or keep an MRI-compatible shim like `TryCompat`. `rake lint` syntax-checks with `ruby -c` always, and with `spinel -c` on those same files when Spinel is on `PATH` (or `SPINEL=`). If Spinel is missing, lint **warns** and continues. ## Coding Style & Naming Conventions - Ruby, 2-space indent, standard library only; keep it single-file unless necessary. @@ -57,7 +57,7 @@ A construct that only one of them can run is a bug. Prefer stdlib and Spinel-fri **Important**: Specs must reflect the full feature set of `try`. They serve as the canonical reference for behavior, enabling new implementations (in any language) to be validated against the same test suite. When adding or changing features, update both the relevant spec markdown and add corresponding tests. ## Testing Guidelines -- `rake` runs lint + unit + spec. Lint is `ruby -c` on `try.rb` and `lib/**/*.rb`, plus `spinel -c try.rb` when Spinel is installed (`SPINEL=/path/to/spinel` to point at a local build). Missing Spinel is a warning, not a hard fail. +- `rake` runs lint + unit + spec. Lint is `ruby -c` on `try.rb` and `lib/**/*.rb`, plus `spinel -c` on each of `try.rb` and `lib/**/*.rb` when Spinel is installed (`SPINEL=/path/to/spinel` to point at a local build). Missing Spinel is a warning, not a hard fail. - Primary spec run: `./spec/tests/runner.sh ./try.rb` (MRI). - Native spec run: `make native-test` / `bash spec/tests/runner.sh dist/try` (Spinel). Behavior must match; the spec runner is language-agnostic. - Manual flows for exploratory testing: diff --git a/Rakefile b/Rakefile index e65def6..5e7aa3b 100644 --- a/Rakefile +++ b/Rakefile @@ -28,9 +28,10 @@ task :lint do next end - # try.rb require_relatives lib/; compiling the entrypoint type-checks the whole program. - Tempfile.create(['try-spinel-syntax', '.c']) do |tmp| - sh spinel_cmd, '-c', 'try.rb', '-o', tmp.path + RUBY_SOURCES.each do |file| + Tempfile.create(['try-spinel-syntax', '.c']) do |tmp| + sh spinel_cmd, '-c', file, '-o', tmp.path + end end end From 6c84b8848f5634b420393536274f31b61b02712c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=BCtke?= Date: Thu, 13 Aug 2026 20:38:50 +0000 Subject: [PATCH 3/3] test: emit dist/try.c, compile it, and compare MRI vs native specs When Spinel is present, rake builds dist/try.c, cc's it to dist/try, runs the full spec suite on the binary, and diffs outputs against MRI. Init's ruby-vs-$0 prefix is normalized; everything else must match exactly. --- AGENTS.md | 5 +++-- Makefile | 19 ++++++++++++++++--- Rakefile | 20 ++++++++++++++++---- spec/tests/runner_and_compare.sh | 7 +++++++ 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a4d4f76..3ac917a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,8 +23,9 @@ - Emitted commands use absolute, quoted paths and are shell-neutral (bash/zsh/fish). Only the wrapper function generated by `init` differs per shell. ## Build, Test, and Development Commands -- `rake` / `rake test`: lint (MRI + Spinel if present), unit tests, spec runner. +- `rake` / `rake test`: lint, unit, MRI spec; if Spinel is present, also emit `dist/try.c`, compile `dist/try`, native spec, and `runner_and_compare.sh`. - `rake lint`: `ruby -c` always; `spinel -c` on `try.rb` and `lib/**/*.rb` when Spinel is available. +- `rake spec_spinel`: `make native` (writes `dist/try.c` then compiles), spec the binary, compare outputs with MRI. - `nix run`: Run the packaged CLI (e.g., `nix run . -- --help`). - `nix build`: Build the binary derivation; output at `./result/bin/try`. - `./try.rb init ~/src/tries`: Emit shell function for your shell config. @@ -59,7 +60,7 @@ A construct that only one of them can run is a bug. Prefer stdlib and Spinel-fri ## Testing Guidelines - `rake` runs lint + unit + spec. Lint is `ruby -c` on `try.rb` and `lib/**/*.rb`, plus `spinel -c` on each of `try.rb` and `lib/**/*.rb` when Spinel is installed (`SPINEL=/path/to/spinel` to point at a local build). Missing Spinel is a warning, not a hard fail. - Primary spec run: `./spec/tests/runner.sh ./try.rb` (MRI). -- Native spec run: `make native-test` / `bash spec/tests/runner.sh dist/try` (Spinel). Behavior must match; the spec runner is language-agnostic. +- Native spec run: `make native-test` / `bash spec/tests/runner.sh dist/try` (Spinel). `make native-compare` / `rake spec_spinel` diffs MRI vs native output (init exec prefix is normalized). - Manual flows for exploratory testing: - `TRY_PATH=$(mktemp -d) ./try.rb cd` then create/select directories. - Validate delete confirmation and scoring by changing `mtime`/`ctime`. diff --git a/Makefile b/Makefile index 72da122..a207a13 100644 --- a/Makefile +++ b/Makefile @@ -120,15 +120,28 @@ i: install ## Shortcut for install # Native binary via Spinel (optional) SPINEL ?= spinel SPINEL_FLAGS ?= -O s +CC ?= cc NATIVE = dist/try +NATIVE_C = dist/try.c +SPINEL_BIN := $(shell command -v $(SPINEL) 2>/dev/null) +SPINEL_LIB ?= $(dir $(SPINEL_BIN))../lib -.PHONY: native native-test +.PHONY: native native-test native-compare native: $(NATIVE) -$(NATIVE): try.rb lib/tui.rb lib/fuzzy.rb +$(NATIVE_C): try.rb lib/tui.rb lib/fuzzy.rb mkdir -p dist - $(SPINEL) $(SPINEL_FLAGS) try.rb -o $(NATIVE) + $(SPINEL) $(SPINEL_FLAGS) -c try.rb -o $(NATIVE_C) + +$(NATIVE): $(NATIVE_C) + $(CC) -Os -Wno-all -ffunction-sections -fdata-sections \ + -I$(SPINEL_LIB) -I$(SPINEL_LIB)/regexp \ + $(NATIVE_C) $(SPINEL_LIB)/libspinel_rt.a \ + -lm -lcrypt -Wl,--gc-sections -o $(NATIVE) strip $(NATIVE) native-test: $(NATIVE) bash spec/tests/runner.sh $(NATIVE) + +native-compare: $(NATIVE) + bash spec/tests/runner_and_compare.sh ./try.rb $(NATIVE) diff --git a/Rakefile b/Rakefile index 5e7aa3b..6568ffe 100644 --- a/Rakefile +++ b/Rakefile @@ -35,12 +35,24 @@ task :lint do end end -desc "Run shell spec compliance tests" +desc "Run shell spec compliance tests (MRI)" task :spec do - sh "bash spec/tests/runner.sh ./try.rb" + sh 'bash', 'spec/tests/runner.sh', './try.rb' end -desc "Run all tests (lint + unit + spec)" -task test: [:lint, :unit, :spec] +desc "Emit dist/try.c, compile dist/try, spec it, and compare with MRI" +task :spec_spinel do + unless spinel_available? + warn "warning: spinel not found (#{spinel_cmd}); skipping native spec + compare" + next + end + + sh 'make', 'native', "SPINEL=#{spinel_cmd}" + sh 'bash', 'spec/tests/runner.sh', 'dist/try' + sh 'bash', 'spec/tests/runner_and_compare.sh', './try.rb', 'dist/try' +end + +desc "Run all tests (lint + unit + spec; native spec+compare if Spinel is present)" +task test: [:lint, :unit, :spec, :spec_spinel] task default: :test diff --git a/spec/tests/runner_and_compare.sh b/spec/tests/runner_and_compare.sh index 39858df..c077de2 100755 --- a/spec/tests/runner_and_compare.sh +++ b/spec/tests/runner_and_compare.sh @@ -102,6 +102,13 @@ compare_test() { norm1=$(echo "$out1" | sed 's/\x1b\[[0-9;]*m//g') norm2=$(echo "$out2" | sed 's/\x1b\[[0-9;]*m//g') + # Init snippets invoke MRI via `ruby try.rb` and the native binary via $0. + # Same wrapper otherwise — collapse the exec prefix so the compare is exact. + if [ "$name" = "init command" ]; then + norm1=$(printf '%s\n' "$norm1" | sed -E "s#/usr/bin/env ruby '[^']+'#'TRY'#g; s#'[^']+' exec#'TRY' exec#g") + norm2=$(printf '%s\n' "$norm2" | sed -E "s#/usr/bin/env ruby '[^']+'#'TRY'#g; s#'[^']+' exec#'TRY' exec#g") + fi + if [ "$norm1" = "$norm2" ] && [ "$exit1" = "$exit2" ]; then echo -en "${GREEN}.${NC}" TESTS_SAME=$((TESTS_SAME + 1))