diff --git a/AGENTS.md b/AGENTS.md index 887df17..3ac917a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,11 +23,22 @@ - 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, 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. - `./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` 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. @@ -47,8 +58,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` 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). `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 c63d5e0..6568ffe 100644 --- a/Rakefile +++ b/Rakefile @@ -1,16 +1,58 @@ 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 "Run shell spec compliance tests" +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 + + RUBY_SOURCES.each do |file| + Tempfile.create(['try-spinel-syntax', '.c']) do |tmp| + sh spinel_cmd, '-c', file, '-o', tmp.path + end + end +end + +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 "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 (unit + spec)" -task test: [:unit, :spec] +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))