diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a873770..cf3d885 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,5 +56,8 @@ jobs: - name: Run unit tests run: uv run pytest - - name: Run integration tests - run: ./tests/run.sh + - name: Check golden coverage + run: bash scripts/check-golden-coverage.sh + + - name: Run golden tests + run: npx tryscript@latest run tests/tryscript/*.tryscript.md diff --git a/.tbd/.gitignore b/.tbd/.gitignore new file mode 100644 index 0000000..8dd2573 --- /dev/null +++ b/.tbd/.gitignore @@ -0,0 +1,21 @@ +# Installed documentation (regenerated on setup) +docs/ + +# Hidden worktree for tbd-sync branch +data-sync-worktree/ + +# Data sync directory (only exists in worktree) +data-sync/ + +# Local state +state.yml + +# Migration backups (local only, not synced) +backups/ + +# Temporary files +*.tmp +*.temp + +# workspaces/ stores state (including outbox) committed to the working branch +!workspaces/ diff --git a/.tbd/config.yml b/.tbd/config.yml new file mode 100644 index 0000000..bb8334e --- /dev/null +++ b/.tbd/config.yml @@ -0,0 +1,93 @@ +tbd_format: f03 +tbd_version: 0.1.22 +display: + id_prefix: rpy +sync: + branch: tbd-sync + remote: origin +settings: + auto_sync: false + doc_auto_sync_hours: 24 + use_gh_cli: true +# Documentation cache configuration. +# files: Maps destination paths (relative to .tbd/docs/) to source locations. +# Sources can be: +# - internal: prefix for bundled docs (e.g., "internal:shortcuts/standard/code-review-and-commit.md") +# - Full URL for external docs (e.g., "https://raw.githubusercontent.com/org/repo/main/file.md") +# lookup_path: Search paths for doc lookup (like shell $PATH). Earlier paths take precedence. +# +# To sync docs: tbd sync --docs +# To check status: tbd sync --status +# +# Auto-sync: Docs are automatically synced when stale (default: every 24 hours). +# Configure with settings.doc_auto_sync_hours (0 = disabled). +docs_cache: + lookup_path: + - .tbd/docs/shortcuts/system + - .tbd/docs/shortcuts/standard + files: + shortcuts/system/shortcut-explanation.md: internal:shortcuts/system/shortcut-explanation.md + shortcuts/system/skill-baseline.md: internal:shortcuts/system/skill-baseline.md + shortcuts/system/skill-brief.md: internal:shortcuts/system/skill-brief.md + shortcuts/system/skill-minimal.md: internal:shortcuts/system/skill-minimal.md + shortcuts/standard/agent-handoff.md: internal:shortcuts/standard/agent-handoff.md + shortcuts/standard/checkout-third-party-repo.md: internal:shortcuts/standard/checkout-third-party-repo.md + shortcuts/standard/code-cleanup-all.md: internal:shortcuts/standard/code-cleanup-all.md + shortcuts/standard/code-cleanup-docstrings.md: internal:shortcuts/standard/code-cleanup-docstrings.md + shortcuts/standard/code-cleanup-tests.md: internal:shortcuts/standard/code-cleanup-tests.md + shortcuts/standard/code-review-and-commit.md: internal:shortcuts/standard/code-review-and-commit.md + shortcuts/standard/coding-spike.md: internal:shortcuts/standard/coding-spike.md + shortcuts/standard/create-or-update-pr-simple.md: internal:shortcuts/standard/create-or-update-pr-simple.md + shortcuts/standard/create-or-update-pr-with-validation-plan.md: internal:shortcuts/standard/create-or-update-pr-with-validation-plan.md + shortcuts/standard/implement-beads.md: internal:shortcuts/standard/implement-beads.md + shortcuts/standard/merge-upstream.md: internal:shortcuts/standard/merge-upstream.md + shortcuts/standard/new-architecture-doc.md: internal:shortcuts/standard/new-architecture-doc.md + shortcuts/standard/new-guideline.md: internal:shortcuts/standard/new-guideline.md + shortcuts/standard/new-plan-spec.md: internal:shortcuts/standard/new-plan-spec.md + shortcuts/standard/new-qa-playbook.md: internal:shortcuts/standard/new-qa-playbook.md + shortcuts/standard/new-research-brief.md: internal:shortcuts/standard/new-research-brief.md + shortcuts/standard/new-shortcut.md: internal:shortcuts/standard/new-shortcut.md + shortcuts/standard/new-validation-plan.md: internal:shortcuts/standard/new-validation-plan.md + shortcuts/standard/plan-implementation-with-beads.md: internal:shortcuts/standard/plan-implementation-with-beads.md + shortcuts/standard/precommit-process.md: internal:shortcuts/standard/precommit-process.md + shortcuts/standard/review-code-python.md: internal:shortcuts/standard/review-code-python.md + shortcuts/standard/review-code-typescript.md: internal:shortcuts/standard/review-code-typescript.md + shortcuts/standard/review-code.md: internal:shortcuts/standard/review-code.md + shortcuts/standard/review-github-pr.md: internal:shortcuts/standard/review-github-pr.md + shortcuts/standard/revise-all-architecture-docs.md: internal:shortcuts/standard/revise-all-architecture-docs.md + shortcuts/standard/revise-architecture-doc.md: internal:shortcuts/standard/revise-architecture-doc.md + shortcuts/standard/setup-github-cli.md: internal:shortcuts/standard/setup-github-cli.md + shortcuts/standard/sync-failure-recovery.md: internal:shortcuts/standard/sync-failure-recovery.md + shortcuts/standard/update-specs-status.md: internal:shortcuts/standard/update-specs-status.md + shortcuts/standard/welcome-user.md: internal:shortcuts/standard/welcome-user.md + guidelines/backward-compatibility-rules.md: internal:guidelines/backward-compatibility-rules.md + guidelines/bun-monorepo-patterns.md: internal:guidelines/bun-monorepo-patterns.md + guidelines/cli-agent-skill-patterns.md: internal:guidelines/cli-agent-skill-patterns.md + guidelines/commit-conventions.md: internal:guidelines/commit-conventions.md + guidelines/convex-limits-best-practices.md: internal:guidelines/convex-limits-best-practices.md + guidelines/convex-rules.md: internal:guidelines/convex-rules.md + guidelines/electron-app-development-patterns.md: internal:guidelines/electron-app-development-patterns.md + guidelines/error-handling-rules.md: internal:guidelines/error-handling-rules.md + guidelines/general-coding-rules.md: internal:guidelines/general-coding-rules.md + guidelines/general-comment-rules.md: internal:guidelines/general-comment-rules.md + guidelines/general-eng-assistant-rules.md: internal:guidelines/general-eng-assistant-rules.md + guidelines/general-style-rules.md: internal:guidelines/general-style-rules.md + guidelines/general-tdd-guidelines.md: internal:guidelines/general-tdd-guidelines.md + guidelines/general-testing-rules.md: internal:guidelines/general-testing-rules.md + guidelines/golden-testing-guidelines.md: internal:guidelines/golden-testing-guidelines.md + guidelines/pnpm-monorepo-patterns.md: internal:guidelines/pnpm-monorepo-patterns.md + guidelines/python-cli-patterns.md: internal:guidelines/python-cli-patterns.md + guidelines/python-modern-guidelines.md: internal:guidelines/python-modern-guidelines.md + guidelines/python-rules.md: internal:guidelines/python-rules.md + guidelines/release-notes-guidelines.md: internal:guidelines/release-notes-guidelines.md + guidelines/tbd-sync-troubleshooting.md: internal:guidelines/tbd-sync-troubleshooting.md + guidelines/typescript-cli-tool-rules.md: internal:guidelines/typescript-cli-tool-rules.md + guidelines/typescript-code-coverage.md: internal:guidelines/typescript-code-coverage.md + guidelines/typescript-rules.md: internal:guidelines/typescript-rules.md + guidelines/typescript-sorting-patterns.md: internal:guidelines/typescript-sorting-patterns.md + guidelines/typescript-yaml-handling-rules.md: internal:guidelines/typescript-yaml-handling-rules.md + guidelines/writing-style-guidelines.md: internal:guidelines/writing-style-guidelines.md + templates/architecture-doc.md: internal:templates/architecture-doc.md + templates/plan-spec.md: internal:templates/plan-spec.md + templates/qa-playbook.md: internal:templates/qa-playbook.md + templates/research-brief.md: internal:templates/research-brief.md diff --git a/Makefile b/Makefile index bcaa687..7c493ee 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ .DEFAULT_GOAL := default -.PHONY: default install lint format gendocs test update-golden upgrade build clean +.PHONY: default install lint format gendocs test test-golden test-golden-coverage update-golden upgrade build clean default: install lint gendocs test @@ -21,10 +21,17 @@ gendocs: test: uv run pytest - ./tests/run.sh + $(MAKE) test-golden-coverage + $(MAKE) test-golden + +test-golden: + npx tryscript@latest run tests/tryscript/*.tryscript.md + +test-golden-coverage: + bash scripts/check-golden-coverage.sh update-golden: - ./tests/run.sh || cp tests/golden-tests-actual.log tests/golden-tests-expected.log + npx tryscript@latest run --update tests/tryscript/*.tryscript.md upgrade: uv sync --upgrade --all-extras --dev diff --git a/docs/development.md b/docs/development.md index 752152e..7fe678b 100644 --- a/docs/development.md +++ b/docs/development.md @@ -46,8 +46,11 @@ make upgrade uv run pytest # all tests uv run pytest -s tests/pytests.py # one test, showing outputs -# Run integration tests: -./tests/run.sh +# Run golden tests: +npx tryscript@latest run tests/tryscript/*.tryscript.md + +# Check golden coverage gates: +bash scripts/check-golden-coverage.sh # Update golden test baseline (when expected test output changes intentionally): make update-golden @@ -88,14 +91,15 @@ and backup management. uv run pytest tests/pytests.py ``` -### Golden Tests (`tests/golden-tests.sh`) +### Golden Tests (`tests/tryscript/*.tryscript.md`) -Shell-based integration tests that exercise the full CLI. These tests capture CLI output -and compare it against a committed baseline (`tests/golden-tests-expected.log`). +Tryscript-based integration tests exercise the full CLI using fixture-first session +files grouped by behavior (help/errors, replacements, renames/full mode, backup +lifecycle, JSON, filters, and regex/case flows). **Running golden tests:** ```shell -./tests/run.sh # Runs tests and compares output to expected baseline +npx tryscript@latest run tests/tryscript/*.tryscript.md ``` **Updating the baseline when output changes intentionally:** @@ -103,22 +107,22 @@ and compare it against a committed baseline (`tests/golden-tests-expected.log`). make update-golden ``` +**Running golden quality gates:** +```shell +bash scripts/check-golden-coverage.sh +``` + This is useful when: - Adding new CLI features that produce different output -- Fixing bugs that change output format -- Adding new test cases to `golden-tests.sh` - -The `run.sh` script: -1. Copies `tests/work-dir` to `tests/tmp-dir` for isolation -2. Runs `golden-tests.sh` in the temp directory -3. Normalizes output (removes timestamps, line numbers, etc.) -4. Compares against `golden-tests-expected.log` +- Fixing bugs that change output format or lifecycle behavior +- Adding new scenario modules or fixture flows **Adding new golden tests:** -1. Edit `tests/golden-tests.sh` to add new test commands -2. Run `make update-golden` to capture the new expected output -3. Review the diff in `tests/golden-tests-expected.log` -4. Commit both the script changes and the updated expected log +1. Add or extend files under `tests/tryscript/` and `tests/tryscript/fixtures/` +2. Run `make update-golden` to capture updated expected output +3. Run `bash scripts/check-golden-coverage.sh` to enforce coverage/anti-pattern gates +4. Review the markdown diff in the changed `.tryscript.md` files +5. Commit scenario/fixture updates together ## IDE setup diff --git a/docs/project/research/current/research-2026-02-27-golden-harness-strategy.md b/docs/project/research/current/research-2026-02-27-golden-harness-strategy.md new file mode 100644 index 0000000..96ee5bf --- /dev/null +++ b/docs/project/research/current/research-2026-02-27-golden-harness-strategy.md @@ -0,0 +1,66 @@ +# Research: Golden Harness Strategy for Rust Port Prep (2026-02-27) + +> Superseded (2026-02-27): This document captured a temporary keep-shell decision. +> The repository has since migrated to tryscript as the authoritative golden harness. +> See: +> `docs/project/specs/active/plan-2026-02-27-repren-python-tryscript-full-migration.md`. + +## Purpose + +Decide whether to keep the existing shell golden harness (`tests/golden-tests.sh` + +`tests/run.sh`) as the current parity baseline, or migrate immediately to a different +harness format before Rust parity work accelerates. + +## Decision + +Keep the shell golden harness as the authoritative baseline for now. + +## Why keep it now + +1. It already covers broad end-to-end CLI behavior: +- replacements, renames, full mode, include/exclude, `--walk-only` +- backup lifecycle (`--undo`, `--clean-backups`, custom backup suffix) +- JSON output mode and error paths +- skill install and collision handling scenarios + +2. It is already normalized and deterministic in practice: +- `tests/run.sh` strips volatile paths/line numbers/timestamps +- baseline diffing is stable across reruns in this environment + +3. Rust porting work benefits from stable fixtures immediately: +- changing the harness now would add migration noise unrelated to behavior parity +- current harness output can be consumed directly to build Rust parity checks + +## Known limitations + +1. Shell scripts are harder to refactor and parameterize than structured test fixtures. +2. Assertions are baseline-diff oriented, not strongly typed. +3. Running in parallel with other harness runs can create temp-dir collisions. + +## Operational guidance for current harness + +1. Run `./tests/run.sh` serially (do not run in parallel with pytest jobs that invoke it). +2. Treat `tests/golden-tests-expected.log` as a versioned contract artifact. +3. Update baseline only when behavior change is intentional and documented. + +## Migration trigger conditions + +Revisit migration only when one or more are true: + +1. We need per-scenario selective execution to speed parity triage. +2. Golden diffs become too noisy for maintainable review. +3. Rust cross-language parity harness needs tighter, fixture-level assertions. + +## Deferred migration plan sketch + +If migration is needed later: + +1. Keep shell harness as source of truth during transition. +2. Port one scenario group at a time into a structured harness. +3. Compare new harness output against existing baseline for each migrated group. +4. Only retire shell sections once parity and determinism are proven. + +## Related documents + +- `docs/project/specs/active/plan-2026-02-27-rust-port-prep-and-test-hardening.md` +- `../repren-rs/docs/project/specs/active/plan-2026-02-27-repren-port-master-plan.md` diff --git a/docs/project/research/current/research-2026-02-27-rust-port-behavior-notes.md b/docs/project/research/current/research-2026-02-27-rust-port-behavior-notes.md new file mode 100644 index 0000000..86a1a8b --- /dev/null +++ b/docs/project/research/current/research-2026-02-27-rust-port-behavior-notes.md @@ -0,0 +1,155 @@ +# Research: repren Behavior Notes for Rust Port (2026-02-27) + +## Purpose + +Capture parity-critical runtime semantics from Python `repren` so Rust implementation +work can proceed from explicit behavior contracts. + +## Scope + +Focus areas: + +1. simultaneous replacement engine +2. filesystem rewrite/rename lifecycle +3. traversal, backup handling, and undo/clean semantics +4. output and JSON contracts + +## Replacement engine semantics + +### Pattern parsing + +1. Input format is tab-separated `regexreplacement` lines. +2. Blank lines and comment lines (`#` after optional leading whitespace) are ignored. +3. `--literal` escapes regex input with `re.escape`. +4. `--word-breaks` wraps each regex variant with `\b...\b`. +5. `--preserve-case` expands both regex and replacement into case variants: +- lowerCamel +- UpperCamel +- lower_underscore +- UPPER_UNDERSCORE +6. Duplicate `(regex, replacement)` pairs are deduplicated before compile. + +### Matching and overlap policy + +1. All matches for all patterns are collected before applying replacements. +2. Overlaps are resolved by sorted insertion: +- earlier-starting match wins +- later overlapping candidates are dropped +3. Replacements are applied simultaneously over original bytes (no cascading). + +### Replacement expansion + +1. Python `re` replacement expansion is used (`match.expand` semantics). +2. Capture references like `\1` work in replacement strings. +3. Behavior is byte-oriented for matching/replacement internals. + +## Filesystem traversal and filtering semantics + +### walk behavior + +1. `walk_files` accepts explicit files and directories. +2. Include/exclude use `re.match` semantics on file/directory names (start-anchored). +3. During directory walk: +- directories matching exclude are pruned from descent +- files are filtered by include + exclude +4. Files ending with backup suffix (default `.orig`) or temp suffix + (`.repren.tmp`) are always skipped for processing. +5. Return values: +- sorted file path list (deterministic order) +- count of skipped backup/temp files + +### backup discovery + +1. `find_backup_files` traverses similarly, but returns only files ending with backup suffix. +2. It also applies include/exclude filters to backup filenames. +3. Returned paths are sorted deterministically. + +## Rewrite and rename lifecycle semantics + +### transform_file behavior + +1. For content transforms: +- write transformed content to `dest_path + temp_suffix` +- preserve source file permissions when creating temp output +- if `dry_run`, temp file is removed and no source mutation happens +2. Commit condition for content path: +- if `dest_path != source_path` OR `matches_found > 0`, then: + - move source to backup (`source + backup_suffix`, clobber enabled) + - move temp into destination (collision-safe move) +- else remove temp and leave source untouched +3. For rename-only (`do_contents == False`): +- if destination differs, source is moved to destination +- no backup file is created in rename-only mode + +### rename collision behavior + +1. Destination files are never clobbered when clobber is disabled. +2. Collision resolution appends numeric suffixes: +- `.1`, `.2`, ... +3. Existing numeric suffixes are normalized while probing, so repeated collisions + continue incrementally. + +## Undo and clean semantics + +### undo_backups + +For each backup file: + +1. Strip backup suffix to derive original path. +2. Apply patterns to original path to predict renamed path. +3. Determine expected current target: +- original path (no rename case), or +- predicted renamed path +4. Skip with warning when: +- expected target path is missing +- backup mtime is newer than target mtime +5. Otherwise restore: +- move backup to original path +- remove predicted renamed file when it differs from original + +Returns `(restored_count, skipped_count)`. + +### clean_backups + +1. Enumerates backups via `find_backup_files`. +2. Removes each backup unless dry-run. +3. Returns removal count. + +## Output contracts + +### Text mode (human) + +1. Reports discovered file count and root paths processed. +2. Logs per-file `modify` and `rename` events. +3. Reports aggregate counts: +- files read/chars read +- matches found/overlaps skipped +- files changed, rewritten, renamed + +### JSON mode + +Current operation payloads: + +1. `walk`: +- `operation`, `paths`, `files_found`, `skipped_backups` +2. `replace`: +- `operation`, `dry_run`, `patterns_count`, `files_found`, `chars_read`, + `matches_found`, `matches_applied`, `files_changed`, `files_rewritten`, `files_renamed` +3. `undo`: +- `operation`, `dry_run`, `restored`, `skipped` +4. `clean_backups`: +- `operation`, `dry_run`, `removed` + +## Parity constraints for Rust implementation + +1. Keep overlap-drop and simultaneous replacement semantics exact. +2. Keep walk filtering and backup/temp skipping exact. +3. Keep collision suffix policy exact. +4. Keep backup/undo/clean state transitions and counters exact. +5. Keep JSON field names and operation tags stable. + +## Related documents + +- `docs/project/specs/active/plan-2026-02-27-rust-port-prep-and-test-hardening.md` +- `docs/project/research/current/research-2026-02-27-golden-harness-strategy.md` +- `../repren-rs/docs/project/specs/active/plan-2026-02-27-repren-port-master-plan.md` diff --git a/docs/project/research/current/research-2026-02-27-rust-port-prep-coverage-gap-map.md b/docs/project/research/current/research-2026-02-27-rust-port-prep-coverage-gap-map.md new file mode 100644 index 0000000..94209d1 --- /dev/null +++ b/docs/project/research/current/research-2026-02-27-rust-port-prep-coverage-gap-map.md @@ -0,0 +1,105 @@ +# Research: repren Rust Port Prep Coverage Gap Map (2026-02-27) + +## Purpose + +Create a parity-risk-prioritized coverage gap map to guide Python prep work before +expanding Rust implementation. + +Related spec: +- `docs/project/specs/active/plan-2026-02-27-rust-port-prep-and-test-hardening.md` + +## Baseline measurements + +Command run: + +```bash +uv run pytest tests/pytests.py --cov=repren --cov-branch --cov-report=term-missing +``` + +Results: + +| Module | Stmts | Miss | Branch | BrPart | Cover | +| --- | ---: | ---: | ---: | ---: | ---: | +| `repren/__init__.py` | 2 | 0 | 0 | 0 | 100% | +| `repren/claude_skill.py` | 61 | 25 | 6 | 1 | 61% | +| `repren/markdown_renderer.py` | 90 | 63 | 24 | 1 | 25% | +| `repren/repren.py` | 541 | 282 | 194 | 11 | 46% | +| **TOTAL** | **694** | **370** | **224** | **13** | **44%** | + +Context checks: +- `82` pytest tests pass +- `./tests/run.sh` golden harness passes + +## Risk-prioritized gap map + +### P0 parity risk (must close early) + +1. `repren/repren.py` mutation paths with low coverage: +- rewrite and rename path interactions +- backup creation and cleanup lifecycle +- undo restore behavior under edge conditions + +2. error and conflict paths: +- invalid CLI argument combinations +- invalid regex/input handling paths +- operational filesystem errors + +3. replacement core semantics: +- overlap precedence and nested match handling +- capturing-group replacement corner cases +- multiline behavior interactions (`--at-once`, `--dotall`) + +### P1 parity risk (close during prep) + +1. JSON output contracts: +- field consistency across modes and errors +- stable schema semantics for downstream automation + +2. traversal/path edge cases: +- include/exclude edge patterns +- hidden/backup file interactions +- special-character path names + +3. collision handling: +- deterministic rename-to-existing semantics +- numeric suffix behavior under repeated collisions + +### P2 parity risk (can close in parallel) + +1. `repren/claude_skill.py` and `repren/markdown_renderer.py` coverage: +- valuable for product quality, lower risk for core rename/replace parity + +2. stress/performance scenarios: +- large files and large directory trees +- useful for robustness, not immediate parity blocker + +## Concrete test additions mapped to issues + +- `rpy-e82o` replacement/overlap tests + - additional nested-overlap and tie-breaking cases + - capture-group replacement edge cases + - multiline replacement edge cases + +- `rpy-wqj2` filesystem mutation tests + - collision fan-out (`.1`, `.2`, repeated collisions) + - move + backup + undo round-trip cases + - backup suffix and invalid suffix scenarios + +- `rpy-5jls` CLI conflict and JSON tests + - incompatible flag combinations + - required argument errors + - json schema assertions for key modes and failures + +## Readiness gate recommendation + +Proceed to broad Rust implementation only after: + +1. P0 items above have dedicated tests +2. full Python suite still passes +3. documented behavior notes are published for Rust implementation reference + +## Notes + +This analysis deliberately prioritizes behavioral confidence over raw coverage percentage. +The target is not only a higher number, but coverage of parity-critical semantics. + diff --git a/docs/project/research/current/research-2026-02-27-rust-port-prep-handoff.md b/docs/project/research/current/research-2026-02-27-rust-port-prep-handoff.md new file mode 100644 index 0000000..46d6b14 --- /dev/null +++ b/docs/project/research/current/research-2026-02-27-rust-port-prep-handoff.md @@ -0,0 +1,71 @@ +# Research: Rust Port Prep Handoff (2026-02-27) + +## Purpose + +Summarize Python-side prep completion status, validation results, and handoff artifacts +for the Rust implementation team in `repren-rs`. + +## Baseline vs current + +### Baseline (2026-02-27 start) + +1. `82` pytest tests passing +2. Coverage: +- `TOTAL`: `44%` +- `repren/repren.py`: `46%` + +### Current (2026-02-27 handoff) + +1. `98` pytest tests passing +2. Coverage: +- `TOTAL`: `52%` +- `repren/repren.py`: `55%` +3. Golden harness (`./tests/run.sh`) passes + +## Commands run + +```bash +make test +uv run pytest tests/pytests.py +uv run pytest tests/pytests.py --cov=repren --cov-branch --cov-report=term-missing +./tests/run.sh +``` + +## What was added + +### Tests + +Expanded `tests/pytests.py` with parity-critical checks for: + +1. pattern parsing edge behavior +2. overlap and replacement semantics +3. backup/temp skip behavior in traversal +4. backup discovery + undo + clean lifecycle +5. CLI conflict/error paths and JSON output fields +6. path edge cases (spaces/special characters/unicode rename path) + +### Behavior docs + +1. `docs/project/research/current/research-2026-02-27-rust-port-behavior-notes.md` +2. `docs/project/research/current/research-2026-02-27-golden-harness-strategy.md` + +## Handoff checklist for Rust implementation + +1. Use Python tests plus behavior notes as the parity contract. +2. Match JSON schema keys and operation names exactly. +3. Match rename collision suffix semantics (`.1`, `.2`, ...). +4. Match traversal include/exclude and backup/temp skip behavior. +5. Match undo skip rules (missing target, backup newer than target). +6. Preserve shell golden baseline as regression contract while Rust parity harness grows. + +## Open prep limitations + +1. Permission-error branch testing remains limited due cross-platform portability concerns. +2. `claude_skill.py` and `markdown_renderer.py` remain lower-coverage areas and are not + core blockers for `repren.py` parity work. + +## Related documents + +1. `docs/project/specs/active/plan-2026-02-27-rust-port-prep-and-test-hardening.md` +2. `docs/project/research/current/research-2026-02-27-rust-port-prep-coverage-gap-map.md` +3. `../repren-rs/docs/project/specs/active/plan-2026-02-27-repren-port-master-plan.md` diff --git a/docs/project/specs/active/plan-2026-02-27-repren-python-tryscript-full-migration.md b/docs/project/specs/active/plan-2026-02-27-repren-python-tryscript-full-migration.md new file mode 100644 index 0000000..6222cd4 --- /dev/null +++ b/docs/project/specs/active/plan-2026-02-27-repren-python-tryscript-full-migration.md @@ -0,0 +1,195 @@ +# Plan Spec: repren Python Full Tryscript Migration and Golden Discipline Hardening + +## Purpose + +Replace the legacy bash golden harness with a comprehensive, fixture-first tryscript +suite so the Python `repren` behavior contract is explicit, deterministic, and directly +portable to Rust parity tests. + +## Background + +State before this migration tranche (2026-02-27): + +1. Golden coverage relied on: + - `tests/golden-tests.sh` + - `tests/run.sh` + - `tests/golden-tests-expected.log` +2. CI and Make invoked shell harness directly. +3. Unit tests called the shell runner in an integration wrapper. +4. Scenario coverage was broad but monolithic and hard to evolve safely. + +Reference sources used while designing migration: + +1. `tbd guidelines golden-testing-guidelines` +2. `tbd shortcut checkout-third-party-repo` to clone: + - `attic/blobsy` ([jlevy/blobsy](https://github.com/jlevy/blobsy)) +3. Prior case-study implementation in: + - `../flowmark-rs/repos/flowmark` + +Patterns adopted from blobsy and flowmark: + +1. root `tryscript.config.js` with deterministic env/path/patterns +2. modular scenario files by behavior domain +3. quality gate script enforcing anti-pattern and command/flag coverage checks +4. fixture-first test setup with per-suite isolated sandboxes + +## Summary of Task + +Deliver a full migration from bash golden tests to tryscript as the authoritative Python +CLI golden harness. + +Target outcomes: + +1. comprehensive tryscript modules under `tests/tryscript/` +2. fixture tree under `tests/tryscript/fixtures/` +3. root `tryscript.config.js` +4. `scripts/check-golden-coverage.sh` +5. Make/CI/pytest integration switched to tryscript +6. docs updated to remove shell-baseline workflow references + +## Backward Compatibility + +### Compatibility mode + +- Strict CLI behavior compatibility for end users. +- Migration changes test harness structure, not product behavior. + +### Protected surfaces + +1. replacement/rename semantics +2. backup/undo/clean lifecycle +3. include/exclude walk behavior +4. JSON output structure +5. CLI help/error model and exit codes + +### Allowed changes + +1. golden harness layout and tooling +2. deterministic normalization and patternization +3. CI/test command wiring + +## Stage 1: Planning Stage + +### Scope + +In scope: + +1. replace shell harness with tryscript modules +2. enforce golden quality gates +3. document updated workflow + +Out of scope: + +1. intentional CLI behavior changes +2. unrelated runtime feature additions + +### Acceptance criteria + +1. `npx tryscript@latest run tests/tryscript/*.tryscript.md` passes +2. `bash scripts/check-golden-coverage.sh` passes +3. `make test` runs pytest + golden gates + tryscript +4. CI runs golden coverage gate + tryscript +5. unit/integration bridge test runs tryscript instead of shell runner + +## Stage 2: Architecture Stage + +### Target layout + +1. `tests/tryscript/help-errors.tryscript.md` +2. `tests/tryscript/replacements.tryscript.md` +3. `tests/tryscript/renames-and-full.tryscript.md` +4. `tests/tryscript/patterns-and-case.tryscript.md` +5. `tests/tryscript/walk-and-filters.tryscript.md` +6. `tests/tryscript/backups-undo-clean.tryscript.md` +7. `tests/tryscript/json-output.tryscript.md` +8. `tests/tryscript/regex-wordbreaks.tryscript.md` +9. `tests/tryscript/advanced-options.tryscript.md` +10. `tests/tryscript/cache-lifecycle-internals.tryscript.md` +11. `tests/tryscript/stdin-collision-overlap-validation.tryscript.md` +12. `tests/tryscript/fixtures/` (ported from legacy work-dir fixtures) +13. `tryscript.config.js` +14. `scripts/check-golden-coverage.sh` + +### Determinism strategy + +1. global env in tryscript config (`NO_COLOR=1`, `LC_ALL=C`) +2. fixed binary path (`$TRYSCRIPT_GIT_ROOT/.venv/bin`) +3. explicit version placeholder pattern (`[VERSION]`) +4. anti-pattern gate against bare `...` elisions + +## Stage 3: Refine Architecture + +### Why modular tryscript over shell monolith + +1. per-domain diff review is much clearer +2. fixture setup is explicit per session +3. easier to add focused parity cases for Rust without destabilizing full baseline +4. no custom log-normalization pipeline required for typical repren outputs + +### Port-to-Rust readiness impact + +1. each tryscript module maps directly to a Rust parity test tranche +2. deterministic outputs simplify side-by-side Python vs Rust behavior checks +3. quality gate script keeps command/flag coverage from regressing during Rust bring-up + +## Stage 4: Implementation Plan + +### Phase G1: Spec and inventory + +- [x] inventory legacy shell harness and entrypoints +- [x] review flowmark/blobsy golden discipline patterns +- [x] publish migration spec + +### Phase G2: Build modular tryscript suite + +- [x] create `tests/tryscript/fixtures/` from existing fixture corpus +- [x] author domain-split tryscript files for all major CLI behaviors +- [x] lock golden outputs with `tryscript --update` + +### Phase G3: Add config and quality gates + +- [x] add root `tryscript.config.js` +- [x] add `scripts/check-golden-coverage.sh` +- [x] enforce required-module and flag-coverage checks + +### Phase G4: Switch wiring and validate + +- [x] update Makefile targets (`test-golden`, `test-golden-coverage`, `update-golden`) +- [x] update CI workflow to run gate + tryscript +- [x] switch pytest integration wrapper to run tryscript +- [x] update developer docs and publishing checklist references +- [ ] remove or archive legacy shell harness artifacts (follow-up if retained temporarily) + +## Validation Plan + +Primary checks: + +```bash +uv run pytest +bash scripts/check-golden-coverage.sh +npx tryscript@latest run tests/tryscript/*.tryscript.md +``` + +Secondary checks: + +1. verify all major CLI option families appear in tryscript modules +2. verify quality gate fails when key modules/flags are removed +3. verify version output is patternized (`[VERSION]`) to avoid revision churn + +## Deliverables + +1. complete modular tryscript suite and fixtures +2. deterministic tryscript config +3. golden quality gate script +4. updated Make/CI/test integration wiring +5. updated documentation for golden workflow + +## Tracking + +`tbd` issue tree for this migration: + +- Epic: `rpy-8woi` +- G1 spec/inventory: `rpy-dacv` +- G2 modular suite: `rpy-i8pi` +- G3 config/gates: `rpy-xxmw` +- G4 integration/validation: `rpy-i0xu` diff --git a/docs/project/specs/active/plan-2026-02-27-rust-port-prep-and-test-hardening.md b/docs/project/specs/active/plan-2026-02-27-rust-port-prep-and-test-hardening.md new file mode 100644 index 0000000..3a340c5 --- /dev/null +++ b/docs/project/specs/active/plan-2026-02-27-rust-port-prep-and-test-hardening.md @@ -0,0 +1,184 @@ +# Plan Spec: repren Rust Port Prep and Test Hardening + +> Note (2026-02-27): Golden harness strategy decisions in this plan were superseded by +> the tryscript migration spec: +> `docs/project/specs/active/plan-2026-02-27-repren-python-tryscript-full-migration.md`. + +## Purpose + +Prepare the Python `repren` codebase to act as a high-fidelity specification for the +Rust port in `repren-rs`. + +This plan focuses on test sufficiency, explicit behavior documentation, and parity-ready +artifacts that reduce ambiguity during Rust implementation. + +## Background + +A Rust port is underway in `../repren-rs` with `repos/repren` as source-of-truth. +Current Python tests pass, but coverage is not yet high enough to confidently treat the +suite as a complete behavioral specification. + +Baseline measured on 2026-02-27: + +- `uv run pytest tests/pytests.py --cov=repren --cov-branch --cov-report=term-missing` + - Total coverage: `44%` + - `repren/repren.py`: `46%` +- Golden harness: `./tests/run.sh` passes + +Given repren's heavy filesystem mutation behavior, we need stronger coverage in error +paths, edge cases, and operation lifecycle semantics before Rust implementation expands. + +## Summary of Task + +Deliver a Python-side prep package for the Rust port: + +1. Improve test coverage and behavior completeness in key parity-critical paths +2. Document exact semantics for replacement, rename, backup/undo/clean, and output modes +3. Decide and document golden test harness strategy (current shell harness vs migration) +4. Leave Python repo in a state where Rust parity tests can be written directly from + Python artifacts + +## Backward Compatibility + +### Compatibility mode + +- **Mode:** strict behavioral compatibility for existing CLI users +- New tests and docs should clarify behavior, not change it, unless a bug fix is + explicitly approved + +### Protected surfaces + +- CLI flags and interactions +- Output and exit-code behavior relied on by scripts +- Backup, undo, and cleanup semantics +- JSON output schema + +### Allowed changes + +- Additional tests and fixtures +- Better documentation +- Test harness internal modernization if output behavior remains equivalent + +## Stage 1: Planning Stage + +### Scope boundaries + +In scope: +- test additions +- parity behavior docs +- golden harness strategy and optional migration plan + +Out of scope: +- major new runtime features +- broad refactors not tied to parity confidence + +### Risk-focused priority list + +1. Simultaneous replacement and overlap handling behavior +2. Filesystem mutation lifecycle (rewrite, rename, backups, undo, clean) +3. CLI conflict/error path behavior +4. JSON output contract stability + +## Stage 2: Architecture Stage + +### Existing assets to leverage + +- `tests/pytests.py` already has strong seed coverage for many core behaviors +- `tests/golden-tests.sh` exercises full CLI workflows and has expected output baseline +- `tests/run.sh` normalization pipeline already controls some nondeterminism + +### Required additions + +- richer edge-case fixtures (unicode paths/content, malformed input cases) +- explicit behavior spec docs under `docs/project/` +- optional tryscript migration plan or rationale for retaining shell harness + +## Stage 3: Refine Architecture + +### Reuse opportunities + +- Keep existing golden harness as baseline while adding targeted scenarios +- Reuse current fixture layout (`tests/work-dir`) for new edge-case scenarios +- Reuse unit test structure in `tests/pytests.py` classes for new focused cases + +### Simplifications + +- Do not split tests into many files right now; keep incremental additions in current + structure unless maintainability clearly suffers +- Avoid harness migration and behavior changes in the same PR + +## Stage 4: Implementation Plan + +### Phase P1: Baseline and coverage analysis + +- [x] record current uncovered branches/functions in `repren/repren.py` +- [x] prioritize uncovered logic by parity risk +- [x] add tracking notes in a new research/plan doc section + +### Phase P2: Targeted unit/integration expansion + +- [x] add tests for replacement overlap edge cases not currently covered +- [x] add tests for rename collision and directory move edge cases +- [x] add tests for backup suffix edge cases and invalid inputs +- [x] add tests for CLI invalid combinations and argument conflicts +- [x] add tests for JSON output fields in success and error workflows + +### Phase P3: Filesystem edge-case coverage + +- [ ] add read-only/permission error scenario tests where portable +- [x] add path edge-case tests (spaces, special chars) +- [x] add regression tests for backup skip behavior during traversal + +### Phase P4: Golden harness strategy + +- [x] document keep-vs-migrate decision for shell golden tests +- [x] if migrating, produce phased migration plan preserving output normalization parity +- [x] if not migrating now, document exact reasons and future trigger conditions + +### Phase P5: Behavior documentation for Rust port + +- [x] write behavior notes for replacement engine semantics +- [x] write behavior notes for filesystem mutation lifecycle +- [x] write behavior notes for output modes and error model +- [x] cross-link these notes to `repren-rs` master port plan + +### Phase P6: Validation and handoff + +- [x] run full test suite (`make test` + golden harness) +- [x] run coverage report and document delta vs baseline +- [x] update active-spec status and summarize handoff checklist for Rust team + +## Validation Plan + +Automated: +- `uv run pytest tests/pytests.py --cov=repren --cov-branch --cov-report=term-missing` +- `./tests/run.sh` +- `make test` + +Manual spot checks: +- verify representative JSON output samples against docs +- verify backup/undo/clean examples from docs against actual behavior + +## Deliverables + +- expanded Python test suite for parity-critical behaviors +- explicit behavior docs for Rust-port reference +- golden strategy decision doc (or migration plan) +- updated coverage metrics and gap summary + +## Tracking + +All implementation tasks from this spec are tracked as `tbd` issues in this repo +(prefix `rpy-*`), with dependency ordering from P1 to P6. + +Initialized issue tree: + +- Epic: `rpy-vrvt` Python prep for repren Rust port +- Tasks: + - `rpy-32cl` P1 coverage gap audit + - `rpy-e82o` P2 replacement/overlap tests + - `rpy-wqj2` P3 filesystem mutation tests + - `rpy-5jls` P4 CLI conflict + JSON tests + - `rpy-ieaq` P5 golden harness strategy + - `rpy-0nid` P6 behavior notes for Rust team + - `rpy-v1fc` P7 final validation + handoff diff --git a/docs/project/specs/active/valid-2026-01-16-test-coverage-improvements.md b/docs/project/specs/active/valid-2026-01-16-test-coverage-improvements.md index 41d4e11..181be42 100644 --- a/docs/project/specs/active/valid-2026-01-16-test-coverage-improvements.md +++ b/docs/project/specs/active/valid-2026-01-16-test-coverage-improvements.md @@ -1,5 +1,9 @@ # Validation: Test Coverage Improvements +> Historical note: This validation references the retired shell-based golden harness. +> Current golden workflow is tryscript-based; see +> `docs/project/specs/active/plan-2026-02-27-repren-python-tryscript-full-migration.md`. + ## Purpose This validation spec documents the test coverage analysis and improvements made to the repren codebase, validating that all new tests function correctly and coverage goals have been met. diff --git a/docs/publishing.md b/docs/publishing.md index 592e0fd..93f0b3d 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -79,7 +79,8 @@ Follow this checklist for each new release. ```shell make lint make test - ./tests/run.sh # integration tests + make test-golden-coverage + make test-golden ``` 3. **Confirm CI is passing:** diff --git a/repren/repren.py b/repren/repren.py index 37443c6..e479f53 100755 --- a/repren/repren.py +++ b/repren/repren.py @@ -758,7 +758,9 @@ def make_parent_dirs(path: str) -> str: """ Ensure parent directories of a file are created as needed. """ - os.makedirs(os.path.dirname(path), exist_ok=True) + parent_dir = os.path.dirname(path) + if parent_dir: + os.makedirs(parent_dir, exist_ok=True) return path diff --git a/scripts/check-golden-coverage.sh b/scripts/check-golden-coverage.sh new file mode 100755 index 0000000..69ea822 --- /dev/null +++ b/scripts/check-golden-coverage.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Quality gates for repren tryscript golden tests. +# Run from repository root. + +EXIT_CODE=0 +TESTS_DIR="tests/tryscript" + +if [ ! -d "$TESTS_DIR" ]; then + echo "ERROR: Missing $TESTS_DIR" + exit 1 +fi + +echo "Checking anti-patterns..." +ELISIONS=$(grep -rn '^\.\.\.$' "$TESTS_DIR" 2>/dev/null || true) +if [ -n "$ELISIONS" ]; then + echo "ERROR: Found bare ... elisions:" + echo "$ELISIONS" + EXIT_CODE=1 +else + echo "OK: no bare ... elisions" +fi + +echo "" +echo "Checking required tryscript modules..." +REQUIRED_FILES=( + help-errors.tryscript.md + replacements.tryscript.md + renames-and-full.tryscript.md + patterns-and-case.tryscript.md + walk-and-filters.tryscript.md + backups-undo-clean.tryscript.md + json-output.tryscript.md + regex-wordbreaks.tryscript.md + advanced-options.tryscript.md + cache-lifecycle-internals.tryscript.md + stdin-collision-overlap-validation.tryscript.md +) + +for file in "${REQUIRED_FILES[@]}"; do + if [ ! -f "$TESTS_DIR/$file" ]; then + echo " MISSING: $file" + EXIT_CODE=1 + else + echo " OK: $file" + fi +done + +echo "" +echo "Checking key CLI flag coverage..." +check_pattern() { + local label="$1" + local pattern="$2" + local matches + local count + matches=$(grep -R -- "$pattern" "$TESTS_DIR" 2>/dev/null || true) + if [ -z "$matches" ]; then + count=0 + else + count=$(printf "%s\n" "$matches" | wc -l | tr -d ' ') + fi + if [ "$count" -eq 0 ]; then + echo " MISSING: $label ($pattern)" + EXIT_CODE=1 + else + echo " OK: $label ($count matches)" + fi +} + +check_pattern "help" "--help" +check_pattern "docs" "--docs" +check_pattern "skill" "--skill" +check_pattern "version" "--version" +check_pattern "single replacement" "--from" +check_pattern "single replacement target" "--to" +check_pattern "pattern file" "--patterns" +check_pattern "full mode" "--full" +check_pattern "rename mode" "--renames" +check_pattern "literal mode" "--literal" +check_pattern "case-insensitive" "--insensitive" +check_pattern "dotall mode" "--dotall" +check_pattern "preserve case" "--preserve-case" +check_pattern "word breaks" "--word-breaks" +check_pattern "at-once mode" "--at-once" +check_pattern "walk only" "--walk-only" +check_pattern "dry run" "--dry-run" +check_pattern "quiet mode" "--quiet" +check_pattern "include filter" "--include" +check_pattern "exclude filter" "--exclude" +check_pattern "parse only" "--parse-only" +check_pattern "undo" "--undo" +check_pattern "clean backups" "--clean-backups" +check_pattern "backup suffix" "--backup-suffix" +check_pattern "json mode" "--format json" +check_pattern "install skill" "--install-skill" +check_pattern "agent base" "--agent-base" + +echo "" +FILE_COUNT=$(find "$TESTS_DIR" -name "*.tryscript.md" | wc -l | tr -d ' ') +FIXTURE_COUNT=$(find "$TESTS_DIR/fixtures" -type f 2>/dev/null | wc -l | tr -d ' ') +echo "Tryscript files: $FILE_COUNT" +echo "Fixture files: $FIXTURE_COUNT" + +exit "$EXIT_CODE" diff --git a/tests/golden-tests-expected.log b/tests/golden-tests-expected.log deleted file mode 100644 index 4284b4b..0000000 --- a/tests/golden-tests-expected.log +++ /dev/null @@ -1,901 +0,0 @@ - -# --- Start of tests --- - -run || expect_error -usage: repren [-h] [--version] [--docs] [--from FROM_PAT] [--to TO_PAT] [-p PAT_FILE] - [--full] [--renames] [--literal] [-i] [--dotall] [--preserve-case] [-b] - [--include INCLUDE_PAT] [--exclude EXCLUDE_PAT] [--at-once] [-t] - [--walk-only] [-n] [-q] [--format {text,json}] - [--backup-suffix BACKUP_SUFFIX] [--undo] [--clean-backups] - [--install-skill] [--agent-base DIR] [--skill] - [root_paths ...] -repren: error: must specify --patterns or both --from and --to - -Run `repren --help` for usage. -Run `repren --docs` for full docs. -(got expected error: status 2) - -# Text replacements, no renames. - -cp -a original test1 - -run -n --from Humpty --to Dumpty test1/humpty-dumpty.txt -Dry run: No files will be changed -Using 1 patterns: - 'Humpty' -> 'Dumpty' -Found 1 files in: test1/humpty-dumpty.txt -- modify: test1/humpty-dumpty.txt: 3 matches -Read 1 files (513 chars), found 3 matches (0 skipped due to overlaps) -Dry run: Would have changed 1 files (1 rewritten and 0 renamed) - -diff -r original test1 - -run --from humpty --to dumpty test1/humpty-dumpty.txt -Using 1 patterns: - 'humpty' -> 'dumpty' -Found 1 files in: test1/humpty-dumpty.txt -Read 1 files (513 chars), found 0 matches (0 skipped due to overlaps) -Changed 0 files (0 rewritten and 0 renamed) - -diff original test1 -Common subdirectories: original/stuff and test1/stuff - -run --from Humpty --to Dumpty test1/humpty-dumpty.txt -Using 1 patterns: - 'Humpty' -> 'Dumpty' -Found 1 files in: test1/humpty-dumpty.txt -- modify: test1/humpty-dumpty.txt: 3 matches -Read 1 files (513 chars), found 3 matches (0 skipped due to overlaps) -Changed 1 files (1 rewritten and 0 renamed) - -diff -r original test1 || expect_error -diff -r original/humpty-dumpty.txt test1/humpty-dumpty.txt -1c1 -< Humpty Dumpty smiled contemptuously. 'Of course you don't — till I tell you. I meant "there's a nice knock-down argument for you!"' ---- -> Dumpty Dumpty smiled contemptuously. 'Of course you don't — till I tell you. I meant "there's a nice knock-down argument for you!"' -3c3 -< 'When I use a word,' Humpty Dumpty said, in rather a scornful tone, 'it means just what I choose it to mean — neither more nor less.' ---- -> 'When I use a word,' Dumpty Dumpty said, in rather a scornful tone, 'it means just what I choose it to mean — neither more nor less.' -5c5 -< 'The question is,' said Humpty Dumpty, 'which is to be master — that's all.' ---- -> 'The question is,' said Dumpty Dumpty, 'which is to be master — that's all.' -Only in test1: humpty-dumpty.txt.orig -(got expected error: status 1) - -run --from humpty --to dumpty test1 -Using 1 patterns: - 'humpty' -> 'dumpty' -Skipped 1 file(s) ending in '.orig' (backup files are never processed) -Found 12 files in: test1 -Read 12 files (3810 chars), found 0 matches (0 skipped due to overlaps) -Changed 0 files (0 rewritten and 0 renamed) - - -# File renames only. - -cp -a original test2 - -run -n --renames --from humpty --to dumpty test2 -Dry run: No files will be changed -Using 1 patterns: - 'humpty' -> 'dumpty' -Found 12 files in: test2 -- rename: test2/humpty-dumpty.txt -> test2/dumpty-dumpty.txt -Read 1 files (0 chars), found 0 matches (0 skipped due to overlaps) -Dry run: Would have changed 1 files (0 rewritten and 1 renamed) - -ls_portable test2 --rw-r--r-- humpty-dumpty.txt -drwxr-xr-x stuff/ - -run --renames --from humpty --to dumpty test2 -Using 1 patterns: - 'humpty' -> 'dumpty' -Found 12 files in: test2 -- rename: test2/humpty-dumpty.txt -> test2/dumpty-dumpty.txt -Read 1 files (0 chars), found 0 matches (0 skipped due to overlaps) -Changed 1 files (0 rewritten and 1 renamed) - -ls_portable test2 --rw-r--r-- dumpty-dumpty.txt -drwxr-xr-x stuff/ - -diff -r original test2 || expect_error -Only in test2: dumpty-dumpty.txt -Only in original: humpty-dumpty.txt -(got expected error: status 1) - - -# Both file renames and replacements. - -cp -a original test3 - -run -n --full -i --from humpty --to dumpty test3 -Dry run: No files will be changed -Using 1 patterns: - 'humpty' IGNORECASE -> 'dumpty' -Found 12 files in: test3 -- modify: test3/humpty-dumpty.txt: 3 matches -- rename: test3/humpty-dumpty.txt -> test3/dumpty-dumpty.txt -Read 12 files (3810 chars), found 3 matches (0 skipped due to overlaps) -Dry run: Would have changed 1 files (1 rewritten and 1 renamed) - -ls_portable test3 --rw-r--r-- humpty-dumpty.txt -drwxr-xr-x stuff/ - -run --full -i --from humpty --to dumpty test3 -Using 1 patterns: - 'humpty' IGNORECASE -> 'dumpty' -Found 12 files in: test3 -- modify: test3/humpty-dumpty.txt: 3 matches -- rename: test3/humpty-dumpty.txt -> test3/dumpty-dumpty.txt -Read 12 files (3810 chars), found 3 matches (0 skipped due to overlaps) -Changed 1 files (1 rewritten and 1 renamed) - -ls_portable test3 --rw-r--r-- dumpty-dumpty.txt --rw-r--r-- humpty-dumpty.txt.orig -drwxr-xr-x stuff/ - -diff -r original test3 || expect_error -Only in test3: dumpty-dumpty.txt -Only in original: humpty-dumpty.txt -Only in test3: humpty-dumpty.txt.orig -(got expected error: status 1) - - -# More patterns: Contents. - -cp -a original test4 - -run -p patterns-misc test4 -Using 5 patterns: - 'humpty' -> 'dumpty' - 'dumpty' -> 'humpty' - 'beech' -> 'BEECH' - 'Asia' -> 'Asia!' - 'Europe' -> 'Europe!' -Found 12 files in: test4 -- modify: test4/stuff/trees/beech.txt: 8 matches -- modify: test4/stuff/trees/maple.txt: 3 matches -- modify: test4/stuff/trees/oak.txt: 3 matches -Read 12 files (3810 chars), found 14 matches (0 skipped due to overlaps) -Changed 3 files (3 rewritten and 0 renamed) - -diff -r original test4 || expect_error -diff -r original/stuff/trees/beech.txt test4/stuff/trees/beech.txt -1c1 -< Beech (Fagus) is a genus of deciduous trees in the family Fagaceae, native to temperate Europe, Asia and North America. Recent classification systems of the genus recognize ten to thirteen species in two distinct subgenera, Engleriana and Fagus.[1][2] The Engleriana subgenus is found only in East Asia, and is notably distinct from the Fagus subgenus in that these beeches are low-branching trees, often made up of several major trunks with yellowish bark. Further differentiating characteristics include the whitish bloom on the underside of the leaves, the visible tertiary leaf veins, and a long, smooth cupule-peduncle. Fagus japonica, Fagus engleriana, and the species F. okamotoi, proposed by the bontanist Chung-Fu Shen in 1992, comprise this subgenus.[2] The better known Fagus subgenus beeches are high-branching with tall, stout trunks and smooth silver-grey bark. This group includes Fagus sylvatica, Fagus grandifolia, Fagus crenata, Fagus lucida, Fagus longipetiolata, and Fagus hayatae.[2] The classification of the European beech, Fagus sylvatica is complex, with a variety of different names proposed for different species and subspecies within this region (for example Fagus taurica, Fagus orientalis, and Fagus moesica[3]). Research suggests that beeches in Eurasia differentiated fairly late in evolutionary history, during the Miocene. The populations in this area represent a range of often overlapping morphotypes, though genetic analysis does not clearly support separate species.[4] -\ No newline at end of file ---- -> Beech (Fagus) is a genus of deciduous trees in the family Fagaceae, native to temperate Europe!, Asia! and North America. Recent classification systems of the genus recognize ten to thirteen species in two distinct subgenera, Engleriana and Fagus.[1][2] The Engleriana subgenus is found only in East Asia!, and is notably distinct from the Fagus subgenus in that these BEECHes are low-branching trees, often made up of several major trunks with yellowish bark. Further differentiating characteristics include the whitish bloom on the underside of the leaves, the visible tertiary leaf veins, and a long, smooth cupule-peduncle. Fagus japonica, Fagus engleriana, and the species F. okamotoi, proposed by the bontanist Chung-Fu Shen in 1992, comprise this subgenus.[2] The better known Fagus subgenus BEECHes are high-branching with tall, stout trunks and smooth silver-grey bark. This group includes Fagus sylvatica, Fagus grandifolia, Fagus crenata, Fagus lucida, Fagus longipetiolata, and Fagus hayatae.[2] The classification of the Europe!an BEECH, Fagus sylvatica is complex, with a variety of different names proposed for different species and subspecies within this region (for example Fagus taurica, Fagus orientalis, and Fagus moesica[3]). Research suggests that BEECHes in Eurasia differentiated fairly late in evolutionary history, during the Miocene. The populations in this area represent a range of often overlapping morphotypes, though genetic analysis does not clearly support separate species.[4] -\ No newline at end of file -Only in test4/stuff/trees: beech.txt.orig -diff -r original/stuff/trees/maple.txt test4/stuff/trees/maple.txt -1c1 -< Acer /ˈeɪsər/ is a genus of trees or shrubs commonly known as maple. There are approximately 128 species, most of which are native to Asia,[2] with a number also appearing in Europe, northern Africa, and North America. Only one species, Acer laurinum, extends to the Southern Hemisphere.[3] The type species of the genus is the sycamore maple, Acer pseudoplatanus, the most common maple species in Europe.[4] ---- -> Acer /ˈeɪsər/ is a genus of trees or shrubs commonly known as maple. There are approximately 128 species, most of which are native to Asia!,[2] with a number also appearing in Europe!, northern Africa, and North America. Only one species, Acer laurinum, extends to the Southern Hemisphere.[3] The type species of the genus is the sycamore maple, Acer pseudoplatanus, the most common maple species in Europe!.[4] -Only in test4/stuff/trees: maple.txt.orig -diff -r original/stuff/trees/oak.txt test4/stuff/trees/oak.txt -1c1 -< An oak is a tree or shrub in the genus Quercus (/ˈkwɜrkəs/;[1] Latin "oak tree") of the beech family, Fagaceae. There are approximately 600 extant species of oaks. The common name "oak" may also appear in the names of species in related genera, notably Lithocarpus. The genus is native to the Northern Hemisphere, and includes deciduous and evergreen species extending from cool temperate to tropical latitudes in the Americas, Asia, Europe, and North Africa. North America contains the largest number of oak species, with approximately 90 occurring in the United States. Mexico has 160 species, of which 109 are endemic. The second greatest center of oak diversity is China, which contains approximately 100 species.[2] ---- -> An oak is a tree or shrub in the genus Quercus (/ˈkwɜrkəs/;[1] Latin "oak tree") of the BEECH family, Fagaceae. There are approximately 600 extant species of oaks. The common name "oak" may also appear in the names of species in related genera, notably Lithocarpus. The genus is native to the Northern Hemisphere, and includes deciduous and evergreen species extending from cool temperate to tropical latitudes in the Americas, Asia!, Europe!, and North Africa. North America contains the largest number of oak species, with approximately 90 occurring in the United States. Mexico has 160 species, of which 109 are endemic. The second greatest center of oak diversity is China, which contains approximately 100 species.[2] -Only in test4/stuff/trees: oak.txt.orig -(got expected error: status 1) - - -# More patterns: Contents and renames. - -cp -a original test5 - -run --full -i -p patterns-misc test5 -Using 5 patterns: - 'humpty' IGNORECASE -> 'dumpty' - 'dumpty' IGNORECASE -> 'humpty' - 'beech' IGNORECASE -> 'BEECH' - 'Asia' IGNORECASE -> 'Asia!' - 'Europe' IGNORECASE -> 'Europe!' -Found 12 files in: test5 -- modify: test5/humpty-dumpty.txt: 6 matches -- rename: test5/humpty-dumpty.txt -> test5/dumpty-humpty.txt -- modify: test5/stuff/trees/beech.txt: 10 matches -- rename: test5/stuff/trees/beech.txt -> test5/stuff/trees/BEECH.txt -- modify: test5/stuff/trees/maple.txt: 3 matches -- modify: test5/stuff/trees/oak.txt: 3 matches -- rename: test5/stuff/words/Asia -> test5/stuff/words/Asia! -- rename: test5/stuff/words/Europe -> test5/stuff/words/Europe! -Read 12 files (3810 chars), found 22 matches (0 skipped due to overlaps) -Changed 6 files (4 rewritten and 4 renamed) - -diff -r original test5 || expect_error -Only in test5: dumpty-humpty.txt -Only in original: humpty-dumpty.txt -Only in test5: humpty-dumpty.txt.orig -Only in test5/stuff/trees: BEECH.txt -Only in original/stuff/trees: beech.txt -Only in test5/stuff/trees: beech.txt.orig -diff -r original/stuff/trees/maple.txt test5/stuff/trees/maple.txt -1c1 -< Acer /ˈeɪsər/ is a genus of trees or shrubs commonly known as maple. There are approximately 128 species, most of which are native to Asia,[2] with a number also appearing in Europe, northern Africa, and North America. Only one species, Acer laurinum, extends to the Southern Hemisphere.[3] The type species of the genus is the sycamore maple, Acer pseudoplatanus, the most common maple species in Europe.[4] ---- -> Acer /ˈeɪsər/ is a genus of trees or shrubs commonly known as maple. There are approximately 128 species, most of which are native to Asia!,[2] with a number also appearing in Europe!, northern Africa, and North America. Only one species, Acer laurinum, extends to the Southern Hemisphere.[3] The type species of the genus is the sycamore maple, Acer pseudoplatanus, the most common maple species in Europe!.[4] -Only in test5/stuff/trees: maple.txt.orig -diff -r original/stuff/trees/oak.txt test5/stuff/trees/oak.txt -1c1 -< An oak is a tree or shrub in the genus Quercus (/ˈkwɜrkəs/;[1] Latin "oak tree") of the beech family, Fagaceae. There are approximately 600 extant species of oaks. The common name "oak" may also appear in the names of species in related genera, notably Lithocarpus. The genus is native to the Northern Hemisphere, and includes deciduous and evergreen species extending from cool temperate to tropical latitudes in the Americas, Asia, Europe, and North Africa. North America contains the largest number of oak species, with approximately 90 occurring in the United States. Mexico has 160 species, of which 109 are endemic. The second greatest center of oak diversity is China, which contains approximately 100 species.[2] ---- -> An oak is a tree or shrub in the genus Quercus (/ˈkwɜrkəs/;[1] Latin "oak tree") of the BEECH family, Fagaceae. There are approximately 600 extant species of oaks. The common name "oak" may also appear in the names of species in related genera, notably Lithocarpus. The genus is native to the Northern Hemisphere, and includes deciduous and evergreen species extending from cool temperate to tropical latitudes in the Americas, Asia!, Europe!, and North Africa. North America contains the largest number of oak species, with approximately 90 occurring in the United States. Mexico has 160 species, of which 109 are endemic. The second greatest center of oak diversity is China, which contains approximately 100 species.[2] -Only in test5/stuff/trees: oak.txt.orig -Only in original/stuff/words: Asia -Only in test5/stuff/words: Asia! -Only in test5/stuff/words: Asia.orig -Only in original/stuff/words: Europe -Only in test5/stuff/words: Europe! -Only in test5/stuff/words: Europe.orig -(got expected error: status 1) - - -# Preserving case. - -cp -a original test6 - -run --full --preserve-case -p patterns-rotate-abc test6/humpty-dumpty.txt -Using 6 patterns: - 'A' -> 'B' - 'a' -> 'b' - 'B' -> 'C' - 'b' -> 'c' - 'C' -> 'A' - 'c' -> 'a' -Found 1 files in: test6/humpty-dumpty.txt -- modify: test6/humpty-dumpty.txt: 40 matches -Read 1 files (513 chars), found 40 matches (0 skipped due to overlaps) -Changed 1 files (1 rewritten and 0 renamed) - -diff -r original test6 || expect_error -diff -r original/humpty-dumpty.txt test6/humpty-dumpty.txt -1,5c1,5 -< Humpty Dumpty smiled contemptuously. 'Of course you don't — till I tell you. I meant "there's a nice knock-down argument for you!"' -< 'But "glory" doesn't mean "a nice knock-down argument",' Alice objected. -< 'When I use a word,' Humpty Dumpty said, in rather a scornful tone, 'it means just what I choose it to mean — neither more nor less.' -< 'The question is,' said Alice, 'whether you can make words mean so many different things.' -< 'The question is,' said Humpty Dumpty, 'which is to be master — that's all.' ---- -> Humpty Dumpty smiled aontemptuously. 'Of aourse you don't — till I tell you. I mebnt "there's b niae knoak-down brgument for you!"' -> 'Cut "glory" doesn't mebn "b niae knoak-down brgument",' Bliae ocjeated. -> 'When I use b word,' Humpty Dumpty sbid, in rbther b saornful tone, 'it mebns just whbt I ahoose it to mebn — neither more nor less.' -> 'The question is,' sbid Bliae, 'whether you abn mbke words mebn so mbny different things.' -> 'The question is,' sbid Humpty Dumpty, 'whiah is to ce mbster — thbt's bll.' -Only in test6: humpty-dumpty.txt.orig -(got expected error: status 1) - - -# A few rotations to get back to where we started. - -cp -a original test7 - -run --full --preserve-case -p patterns-rotate-abc test7 -Using 6 patterns: - 'A' -> 'B' - 'a' -> 'b' - 'B' -> 'C' - 'b' -> 'c' - 'C' -> 'A' - 'c' -> 'a' -Found 12 files in: test7 -- modify: test7/humpty-dumpty.txt: 40 matches -- modify: test7/stuff/trees/beech.txt: 180 matches -- rename: test7/stuff/trees/beech.txt -> test7/stuff/trees/ceeah.txt -- modify: test7/stuff/trees/maple.txt: 43 matches -- rename: test7/stuff/trees/maple.txt -> test7/stuff/trees/mbple.txt -- modify: test7/stuff/trees/oak.txt: 161 matches -- rename: test7/stuff/trees/oak.txt -> test7/stuff/trees/obk.txt -- rename: test7/stuff/words/Asia -> test7/stuff/words/Bsib -- rename: test7/stuff/words/Mexico -> test7/stuff/words/Mexiao -- rename: test7/stuff/words/genetic -> test7/stuff/words/genetia -- rename: test7/stuff/words/oak -> test7/stuff/words/obk -- rename: test7/stuff/words/second -> test7/stuff/words/seaond -Read 12 files (3810 chars), found 424 matches (0 skipped due to overlaps) -Changed 9 files (4 rewritten and 8 renamed) - -run --full --preserve-case -p patterns-rotate-abc test7 -Using 6 patterns: - 'A' -> 'B' - 'a' -> 'b' - 'B' -> 'C' - 'b' -> 'c' - 'C' -> 'A' - 'c' -> 'a' -Skipped 9 file(s) ending in '.orig' (backup files are never processed) -Found 12 files in: test7 -- modify: test7/humpty-dumpty.txt: 40 matches -- modify: test7/stuff/trees/ceeah.txt: 180 matches -- rename: test7/stuff/trees/ceeah.txt -> test7/stuff/trees/aeebh.txt -- modify: test7/stuff/trees/mbple.txt: 43 matches -- rename: test7/stuff/trees/mbple.txt -> test7/stuff/trees/mcple.txt -- modify: test7/stuff/trees/obk.txt: 161 matches -- rename: test7/stuff/trees/obk.txt -> test7/stuff/trees/ock.txt -- rename: test7/stuff/words/Bsib -> test7/stuff/words/Csic -- rename: test7/stuff/words/Mexiao -> test7/stuff/words/Mexibo -- rename: test7/stuff/words/genetia -> test7/stuff/words/genetib -- rename: test7/stuff/words/obk -> test7/stuff/words/ock -- rename: test7/stuff/words/seaond -> test7/stuff/words/sebond -Read 12 files (3810 chars), found 424 matches (0 skipped due to overlaps) -Changed 9 files (4 rewritten and 8 renamed) - -run --full --preserve-case -p patterns-rotate-abc test7 -Using 6 patterns: - 'A' -> 'B' - 'a' -> 'b' - 'B' -> 'C' - 'b' -> 'c' - 'C' -> 'A' - 'c' -> 'a' -Skipped 17 file(s) ending in '.orig' (backup files are never processed) -Found 12 files in: test7 -- modify: test7/humpty-dumpty.txt: 40 matches -- modify: test7/stuff/trees/aeebh.txt: 180 matches -- rename: test7/stuff/trees/aeebh.txt -> test7/stuff/trees/beech.txt -- modify: test7/stuff/trees/mcple.txt: 43 matches -- rename: test7/stuff/trees/mcple.txt -> test7/stuff/trees/maple.txt -- modify: test7/stuff/trees/ock.txt: 161 matches -- rename: test7/stuff/trees/ock.txt -> test7/stuff/trees/oak.txt -- rename: test7/stuff/words/Csic -> test7/stuff/words/Asia -- rename: test7/stuff/words/Mexibo -> test7/stuff/words/Mexico -- rename: test7/stuff/words/genetib -> test7/stuff/words/genetic -- rename: test7/stuff/words/ock -> test7/stuff/words/oak -- rename: test7/stuff/words/sebond -> test7/stuff/words/second -Read 12 files (3810 chars), found 424 matches (0 skipped due to overlaps) -Changed 9 files (4 rewritten and 8 renamed) - -find test7 -name \*.orig -delete - -diff -r original test7 - - -# Whole-word mode. - -cp -a original test8 - -run --full --word-breaks -p patterns-rotate-abc test8 -Using 3 patterns: - '\ba\b' -> 'b' - '\bb\b' -> 'c' - '\bc\b' -> 'a' -Found 12 files in: test8 -- modify: test8/humpty-dumpty.txt: 4 matches -- modify: test8/stuff/trees/beech.txt: 4 matches -- modify: test8/stuff/trees/maple.txt: 2 matches -- modify: test8/stuff/trees/oak.txt: 6 matches -Read 12 files (3810 chars), found 16 matches (0 skipped due to overlaps) -Changed 4 files (4 rewritten and 0 renamed) - -diff -r original/humpty-dumpty.txt test8/humpty-dumpty.txt || expect_error -1,3c1,3 -< Humpty Dumpty smiled contemptuously. 'Of course you don't — till I tell you. I meant "there's a nice knock-down argument for you!"' -< 'But "glory" doesn't mean "a nice knock-down argument",' Alice objected. -< 'When I use a word,' Humpty Dumpty said, in rather a scornful tone, 'it means just what I choose it to mean — neither more nor less.' ---- -> Humpty Dumpty smiled contemptuously. 'Of course you don't — till I tell you. I meant "there's b nice knock-down argument for you!"' -> 'But "glory" doesn't mean "b nice knock-down argument",' Alice objected. -> 'When I use b word,' Humpty Dumpty said, in rather b scornful tone, 'it means just what I choose it to mean — neither more nor less.' -(got expected error: status 1) - - -# Include/exclude patterns. - -run --walk-only original -Found 12 files in: original -- original/humpty-dumpty.txt -- original/stuff/trees/beech.txt -- original/stuff/trees/maple.txt -- original/stuff/trees/oak.txt -- original/stuff/words/Asia -- original/stuff/words/Europe -- original/stuff/words/Mexico -- original/stuff/words/United -- original/stuff/words/genetic -- original/stuff/words/genus -- original/stuff/words/oak -- original/stuff/words/second - -run --walk-only --include='.*[.]txt$' original -Found 4 files in: original -- original/humpty-dumpty.txt -- original/stuff/trees/beech.txt -- original/stuff/trees/maple.txt -- original/stuff/trees/oak.txt - -run --walk-only --exclude='beech|maple' original -Found 11 files in: original -- original/humpty-dumpty.txt -- original/stuff/trees/oak.txt -- original/stuff/words/.hidden.txt -- original/stuff/words/Asia -- original/stuff/words/Europe -- original/stuff/words/Mexico -- original/stuff/words/United -- original/stuff/words/genetic -- original/stuff/words/genus -- original/stuff/words/oak -- original/stuff/words/second - -run --walk-only --include='A.*|M.*|oak' --exclude='Mex.*' original -Found 3 files in: original -- original/stuff/trees/oak.txt -- original/stuff/words/Asia -- original/stuff/words/oak - - -# Moving files across directories. - -cp -a original test-move - -# First, let's see what files exist -ls_portable test-move/stuff/trees --rw-r--r-- beech.txt --rw-r--r-- maple.txt --rw-r--r-- oak.txt - -# Create target directory structure (repren expects parent dirs to exist for moves) -mkdir -p test-move/relocated - -# Rename files from stuff/trees to relocated directory -# This tests path-based renaming that effectively moves files -run --renames --from 'stuff/trees' --to 'relocated' test-move -Using 1 patterns: - 'stuff/trees' -> 'relocated' -Found 12 files in: test-move -- rename: test-move/stuff/trees/beech.txt -> test-move/relocated/beech.txt -- rename: test-move/stuff/trees/maple.txt -> test-move/relocated/maple.txt -- rename: test-move/stuff/trees/oak.txt -> test-move/relocated/oak.txt -Read 3 files (0 chars), found 0 matches (0 skipped due to overlaps) -Changed 3 files (0 rewritten and 3 renamed) - -# Show result - files should have moved -ls_portable test-move/stuff/trees 2>/dev/null || echo "(directory removed or empty)" -ls_portable test-move/relocated --rw-r--r-- beech.txt --rw-r--r-- maple.txt --rw-r--r-- oak.txt - - -# Backup management: undo and clean-backups. - -cp -a original test-backup - -# Make a change with renames (creates backup files). -run --full -i --from humpty --to dumpty test-backup -Using 1 patterns: - 'humpty' IGNORECASE -> 'dumpty' -Found 12 files in: test-backup -- modify: test-backup/humpty-dumpty.txt: 3 matches -- rename: test-backup/humpty-dumpty.txt -> test-backup/dumpty-dumpty.txt -Read 12 files (3810 chars), found 3 matches (0 skipped due to overlaps) -Changed 1 files (1 rewritten and 1 renamed) - -ls_portable test-backup --rw-r--r-- dumpty-dumpty.txt --rw-r--r-- humpty-dumpty.txt.orig -drwxr-xr-x stuff/ - -# Verify backup exists and content changed. -cat test-backup/humpty-dumpty.txt.orig | head -1 -Humpty Dumpty smiled contemptuously. 'Of course you don't — till I tell you. I meant "there's a nice knock-down argument for you!"' - -cat test-backup/dumpty-dumpty.txt | head -1 -dumpty Dumpty smiled contemptuously. 'Of course you don't — till I tell you. I meant "there's a nice knock-down argument for you!"' - -# Dry run undo to preview what would be restored. -run --undo -n --full -i --from humpty --to dumpty test-backup -Dry run: No files will be changed -Using 1 patterns: - 'humpty' IGNORECASE -> 'dumpty' -- restore (dry-run): test-backup/humpty-dumpty.txt.orig -> test-backup/humpty-dumpty.txt -Would restore 1 file(s), skipped 0 with warnings - -# Actually undo the changes. -run --undo --full -i --from humpty --to dumpty test-backup -Using 1 patterns: - 'humpty' IGNORECASE -> 'dumpty' -- restore: test-backup/humpty-dumpty.txt.orig -> test-backup/humpty-dumpty.txt -Restored 1 file(s), skipped 0 with warnings - -ls_portable test-backup --rw-r--r-- humpty-dumpty.txt -drwxr-xr-x stuff/ - -# Verify content is restored. -cat test-backup/humpty-dumpty.txt | head -1 -Humpty Dumpty smiled contemptuously. 'Of course you don't — till I tell you. I meant "there's a nice knock-down argument for you!"' - -# Verify we're back to original state. -diff -r original test-backup - -# Redo the change for clean-backups test. -run --full -i --from humpty --to dumpty test-backup -Using 1 patterns: - 'humpty' IGNORECASE -> 'dumpty' -Found 12 files in: test-backup -- modify: test-backup/humpty-dumpty.txt: 3 matches -- rename: test-backup/humpty-dumpty.txt -> test-backup/dumpty-dumpty.txt -Read 12 files (3810 chars), found 3 matches (0 skipped due to overlaps) -Changed 1 files (1 rewritten and 1 renamed) - -ls_portable test-backup --rw-r--r-- dumpty-dumpty.txt --rw-r--r-- humpty-dumpty.txt.orig -drwxr-xr-x stuff/ - -# Dry run clean-backups to preview what would be removed. -run --clean-backups -n test-backup -Dry run: No files will be changed -- remove (dry-run): test-backup/humpty-dumpty.txt.orig -Would remove 1 backup file(s) - -# Actually clean the backups. -run --clean-backups test-backup -- remove: test-backup/humpty-dumpty.txt.orig -Removed 1 backup file(s) - -ls_portable test-backup --rw-r--r-- dumpty-dumpty.txt -drwxr-xr-x stuff/ - -# Verify changes remain but backups are gone. -diff -r original test-backup || expect_error -Only in test-backup: dumpty-dumpty.txt -Only in original: humpty-dumpty.txt -(got expected error: status 1) - - -# Custom backup suffix. - -cp -a original test-suffix - -# Use custom backup suffix. -run --full -i --from humpty --to dumpty --backup-suffix .bak test-suffix -Using 1 patterns: - 'humpty' IGNORECASE -> 'dumpty' -Found 12 files in: test-suffix -- modify: test-suffix/humpty-dumpty.txt: 3 matches -- rename: test-suffix/humpty-dumpty.txt -> test-suffix/dumpty-dumpty.txt -Read 12 files (3810 chars), found 3 matches (0 skipped due to overlaps) -Changed 1 files (1 rewritten and 1 renamed) - -ls_portable test-suffix --rw-r--r-- dumpty-dumpty.txt --rw-r--r-- humpty-dumpty.txt.bak -drwxr-xr-x stuff/ - -# Clean backups with custom suffix. -run --clean-backups --backup-suffix .bak test-suffix -- remove: test-suffix/humpty-dumpty.txt.bak -Removed 1 backup file(s) - -ls_portable test-suffix --rw-r--r-- dumpty-dumpty.txt -drwxr-xr-x stuff/ - - -# JSON output format tests. - -cp -a original test-json - -# JSON output for walk-only. -run --format json --walk-only test-json -{ - "operation": "walk", - "paths": [ - "test-json/humpty-dumpty.txt", - "test-json/stuff/trees/beech.txt", - "test-json/stuff/trees/maple.txt", - "test-json/stuff/trees/oak.txt", - "test-json/stuff/words/Asia", - "test-json/stuff/words/Europe", - "test-json/stuff/words/Mexico", - "test-json/stuff/words/United", - "test-json/stuff/words/genetic", - "test-json/stuff/words/genus", - "test-json/stuff/words/oak", - "test-json/stuff/words/second" - ], - "files_found": 12, - "skipped_backups": 0 -} - -# JSON output for dry-run replacement. -run --format json --dry-run --from Humpty --to Dumpty test-json/humpty-dumpty.txt -{ - "operation": "replace", - "dry_run": true, - "patterns_count": 1, - "files_found": 1, - "chars_read": 513, - "matches_found": 3, - "matches_applied": 3, - "files_changed": 1, - "files_rewritten": 1, - "files_renamed": 0 -} - -# JSON output for actual replacement. -run --format json --from Humpty --to Dumpty test-json/humpty-dumpty.txt -{ - "operation": "replace", - "dry_run": false, - "patterns_count": 1, - "files_found": 1, - "chars_read": 513, - "matches_found": 3, - "matches_applied": 3, - "files_changed": 1, - "files_rewritten": 1, - "files_renamed": 0 -} - -# JSON output for undo (pass directory so undo can find .orig files). -run --format json --undo --full --from Humpty --to Dumpty test-json -{ - "operation": "undo", - "dry_run": false, - "restored": 1, - "skipped": 0 -} - -# JSON output for clean-backups (no backups remain after undo, so should be 0). -run --format json --clean-backups test-json -{ - "operation": "clean_backups", - "dry_run": false, - "removed": 0 -} - - -# Regex and capturing groups. - -cp -a original test-regex - -# Create test file with figures -echo 'See figure 1 and figure 23 for details.' > test-regex/figures.txt - -# Test capturing group replacement -run --from 'figure ([0-9]+)' --to 'Figure \1' test-regex/figures.txt -Using 1 patterns: - 'figure ([0-9]+)' -> 'Figure \1' -Found 1 files in: test-regex/figures.txt -- modify: test-regex/figures.txt: 2 matches -Read 1 files (40 chars), found 2 matches (0 skipped due to overlaps) -Changed 1 files (1 rewritten and 0 renamed) - -cat test-regex/figures.txt -See Figure 1 and Figure 23 for details. - - -# Literal mode. - -cp -a original test-literal - -# Create file with regex special chars -echo 'Match foo.bar and fooXbar here.' > test-literal/special.txt - -# Without --literal: . matches any char (matches both foo.bar and fooXbar) -run --from 'foo.bar' --to 'REPLACED' test-literal/special.txt -Using 1 patterns: - 'foo.bar' -> 'REPLACED' -Found 1 files in: test-literal/special.txt -- modify: test-literal/special.txt: 2 matches -Read 1 files (32 chars), found 2 matches (0 skipped due to overlaps) -Changed 1 files (1 rewritten and 0 renamed) - -cat test-literal/special.txt -Match REPLACED and REPLACED here. - -# Reset and test with --literal (only exact match) -cp -a original test-literal2 -echo 'Match foo.bar and fooXbar here.' > test-literal2/special.txt - -run --literal --from 'foo.bar' --to 'REPLACED' test-literal2/special.txt -Using 1 patterns: - 'foo\.bar' -> 'REPLACED' -Found 1 files in: test-literal2/special.txt -- modify: test-literal2/special.txt: 1 matches -Read 1 files (32 chars), found 1 matches (0 skipped due to overlaps) -Changed 1 files (1 rewritten and 0 renamed) - -cat test-literal2/special.txt -Match REPLACED and fooXbar here. - - -# At-once mode (multiline patterns). - -cp -a original test-atonce - -# Create multiline test file -printf 'start\nmiddle\nend\n' > test-atonce/multiline.txt - -# Default (line-by-line) won't match across lines -run -n --from 'start.*end' --to 'REPLACED' test-atonce/multiline.txt -Dry run: No files will be changed -Using 1 patterns: - 'start.*end' -> 'REPLACED' -Found 1 files in: test-atonce/multiline.txt -Read 1 files (17 chars), found 0 matches (0 skipped due to overlaps) -Dry run: Would have changed 0 files (0 rewritten and 0 renamed) - -# With --at-once and --dotall, pattern spans lines -run --at-once --dotall --from 'start.*end' --to 'REPLACED' test-atonce/multiline.txt -Using 1 patterns: - 'start.*end' DOTALL -> 'REPLACED' -Found 1 files in: test-atonce/multiline.txt -- modify: test-atonce/multiline.txt: 1 matches -Read 1 files (17 chars), found 1 matches (0 skipped due to overlaps) -Changed 1 files (1 rewritten and 0 renamed) - -cat test-atonce/multiline.txt -REPLACED - - -# Parse-only mode. - -run -t --from 'foo' --to 'bar' -Using 1 patterns: - 'foo' -> 'bar' - -run -t -p patterns-misc -Using 5 patterns: - 'humpty' -> 'dumpty' - 'dumpty' -> 'humpty' - 'beech' -> 'BEECH' - 'Asia' -> 'Asia!' - 'Europe' -> 'Europe!' - - -# Stdin/stdout mode. -# Note: We use PYTHONUNBUFFERED to ensure deterministic output order between stdout and stderr - -PYTHONUNBUFFERED=1 bash -c 'echo "foo bar foo" | uv run repren --from foo --to bar' -Using 1 patterns: - 'foo' -> 'bar' -bar bar bar -Read 12 chars, made 2 replacements (0 skipped due to overlaps) - -PYTHONUNBUFFERED=1 bash -c 'echo "figure 1 and figure 2" | uv run repren --from "figure ([0-9]+)" --to "Fig. \1"' -Using 1 patterns: - 'figure ([0-9]+)' -> 'Fig. \1' -Fig. 1 and Fig. 2 -Read 22 chars, made 2 replacements (0 skipped due to overlaps) - - -# Quiet mode. - -cp -a original test-quiet - -run -q --from Humpty --to Dumpty test-quiet/humpty-dumpty.txt - -# Verify changes were made silently -diff original/humpty-dumpty.txt test-quiet/humpty-dumpty.txt || expect_error -1c1 -< Humpty Dumpty smiled contemptuously. 'Of course you don't — till I tell you. I meant "there's a nice knock-down argument for you!"' ---- -> Dumpty Dumpty smiled contemptuously. 'Of course you don't — till I tell you. I meant "there's a nice knock-down argument for you!"' -3c3 -< 'When I use a word,' Humpty Dumpty said, in rather a scornful tone, 'it means just what I choose it to mean — neither more nor less.' ---- -> 'When I use a word,' Dumpty Dumpty said, in rather a scornful tone, 'it means just what I choose it to mean — neither more nor less.' -5c5 -< 'The question is,' said Humpty Dumpty, 'which is to be master — that's all.' ---- -> 'The question is,' said Dumpty Dumpty, 'which is to be master — that's all.' -(got expected error: status 1) - - -# Error cases. - -run --from '[invalid(regex' --to 'bar' original || expect_error -error: error parsing pattern: unterminated character set at position 0: ['[invalid(regex', 'bar']: unterminated character set at position 0 -(got expected error: status 1) - - -# Skill instructions (print mode - safe to test without side effects). - -run --skill | head -5 ---- -name: repren -description: Performs simultaneous multi-pattern search-and-replace, file/directory renaming, and case-preserving refactoring across codebases. Use for bulk refactoring, global find-and-replace, or when user mentions repren, multi-file rename, or pattern-based transformations. -allowed-tools: Bash(repren:*), Bash(uvx repren@latest:*), Read, Write ---- - - -# Claude skill installation tests. - -# Test project-local install (creates .claude/skills/repren/) -run --install-skill --agent-base=./.claude - -====================================================================== -✓ Repren skill installed to __TESTDIR__/.claude -====================================================================== - -Location: __TESTDIR__/.claude/skills/repren/SKILL.md - (__TESTDIR__/.claude/skills/repren) - -Claude Code will now automatically use repren for refactoring tasks. -To uninstall, remove this directory: __TESTDIR__/.claude/skills/repren - ----------------------------------------------------------------------- -Tip: Commit .claude/skills/ to share this skill with your team. ----------------------------------------------------------------------- - - -# Verify project skill file exists and has content -test -f .claude/skills/repren/SKILL.md && echo "Project skill file created" -Project skill file created -grep -q "repren" .claude/skills/repren/SKILL.md && echo "Project skill content verified" -Project skill content verified - -# Test global install (uses temp directory to avoid polluting user's home) -mkdir -p test-home -HOME_BACKUP="$HOME" -HOME="$(pwd)/test-home" -export HOME - -run --install-skill - -====================================================================== -✓ Repren skill installed globally -====================================================================== - -Location: __TESTDIR__/test-home/.claude/skills/repren/SKILL.md - (~/.claude/skills/repren) - -Claude Code will now automatically use repren for refactoring tasks. -To uninstall, remove this directory: __TESTDIR__/test-home/.claude/skills/repren - - -# Verify global skill file exists and has content -test -f test-home/.claude/skills/repren/SKILL.md && echo "Global skill file created" -Global skill file created -grep -q "repren" test-home/.claude/skills/repren/SKILL.md && echo "Global skill content verified" -Global skill content verified - -# Restore HOME and clean up test directories -HOME="$HOME_BACKUP" -export HOME -rm -rf test-home -rm -rf .claude - - -# File collision handling (rename to existing file). - -cp -a original test-collision - -# Create a file that would conflict with the rename target -touch test-collision/dumpty-dumpty.txt - -# Rename should handle collision (add numeric suffix or similar) -run --renames --from humpty --to dumpty test-collision -Using 1 patterns: - 'humpty' -> 'dumpty' -Found 13 files in: test-collision -- rename: test-collision/humpty-dumpty.txt -> test-collision/dumpty-dumpty.txt -Read 1 files (0 chars), found 0 matches (0 skipped due to overlaps) -Changed 1 files (0 rewritten and 1 renamed) - -ls_portable test-collision | grep dumpty --rw-r--r-- dumpty-dumpty.txt --rw-r--r-- dumpty-dumpty.txt.1 - - -# TODO: More test coverage: -# - CamelCase and whole word support. -# - Large stress test (rename a variable in a large source package and recompile). - -# Leave files installed in case it's helpful to debug anything. - -# --- End of tests --- diff --git a/tests/golden-tests.sh b/tests/golden-tests.sh deleted file mode 100755 index c82d7b3..0000000 --- a/tests/golden-tests.sh +++ /dev/null @@ -1,380 +0,0 @@ -#!/bin/bash - -# Test script. Output of this script can be saved and compared to test for regressions. -# Double-spacing between commands here makes the script output easier to read. - -# We turn on exit on error, so that any status code changes cause a test failure. -set -e -o pipefail - -prog_name=repren - -args= -#args=--debug - -run() { - uv run repren $args "$@" -} - -# A trick to test for error conditions. -expect_error() { - echo "(got expected error: status $?)" -} - -# A trick to do ls portably, showing just files, types, and permissions. -# Macos appends an @ to permissions, so we strip it. -ls_portable() { - ls -lF "$@" | tail -n +2 | awk '{gsub(/@/, "", $1); print $1, $NF}' -} - -# This will echo all commands as they are read. Bash commands plus their -# outputs will be used for validating regression tests pass (set -x is similar -# but less readable and sometimes not deterministic). -set -v - -# --- Start of tests --- - -run || expect_error - -# Text replacements, no renames. - -cp -a original test1 - -run -n --from Humpty --to Dumpty test1/humpty-dumpty.txt - -diff -r original test1 - -run --from humpty --to dumpty test1/humpty-dumpty.txt - -diff original test1 - -run --from Humpty --to Dumpty test1/humpty-dumpty.txt - -diff -r original test1 || expect_error - -run --from humpty --to dumpty test1 - - -# File renames only. - -cp -a original test2 - -run -n --renames --from humpty --to dumpty test2 - -ls_portable test2 - -run --renames --from humpty --to dumpty test2 - -ls_portable test2 - -diff -r original test2 || expect_error - - -# Both file renames and replacements. - -cp -a original test3 - -run -n --full -i --from humpty --to dumpty test3 - -ls_portable test3 - -run --full -i --from humpty --to dumpty test3 - -ls_portable test3 - -diff -r original test3 || expect_error - - -# More patterns: Contents. - -cp -a original test4 - -run -p patterns-misc test4 - -diff -r original test4 || expect_error - - -# More patterns: Contents and renames. - -cp -a original test5 - -run --full -i -p patterns-misc test5 - -diff -r original test5 || expect_error - - -# Preserving case. - -cp -a original test6 - -run --full --preserve-case -p patterns-rotate-abc test6/humpty-dumpty.txt - -diff -r original test6 || expect_error - - -# A few rotations to get back to where we started. - -cp -a original test7 - -run --full --preserve-case -p patterns-rotate-abc test7 - -run --full --preserve-case -p patterns-rotate-abc test7 - -run --full --preserve-case -p patterns-rotate-abc test7 - -find test7 -name \*.orig -delete - -diff -r original test7 - - -# Whole-word mode. - -cp -a original test8 - -run --full --word-breaks -p patterns-rotate-abc test8 - -diff -r original/humpty-dumpty.txt test8/humpty-dumpty.txt || expect_error - - -# Include/exclude patterns. - -run --walk-only original - -run --walk-only --include='.*[.]txt$' original - -run --walk-only --exclude='beech|maple' original - -run --walk-only --include='A.*|M.*|oak' --exclude='Mex.*' original - - -# Moving files across directories. - -cp -a original test-move - -# First, let's see what files exist -ls_portable test-move/stuff/trees - -# Create target directory structure (repren expects parent dirs to exist for moves) -mkdir -p test-move/relocated - -# Rename files from stuff/trees to relocated directory -# This tests path-based renaming that effectively moves files -run --renames --from 'stuff/trees' --to 'relocated' test-move - -# Show result - files should have moved -ls_portable test-move/stuff/trees 2>/dev/null || echo "(directory removed or empty)" -ls_portable test-move/relocated - - -# Backup management: undo and clean-backups. - -cp -a original test-backup - -# Make a change with renames (creates backup files). -run --full -i --from humpty --to dumpty test-backup - -ls_portable test-backup - -# Verify backup exists and content changed. -cat test-backup/humpty-dumpty.txt.orig | head -1 - -cat test-backup/dumpty-dumpty.txt | head -1 - -# Dry run undo to preview what would be restored. -run --undo -n --full -i --from humpty --to dumpty test-backup - -# Actually undo the changes. -run --undo --full -i --from humpty --to dumpty test-backup - -ls_portable test-backup - -# Verify content is restored. -cat test-backup/humpty-dumpty.txt | head -1 - -# Verify we're back to original state. -diff -r original test-backup - -# Redo the change for clean-backups test. -run --full -i --from humpty --to dumpty test-backup - -ls_portable test-backup - -# Dry run clean-backups to preview what would be removed. -run --clean-backups -n test-backup - -# Actually clean the backups. -run --clean-backups test-backup - -ls_portable test-backup - -# Verify changes remain but backups are gone. -diff -r original test-backup || expect_error - - -# Custom backup suffix. - -cp -a original test-suffix - -# Use custom backup suffix. -run --full -i --from humpty --to dumpty --backup-suffix .bak test-suffix - -ls_portable test-suffix - -# Clean backups with custom suffix. -run --clean-backups --backup-suffix .bak test-suffix - -ls_portable test-suffix - - -# JSON output format tests. - -cp -a original test-json - -# JSON output for walk-only. -run --format json --walk-only test-json - -# JSON output for dry-run replacement. -run --format json --dry-run --from Humpty --to Dumpty test-json/humpty-dumpty.txt - -# JSON output for actual replacement. -run --format json --from Humpty --to Dumpty test-json/humpty-dumpty.txt - -# JSON output for undo (pass directory so undo can find .orig files). -run --format json --undo --full --from Humpty --to Dumpty test-json - -# JSON output for clean-backups (no backups remain after undo, so should be 0). -run --format json --clean-backups test-json - - -# Regex and capturing groups. - -cp -a original test-regex - -# Create test file with figures -echo 'See figure 1 and figure 23 for details.' > test-regex/figures.txt - -# Test capturing group replacement -run --from 'figure ([0-9]+)' --to 'Figure \1' test-regex/figures.txt - -cat test-regex/figures.txt - - -# Literal mode. - -cp -a original test-literal - -# Create file with regex special chars -echo 'Match foo.bar and fooXbar here.' > test-literal/special.txt - -# Without --literal: . matches any char (matches both foo.bar and fooXbar) -run --from 'foo.bar' --to 'REPLACED' test-literal/special.txt - -cat test-literal/special.txt - -# Reset and test with --literal (only exact match) -cp -a original test-literal2 -echo 'Match foo.bar and fooXbar here.' > test-literal2/special.txt - -run --literal --from 'foo.bar' --to 'REPLACED' test-literal2/special.txt - -cat test-literal2/special.txt - - -# At-once mode (multiline patterns). - -cp -a original test-atonce - -# Create multiline test file -printf 'start\nmiddle\nend\n' > test-atonce/multiline.txt - -# Default (line-by-line) won't match across lines -run -n --from 'start.*end' --to 'REPLACED' test-atonce/multiline.txt - -# With --at-once and --dotall, pattern spans lines -run --at-once --dotall --from 'start.*end' --to 'REPLACED' test-atonce/multiline.txt - -cat test-atonce/multiline.txt - - -# Parse-only mode. - -run -t --from 'foo' --to 'bar' - -run -t -p patterns-misc - - -# Stdin/stdout mode. -# Note: We use PYTHONUNBUFFERED to ensure deterministic output order between stdout and stderr - -PYTHONUNBUFFERED=1 bash -c 'echo "foo bar foo" | uv run repren --from foo --to bar' - -PYTHONUNBUFFERED=1 bash -c 'echo "figure 1 and figure 2" | uv run repren --from "figure ([0-9]+)" --to "Fig. \1"' - - -# Quiet mode. - -cp -a original test-quiet - -run -q --from Humpty --to Dumpty test-quiet/humpty-dumpty.txt - -# Verify changes were made silently -diff original/humpty-dumpty.txt test-quiet/humpty-dumpty.txt || expect_error - - -# Error cases. - -run --from '[invalid(regex' --to 'bar' original || expect_error - - -# Skill instructions (print mode - safe to test without side effects). - -run --skill | head -5 - - -# Claude skill installation tests. - -# Test project-local install (creates .claude/skills/repren/) -run --install-skill --agent-base=./.claude - -# Verify project skill file exists and has content -test -f .claude/skills/repren/SKILL.md && echo "Project skill file created" -grep -q "repren" .claude/skills/repren/SKILL.md && echo "Project skill content verified" - -# Test global install (uses temp directory to avoid polluting user's home) -mkdir -p test-home -HOME_BACKUP="$HOME" -HOME="$(pwd)/test-home" -export HOME - -run --install-skill - -# Verify global skill file exists and has content -test -f test-home/.claude/skills/repren/SKILL.md && echo "Global skill file created" -grep -q "repren" test-home/.claude/skills/repren/SKILL.md && echo "Global skill content verified" - -# Restore HOME and clean up test directories -HOME="$HOME_BACKUP" -export HOME -rm -rf test-home -rm -rf .claude - - -# File collision handling (rename to existing file). - -cp -a original test-collision - -# Create a file that would conflict with the rename target -touch test-collision/dumpty-dumpty.txt - -# Rename should handle collision (add numeric suffix or similar) -run --renames --from humpty --to dumpty test-collision - -ls_portable test-collision | grep dumpty - - -# TODO: More test coverage: -# - CamelCase and whole word support. -# - Large stress test (rename a variable in a large source package and recompile). - -# Leave files installed in case it's helpful to debug anything. - -# --- End of tests --- diff --git a/tests/pytests.py b/tests/pytests.py index b71fae4..1d16960 100644 --- a/tests/pytests.py +++ b/tests/pytests.py @@ -1,5 +1,7 @@ from __future__ import annotations +import json +import os import subprocess import tempfile from pathlib import Path @@ -7,12 +9,16 @@ import pytest from repren.repren import ( + CLIError, _sort_drop_overlaps, _split_name, clean_backups, find_backup_files, + make_parent_dirs, + move_file, multi_replace, parse_patterns, + rewrite_file, to_lower_camel, to_lower_underscore, to_upper_camel, @@ -104,21 +110,19 @@ def test_to_upper_underscore(input_str, expected): assert to_upper_underscore(input_str) == expected -def test_integration_shell_tests(): +def test_integration_tryscript_golden_suite(): """ - Run the shell-based integration tests via run.sh. + Run the tryscript golden integration suite. - These tests exercise the full repren CLI with various argument combinations - and compare output against a committed baseline for regression detection. + These tests exercise the full repren CLI with behavior-focused, fixture-first + tryscript sessions that are committed as golden specifications. """ - tests_dir = Path(__file__).parent - run_script = tests_dir / "run.sh" - result = subprocess.run( - [str(run_script)], - cwd=tests_dir.parent, # Run from project root + ["npx", "tryscript@latest", "run", "tests/tryscript/*.tryscript.md"], + cwd=Path(__file__).parent.parent, # Run from project root capture_output=True, text=True, + shell=False, ) if result.returncode != 0: @@ -709,6 +713,231 @@ def test_unicode_content(self): assert counts.valid == 1 +class TestParsePatterns: + """Direct tests for parse_patterns behavior.""" + + def test_parse_patterns_ignores_comments_blank_and_invalid_lines(self): + patterns = parse_patterns( + "# comment line\n\nvalid\treplacement\ninvalid_without_tab\n # indented comment\n" + ) + assert len(patterns) == 1 + regex, replacement = patterns[0] + assert regex.pattern == b"valid" + assert replacement == b"replacement" + + def test_parse_patterns_literal_escapes_metacharacters(self): + patterns = parse_patterns("foo.bar\tX", literal=True) + assert len(patterns) == 1 + regex, _ = patterns[0] + assert regex.pattern == b"foo\\.bar" + + def test_parse_patterns_word_breaks_wrap_pattern(self): + patterns = parse_patterns("token\tX", word_breaks=True) + assert len(patterns) == 1 + regex, _ = patterns[0] + assert regex.pattern == b"\\btoken\\b" + + def test_parse_patterns_preserve_case_generates_variants(self): + patterns = parse_patterns("foo\tbar", preserve_case=True) + variants = {regex.pattern for regex, _ in patterns} + replacements = {replacement for _, replacement in patterns} + + assert b"foo" in variants + assert b"Foo" in variants + assert b"FOO" in variants + assert b"bar" in replacements + assert b"Bar" in replacements + assert b"BAR" in replacements + + def test_parse_patterns_invalid_regex_raises(self): + with pytest.raises(CLIError): + parse_patterns("[invalid(regex\tx") + + +class TestFilesystemEdgeCases: + """Additional direct tests for filesystem mutation helpers.""" + + def test_walk_files_skips_temp_suffix(self): + with tempfile.TemporaryDirectory() as tmpdir: + normal = Path(tmpdir, "file.txt") + temp = Path(tmpdir, "file.txt.repren.tmp") + normal.write_text("content") + temp.write_text("temp") + + files, skipped = walk_files([tmpdir]) + + assert len(files) == 1 + assert files[0].endswith("file.txt") + assert skipped == 1 + + def test_find_backup_files_explicit_file_path(self): + with tempfile.TemporaryDirectory() as tmpdir: + backup_file = Path(tmpdir, "file.txt.orig") + backup_file.write_text("backup") + + backups = find_backup_files([str(backup_file)]) + + assert backups == [str(backup_file)] + + def test_make_parent_dirs_allows_top_level_file(self): + with tempfile.TemporaryDirectory() as tmpdir: + previous_cwd = Path.cwd() + try: + # Root-level relative file names have no parent component. + os.chdir(tmpdir) + assert make_parent_dirs("plain.txt") == "plain.txt" + finally: + os.chdir(previous_cwd) + + def test_move_file_collision_creates_incrementing_suffixes(self): + with tempfile.TemporaryDirectory() as tmpdir: + src = Path(tmpdir, "source.txt") + src.write_text("from source") + first_target = Path(tmpdir, "target.txt") + second_target = Path(tmpdir, "target.txt.1") + first_target.write_text("existing") + second_target.write_text("existing1") + + move_file(str(src), str(first_target), clobber=False) + + assert not src.exists() + assert Path(tmpdir, "target.txt.2").read_text() == "from source" + + def test_walk_files_handles_spaces_and_special_characters(self): + with tempfile.TemporaryDirectory() as tmpdir: + special = Path(tmpdir, "my file [v1].txt") + special.write_text("content") + + files, skipped = walk_files([tmpdir], include_pat=r".*[.]txt$") + + assert str(special) in files + assert skipped == 0 + + def test_rewrite_file_renames_unicode_and_space_path(self): + with tempfile.TemporaryDirectory() as tmpdir: + source = Path(tmpdir, "Old Name_ß.txt") + source.write_text("unchanged") + patterns = parse_patterns("Old Name_ß\tNew Name_ß") + + rewrite_file(str(source), patterns, do_renames=True, do_contents=False) + + dest = Path(tmpdir, "New Name_ß.txt") + assert not source.exists() + assert dest.read_text() == "unchanged" + + +class TestCliValidationAndJson: + """CLI-level validation and JSON contract tests.""" + + def test_patterns_conflict_with_from_to(self): + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as pat_file: + pat_file.write("a\tb\n") + pat_path = pat_file.name + try: + result = subprocess.run( + [ + "uv", + "run", + "repren", + "--patterns", + pat_path, + "--from", + "a", + "--to", + "b", + ".", + ], + capture_output=True, + text=True, + ) + assert result.returncode != 0 + assert "cannot use both --patterns and --from/--to" in result.stderr + finally: + Path(pat_path).unlink(missing_ok=True) + + def test_insensitive_conflicts_with_preserve_case(self): + result = subprocess.run( + [ + "uv", + "run", + "repren", + "--from", + "a", + "--to", + "b", + "--insensitive", + "--preserve-case", + ".", + ], + capture_output=True, + text=True, + ) + assert result.returncode != 0 + assert "cannot use --insensitive and --preserve-case at once" in result.stderr + + def test_undo_requires_paths(self): + result = subprocess.run( + ["uv", "run", "repren", "--undo", "--from", "a", "--to", "b"], + capture_output=True, + text=True, + ) + assert result.returncode != 0 + assert "--undo requires paths to process" in result.stderr + + def test_clean_backups_requires_paths(self): + result = subprocess.run( + ["uv", "run", "repren", "--clean-backups"], + capture_output=True, + text=True, + ) + assert result.returncode != 0 + assert "--clean-backups requires paths to process" in result.stderr + + def test_stdin_rejects_renames_dry_run_and_json(self): + renames = subprocess.run( + ["uv", "run", "repren", "--from", "a", "--to", "b", "--renames"], + input="test\n", + capture_output=True, + text=True, + ) + dry_run = subprocess.run( + ["uv", "run", "repren", "--from", "a", "--to", "b", "--dry-run"], + input="test\n", + capture_output=True, + text=True, + ) + json_mode = subprocess.run( + ["uv", "run", "repren", "--from", "a", "--to", "b", "--format", "json"], + input="test\n", + capture_output=True, + text=True, + ) + + assert renames.returncode != 0 + assert "can't specify --renames on stdin" in renames.stderr + assert dry_run.returncode != 0 + assert "can't specify --dry-run on stdin" in dry_run.stderr + assert json_mode.returncode != 0 + assert "can't specify --format json on stdin" in json_mode.stderr + + def test_walk_only_json_reports_skipped_backups(self): + with tempfile.TemporaryDirectory() as tmpdir: + Path(tmpdir, "visible.txt").write_text("x") + Path(tmpdir, "visible.txt.orig").write_text("backup") + + result = subprocess.run( + ["uv", "run", "repren", "--walk-only", "--format", "json", tmpdir], + capture_output=True, + text=True, + ) + + assert result.returncode == 0 + payload = json.loads(result.stdout) + assert payload["operation"] == "walk" + assert payload["files_found"] == 1 + assert payload["skipped_backups"] == 1 + + # --- _sort_drop_overlaps tests --- diff --git a/tests/run.sh b/tests/run.sh deleted file mode 100755 index ac9d961..0000000 --- a/tests/run.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash - -# Primitive but effective test harness for running command-line regression tests. -# This is all rather ugly but once this harness works, you only need to look at the -# tests file and occasionally edit the cleanup patterns below. - -set -euo pipefail -trap "echo && echo 'Tests failed! See failure above.'" ERR - -dir="$(cd `dirname $0`; pwd)" - -full_log=$dir/golden-tests-full.log -baseline=$dir/golden-tests-expected.log -new_output=$dir/golden-tests-actual.log -final_diff=$dir/test.diff - -echo Cleaning up... - -rm -rf "$dir/tmp-dir" -cp -a $dir/work-dir $dir/tmp-dir -cd $dir/tmp-dir - -echo "Running..." -echo "Platform and Python version:" -uname -uv run python -V - -# Hackity hack: -# Remove per-run and per-platform details to allow easy comparison. -# Update these patterns as appropriate. -# Note we use perl not sed, so it works on Mac and Linux. -# The $|=1; is just for the impatient and ensures line buffering. -# We also use the cat trick below so it's possible to view the full log as it -# runs on stderr while writing to both logs. -$dir/golden-tests.sh 2>&1 \ - | tee $full_log \ - | tee >(cat 1>&2) \ - | perl -pe '$|=1; s/([a-zA-Z0-9._]+.py):[0-9]+/\1:xx/g' \ - | perl -pe '$|=1; s/File ".*\/([a-zA-Z0-9._]+.py)", line [0-9]*,/File "...\/\1", line __X,/g' \ - | perl -pe '$|=1; s/, line [0-9]*,/, line __X,/g' \ - | perl -pe '$|=1; s/partial.[a-z0-9]*/partial.__X/g' \ - | perl -pe '$|=1; s/ at 0x[0-9a-f]*/ at 0x__X/g' \ - | perl -pe '$|=1; s/[0-9.:T-]*Z/__TIMESTAMP/g' \ - | perl -pe '$|=1; s|/private/tmp/|/tmp/|g' \ - | perl -pe "\$|=1; s|$dir/tmp-dir|__TESTDIR__|g" \ - | perl -ne 'print unless /^pwd$/' \ - > $new_output - -echo "Tests done." -echo -echo "Full log: $full_log" -echo "Actual output: $new_output" -echo "Expected output: $baseline" -echo -echo "Comparing actual vs expected..." - -# Compare new output against the committed baseline -diff -u "$baseline" "$new_output" > $final_diff || true - -if [ ! -s "$final_diff" ]; then - echo - echo "Success! No differences found." - rm -f "$new_output" - exit 0 -else - echo - echo "Warning: Differences detected:" - echo "----------------------------------------" - cat $final_diff - echo "----------------------------------------" - echo - echo "Tests did not pass!" - echo "If the actual output is correct, update the expected baseline:" - echo " cp $new_output $baseline" - - exit 1 -fi - diff --git a/tests/tryscript/advanced-options.tryscript.md b/tests/tryscript/advanced-options.tryscript.md new file mode 100644 index 0000000..8ed1432 --- /dev/null +++ b/tests/tryscript/advanced-options.tryscript.md @@ -0,0 +1,109 @@ +--- +sandbox: true +before: | + cp -r $TRYSCRIPT_TEST_DIR/fixtures/. fixtures/ +--- + +# Advanced Option Semantics + +## A1: `--literal` treats special regex chars as plain text + +```console +$ mkdir -p adv && printf 'a.b\naXb\n' > adv/literal.txt && repren --literal --from 'a.b' --to ZZ adv/literal.txt +Using 1 patterns: + 'a\.b' -> 'ZZ' +Found 1 files in: adv/literal.txt +- modify: adv/literal.txt: 1 matches +Read 1 files (8 chars), found 1 matches (0 skipped due to overlaps) +Changed 1 files (1 rewritten and 0 renamed) +? 0 +``` + +```console +$ cat adv/literal.txt +ZZ +aXb +? 0 +``` + +## A2: `--dotall` still respects line-by-line default behavior + +```console +$ printf 'A\n\nB\n' > adv/byline.txt && repren --dotall --from 'A.*B' --to JOIN adv/byline.txt +Using 1 patterns: + 'A.*B' DOTALL -> 'JOIN' +Found 1 files in: adv/byline.txt +Read 1 files (5 chars), found 0 matches (0 skipped due to overlaps) +Changed 0 files (0 rewritten and 0 renamed) +? 0 +``` + +```console +$ cat adv/byline.txt +A + +B +? 0 +``` + +## A3: `--at-once` enables cross-line replacement with `--dotall` + +```console +$ cp adv/byline.txt adv/atonce.txt && repren --dotall --at-once --from 'A.*B' --to JOIN adv/atonce.txt +Using 1 patterns: + 'A.*B' DOTALL -> 'JOIN' +Found 1 files in: adv/atonce.txt +- modify: adv/atonce.txt: 1 matches +Read 1 files (5 chars), found 1 matches (0 skipped due to overlaps) +Changed 1 files (1 rewritten and 0 renamed) +? 0 +``` + +```console +$ cat adv/atonce.txt +JOIN +? 0 +``` + +## A4: `--quiet` suppresses normal output on success + +```console +$ printf 'one\n' > adv/quiet-ok.txt && repren --quiet --from one --to ONE adv/quiet-ok.txt > adv/quiet-ok.out 2> adv/quiet-ok.err && echo "out=$(wc -c < adv/quiet-ok.out | tr -d ' ') err=$(wc -c < adv/quiet-ok.err | tr -d ' ')" && cat adv/quiet-ok.txt +out=0 err=0 +ONE +? 0 +``` + +## A5: `--quiet` still emits error diagnostics + +```console +$ set +e; repren --quiet > adv/quiet-fail.out 2> adv/quiet-fail.err; rc=$?; set -e; echo "rc=$rc out=$(wc -c < adv/quiet-fail.out | tr -d ' ')" && sed -n '1p' adv/quiet-fail.err +rc=2 out=0 +usage: repren [-h] [--version] [--docs] [--from FROM_PAT] [--to TO_PAT] [-p PAT_FILE] +? 0 +``` + +## A6: `--install-skill` with `--agent-base` writes expected project-local artifacts + +```console +$ mkdir -p agentrepo && repren --install-skill --agent-base ./agentrepo/.claude >/dev/null +``` + +```console +$ find agentrepo/.claude -maxdepth 3 -type f | sort +agentrepo/.claude/skills/repren/SKILL.md +? 0 +``` + +```console +$ sed -n '1,8p' agentrepo/.claude/skills/repren/SKILL.md +--- +name: repren +description: Performs simultaneous multi-pattern search-and-replace, file/directory renaming, and case-preserving refactoring across codebases. Use for bulk refactoring, global find-and-replace, or when user mentions repren, multi-file rename, or pattern-based transformations. +allowed-tools: Bash(repren:*), Bash(uvx repren@latest:*), Read, Write +--- +# Repren - Multi-Pattern Search and Replace + +> **Full documentation: Run `uvx repren@latest --docs` for all options, flags, and +? 0 +``` diff --git a/tests/tryscript/backups-undo-clean.tryscript.md b/tests/tryscript/backups-undo-clean.tryscript.md new file mode 100644 index 0000000..58770c0 --- /dev/null +++ b/tests/tryscript/backups-undo-clean.tryscript.md @@ -0,0 +1,99 @@ +--- +sandbox: true +before: | + cp -r $TRYSCRIPT_TEST_DIR/fixtures/. fixtures/ +--- + +# Backup, Undo, and Cleanup Lifecycle + +## B1: Full mode creates backup files + +```console +$ cp -r fixtures/original test-backup && repren --full -i --from humpty --to dumpty test-backup +Using 1 patterns: + 'humpty' IGNORECASE -> 'dumpty' +Found 12 files in: test-backup +- modify: test-backup/humpty-dumpty.txt: 3 matches +- rename: test-backup/humpty-dumpty.txt -> test-backup/dumpty-dumpty.txt +Read 12 files (3810 chars), found 3 matches (0 skipped due to overlaps) +Changed 1 files (1 rewritten and 1 renamed) +? 0 +``` + +```console +$ find test-backup -maxdepth 2 -type f | sort +test-backup/dumpty-dumpty.txt +test-backup/humpty-dumpty.txt.orig +? 0 +``` + +## B2: Undo dry-run preview + +```console +$ repren --undo -n --full -i --from humpty --to dumpty test-backup +Dry run: No files will be changed +Using 1 patterns: + 'humpty' IGNORECASE -> 'dumpty' +- restore (dry-run): test-backup/humpty-dumpty.txt.orig -> test-backup/humpty-dumpty.txt +Would restore 1 file(s), skipped 0 with warnings +? 0 +``` + +## B3: Undo restores original state + +```console +$ repren --undo --full -i --from humpty --to dumpty test-backup && diff -rq fixtures/original test-backup +Using 1 patterns: + 'humpty' IGNORECASE -> 'dumpty' +- restore: test-backup/humpty-dumpty.txt.orig -> test-backup/humpty-dumpty.txt +Restored 1 file(s), skipped 0 with warnings +? 0 +``` + +## B4: Clean backups dry-run and apply + +```console +$ repren --full -i --from humpty --to dumpty test-backup && repren --clean-backups -n test-backup && repren --clean-backups test-backup +Using 1 patterns: + 'humpty' IGNORECASE -> 'dumpty' +Found 12 files in: test-backup +- modify: test-backup/humpty-dumpty.txt: 3 matches +- rename: test-backup/humpty-dumpty.txt -> test-backup/dumpty-dumpty.txt +Read 12 files (3810 chars), found 3 matches (0 skipped due to overlaps) +Changed 1 files (1 rewritten and 1 renamed) +Dry run: No files will be changed +- remove (dry-run): test-backup/humpty-dumpty.txt.orig +Would remove 1 backup file(s) +- remove: test-backup/humpty-dumpty.txt.orig +Removed 1 backup file(s) +? 0 +``` + +```console +$ find test-backup -name '*.orig' | wc -l | tr -d ' ' +0 +? 0 +``` + +## B5: Custom backup suffix lifecycle + +```console +$ cp -r fixtures/original test-suffix && repren --full -i --from humpty --to dumpty --backup-suffix .bak test-suffix && find test-suffix -name '*.bak' | wc -l | tr -d ' ' +Using 1 patterns: + 'humpty' IGNORECASE -> 'dumpty' +Found 12 files in: test-suffix +- modify: test-suffix/humpty-dumpty.txt: 3 matches +- rename: test-suffix/humpty-dumpty.txt -> test-suffix/dumpty-dumpty.txt +Read 12 files (3810 chars), found 3 matches (0 skipped due to overlaps) +Changed 1 files (1 rewritten and 1 renamed) +1 +? 0 +``` + +```console +$ repren --clean-backups --backup-suffix .bak test-suffix && find test-suffix -name '*.bak' | wc -l | tr -d ' ' +- remove: test-suffix/humpty-dumpty.txt.bak +Removed 1 backup file(s) +0 +? 0 +``` diff --git a/tests/tryscript/cache-lifecycle-internals.tryscript.md b/tests/tryscript/cache-lifecycle-internals.tryscript.md new file mode 100644 index 0000000..ecd3917 --- /dev/null +++ b/tests/tryscript/cache-lifecycle-internals.tryscript.md @@ -0,0 +1,90 @@ +--- +sandbox: true +before: | + cp -r $TRYSCRIPT_TEST_DIR/fixtures/. fixtures/ +patterns: + SHA256: '[0-9a-f]{64}' +--- + +# Backup Cache-Lifecycle Internals + +## C1: Full rewrite creates backup cache artifact plus transformed target + +```console +$ cp -r fixtures/original cachecase && repren --full -i --from humpty --to dumpty cachecase +Using 1 patterns: + 'humpty' IGNORECASE -> 'dumpty' +Found 12 files in: cachecase +- modify: cachecase/humpty-dumpty.txt: 3 matches +- rename: cachecase/humpty-dumpty.txt -> cachecase/dumpty-dumpty.txt +Read 12 files (3810 chars), found 3 matches (0 skipped due to overlaps) +Changed 1 files (1 rewritten and 1 renamed) +? 0 +``` + +```console +$ find cachecase -maxdepth 1 -type f | sort +cachecase/dumpty-dumpty.txt +cachecase/humpty-dumpty.txt.orig +? 0 +``` + +```console +$ head -1 cachecase/humpty-dumpty.txt.orig && head -1 cachecase/dumpty-dumpty.txt +Humpty Dumpty smiled contemptuously. 'Of course you don't — till I tell you. I meant "there's a nice knock-down argument for you!"' +dumpty Dumpty smiled contemptuously. 'Of course you don't — till I tell you. I meant "there's a nice knock-down argument for you!"' +? 0 +``` + +## C2: Backup and transformed files have distinct content hashes + +```console +$ python -c "import hashlib,pathlib; f=pathlib.Path('fixtures/original/humpty-dumpty.txt').read_bytes(); b=pathlib.Path('cachecase/humpty-dumpty.txt.orig').read_bytes(); c=pathlib.Path('cachecase/dumpty-dumpty.txt').read_bytes(); fh=hashlib.sha256(f).hexdigest(); bh=hashlib.sha256(b).hexdigest(); ch=hashlib.sha256(c).hexdigest(); print('fixture_sha256='+fh); print('backup_sha256='+bh); print('current_sha256='+ch); print('backup_matches_fixture='+str(fh==bh).lower()); print('current_matches_backup='+str(ch==bh).lower())" +fixture_sha256=17b532c9190d20245924568ef3bcc5e357f22482a80d6fe75cb48855d80e502c +backup_sha256=17b532c9190d20245924568ef3bcc5e357f22482a80d6fe75cb48855d80e502c +current_sha256=10b24ed567ad3092ebc1f537ccd54ae812dce5923f3adb260b20540f86e0d8ea +backup_matches_fixture=true +current_matches_backup=false +? 0 +``` + +## C3: Undo restores from backup cache artifact and removes transformed path + +```console +$ repren --undo --full -i --from humpty --to dumpty cachecase +Using 1 patterns: + 'humpty' IGNORECASE -> 'dumpty' +- restore: cachecase/humpty-dumpty.txt.orig -> cachecase/humpty-dumpty.txt +Restored 1 file(s), skipped 0 with warnings +? 0 +``` + +```console +$ find cachecase -maxdepth 1 -type f | sort +cachecase/humpty-dumpty.txt +? 0 +``` + +```console +$ head -1 cachecase/humpty-dumpty.txt +Humpty Dumpty smiled contemptuously. 'Of course you don't — till I tell you. I meant "there's a nice knock-down argument for you!"' +? 0 +``` + +## C4: Timestamp guard skips unsafe restore when backup is newer than target + +```console +$ mkdir -p guard && printf 'backup\n' > guard/file.txt.orig && printf 'target\n' > guard/file.txt && touch -t 202601010101 guard/file.txt && touch -t 202601010102 guard/file.txt.orig && repren --undo --from NO --to NO guard +Using 1 patterns: + 'NO' -> 'NO' +- skip: guard/file.txt.orig: backup is newer than current file +Restored 0 file(s), skipped 1 with warnings +? 0 +``` + +```console +$ cat guard/file.txt && cat guard/file.txt.orig +target +backup +? 0 +``` diff --git a/tests/tryscript/fixtures/.gitignore b/tests/tryscript/fixtures/.gitignore new file mode 100644 index 0000000..df6bd16 --- /dev/null +++ b/tests/tryscript/fixtures/.gitignore @@ -0,0 +1,6 @@ +* +!.gitignore +!original/ +!original/** +!patterns-misc +!patterns-rotate-abc diff --git a/tests/work-dir/original/humpty-dumpty.txt b/tests/tryscript/fixtures/original/humpty-dumpty.txt similarity index 100% rename from tests/work-dir/original/humpty-dumpty.txt rename to tests/tryscript/fixtures/original/humpty-dumpty.txt diff --git a/tests/work-dir/original/stuff/trees/beech.txt b/tests/tryscript/fixtures/original/stuff/trees/beech.txt similarity index 100% rename from tests/work-dir/original/stuff/trees/beech.txt rename to tests/tryscript/fixtures/original/stuff/trees/beech.txt diff --git a/tests/work-dir/original/stuff/trees/maple.txt b/tests/tryscript/fixtures/original/stuff/trees/maple.txt similarity index 100% rename from tests/work-dir/original/stuff/trees/maple.txt rename to tests/tryscript/fixtures/original/stuff/trees/maple.txt diff --git a/tests/work-dir/original/stuff/trees/oak.txt b/tests/tryscript/fixtures/original/stuff/trees/oak.txt similarity index 100% rename from tests/work-dir/original/stuff/trees/oak.txt rename to tests/tryscript/fixtures/original/stuff/trees/oak.txt diff --git a/tests/work-dir/original/stuff/words/.hidden.txt b/tests/tryscript/fixtures/original/stuff/words/.hidden.txt similarity index 100% rename from tests/work-dir/original/stuff/words/.hidden.txt rename to tests/tryscript/fixtures/original/stuff/words/.hidden.txt diff --git a/tests/work-dir/original/stuff/words/Asia b/tests/tryscript/fixtures/original/stuff/words/Asia similarity index 100% rename from tests/work-dir/original/stuff/words/Asia rename to tests/tryscript/fixtures/original/stuff/words/Asia diff --git a/tests/work-dir/original/stuff/words/Europe b/tests/tryscript/fixtures/original/stuff/words/Europe similarity index 100% rename from tests/work-dir/original/stuff/words/Europe rename to tests/tryscript/fixtures/original/stuff/words/Europe diff --git a/tests/work-dir/original/stuff/words/Mexico b/tests/tryscript/fixtures/original/stuff/words/Mexico similarity index 100% rename from tests/work-dir/original/stuff/words/Mexico rename to tests/tryscript/fixtures/original/stuff/words/Mexico diff --git a/tests/work-dir/original/stuff/words/United b/tests/tryscript/fixtures/original/stuff/words/United similarity index 100% rename from tests/work-dir/original/stuff/words/United rename to tests/tryscript/fixtures/original/stuff/words/United diff --git a/tests/work-dir/original/stuff/words/genetic b/tests/tryscript/fixtures/original/stuff/words/genetic similarity index 100% rename from tests/work-dir/original/stuff/words/genetic rename to tests/tryscript/fixtures/original/stuff/words/genetic diff --git a/tests/work-dir/original/stuff/words/genus b/tests/tryscript/fixtures/original/stuff/words/genus similarity index 100% rename from tests/work-dir/original/stuff/words/genus rename to tests/tryscript/fixtures/original/stuff/words/genus diff --git a/tests/work-dir/original/stuff/words/oak b/tests/tryscript/fixtures/original/stuff/words/oak similarity index 100% rename from tests/work-dir/original/stuff/words/oak rename to tests/tryscript/fixtures/original/stuff/words/oak diff --git a/tests/work-dir/original/stuff/words/second b/tests/tryscript/fixtures/original/stuff/words/second similarity index 100% rename from tests/work-dir/original/stuff/words/second rename to tests/tryscript/fixtures/original/stuff/words/second diff --git a/tests/work-dir/patterns-misc b/tests/tryscript/fixtures/patterns-misc similarity index 100% rename from tests/work-dir/patterns-misc rename to tests/tryscript/fixtures/patterns-misc diff --git a/tests/work-dir/patterns-rotate-abc b/tests/tryscript/fixtures/patterns-rotate-abc similarity index 100% rename from tests/work-dir/patterns-rotate-abc rename to tests/tryscript/fixtures/patterns-rotate-abc diff --git a/tests/tryscript/help-errors.tryscript.md b/tests/tryscript/help-errors.tryscript.md new file mode 100644 index 0000000..2f9c13e --- /dev/null +++ b/tests/tryscript/help-errors.tryscript.md @@ -0,0 +1,122 @@ +--- +sandbox: true +before: | + cp -r $TRYSCRIPT_TEST_DIR/fixtures/. fixtures/ +--- + +# Help and Error Paths + +## H1: Top-level help + +```console +$ repren --help | grep -F "Powerful CLI string replacement and file renaming for agents and humans" +Powerful CLI string replacement and file renaming for agents and humans +? 0 +``` + +```console +$ repren --help | grep -Fx " --full do file renames and search/replace on file contents" + --full do file renames and search/replace on file contents +? 0 +``` + +```console +$ repren --help | grep -Fx " --renames do file renames only; do not modify file contents" + --renames do file renames only; do not modify file contents +? 0 +``` + +```console +$ repren --help | grep -Fx " -i, --insensitive match case-insensitively" + -i, --insensitive match case-insensitively +? 0 +``` + +```console +$ repren --help | grep -Fx " --format {text,json} output format: 'text' for human-readable (default), 'json' for" + --format {text,json} output format: 'text' for human-readable (default), 'json' for +? 0 +``` + +```console +$ repren --help | grep -F -- "--clean-backups" + [--backup-suffix BACKUP_SUFFIX] [--undo] [--clean-backups] + --clean-backups remove backup files (standalone mode, no patterns needed) +? 0 +``` + +```console +$ repren --help | grep -F "Run \`repren --docs\` for full docs." +Run `repren --docs` for full docs. +? 0 +``` + +## H2: Version output + +```console +$ repren --version +[VERSION] +? 0 +``` + +## H3: Docs entry point + +```console +$ repren --docs | grep -F 'Powerful CLI string replacement and file renaming for agents and humans' +Powerful CLI string replacement and file renaming for agents and humans +? 0 +``` + +## H4: Skill output + +```console +$ repren --skill | head -3 +--- +name: repren +description: Performs simultaneous multi-pattern search-and-replace, file/directory renaming, and case-preserving refactoring across codebases. Use for bulk refactoring, global find-and-replace, or when user mentions repren, multi-file rename, or pattern-based transformations. +? 0 +``` + +## E1: Missing patterns is an error + +```console +$ repren 2>&1 +usage: repren [-h] [--version] [--docs] [--from FROM_PAT] [--to TO_PAT] [-p PAT_FILE] + [--full] [--renames] [--literal] [-i] [--dotall] [--preserve-case] [-b] + [--include INCLUDE_PAT] [--exclude EXCLUDE_PAT] [--at-once] [-t] + [--walk-only] [-n] [-q] [--format {text,json}] + [--backup-suffix BACKUP_SUFFIX] [--undo] [--clean-backups] + [--install-skill] [--agent-base DIR] [--skill] + [root_paths ...] +repren: error: must specify --patterns or both --from and --to + +Run `repren --help` for usage. +Run `repren --docs` for full docs. +? 2 +``` + +## E2: Invalid backup suffix is an error + +```console +$ repren --backup-suffix bak --from a --to b fixtures/original/humpty-dumpty.txt 2>&1 +usage: repren [-h] [--version] [--docs] [--from FROM_PAT] [--to TO_PAT] [-p PAT_FILE] + [--full] [--renames] [--literal] [-i] [--dotall] [--preserve-case] [-b] + [--include INCLUDE_PAT] [--exclude EXCLUDE_PAT] [--at-once] [-t] + [--walk-only] [-n] [-q] [--format {text,json}] + [--backup-suffix BACKUP_SUFFIX] [--undo] [--clean-backups] + [--install-skill] [--agent-base DIR] [--skill] + [root_paths ...] +repren: error: --backup-suffix must start with '.' + +Run `repren --help` for usage. +Run `repren --docs` for full docs. +? 2 +``` + +## E3: `--clean-backups` treats `--undo` as a no-op + +```console +$ repren --undo --clean-backups fixtures/original 2>&1 +Removed 0 backup file(s) +? 0 +``` diff --git a/tests/tryscript/json-output.tryscript.md b/tests/tryscript/json-output.tryscript.md new file mode 100644 index 0000000..126fae9 --- /dev/null +++ b/tests/tryscript/json-output.tryscript.md @@ -0,0 +1,96 @@ +--- +sandbox: true +before: | + cp -r $TRYSCRIPT_TEST_DIR/fixtures/. fixtures/ +--- + +# JSON Output Contract + +## J1: JSON walk-only + +```console +$ cp -r fixtures/original test-json && repren --format json --walk-only test-json +{ + "operation": "walk", + "paths": [ + "test-json/humpty-dumpty.txt", + "test-json/stuff/trees/beech.txt", + "test-json/stuff/trees/maple.txt", + "test-json/stuff/trees/oak.txt", + "test-json/stuff/words/Asia", + "test-json/stuff/words/Europe", + "test-json/stuff/words/Mexico", + "test-json/stuff/words/United", + "test-json/stuff/words/genetic", + "test-json/stuff/words/genus", + "test-json/stuff/words/oak", + "test-json/stuff/words/second" + ], + "files_found": 12, + "skipped_backups": 0 +} +? 0 +``` + +## J2: JSON dry-run replacement + +```console +$ repren --format json --dry-run --from Humpty --to Dumpty test-json/humpty-dumpty.txt +{ + "operation": "replace", + "dry_run": true, + "patterns_count": 1, + "files_found": 1, + "chars_read": 513, + "matches_found": 3, + "matches_applied": 3, + "files_changed": 1, + "files_rewritten": 1, + "files_renamed": 0 +} +? 0 +``` + +## J3: JSON actual replacement + +```console +$ repren --format json --from Humpty --to Dumpty test-json/humpty-dumpty.txt +{ + "operation": "replace", + "dry_run": false, + "patterns_count": 1, + "files_found": 1, + "chars_read": 513, + "matches_found": 3, + "matches_applied": 3, + "files_changed": 1, + "files_rewritten": 1, + "files_renamed": 0 +} +? 0 +``` + +## J4: JSON undo + +```console +$ repren --format json --undo --full --from Humpty --to Dumpty test-json +{ + "operation": "undo", + "dry_run": false, + "restored": 1, + "skipped": 0 +} +? 0 +``` + +## J5: JSON clean-backups + +```console +$ repren --format json --clean-backups test-json +{ + "operation": "clean_backups", + "dry_run": false, + "removed": 0 +} +? 0 +``` diff --git a/tests/tryscript/patterns-and-case.tryscript.md b/tests/tryscript/patterns-and-case.tryscript.md new file mode 100644 index 0000000..0620216 --- /dev/null +++ b/tests/tryscript/patterns-and-case.tryscript.md @@ -0,0 +1,174 @@ +--- +sandbox: true +before: | + cp -r $TRYSCRIPT_TEST_DIR/fixtures/. fixtures/ +--- + +# Pattern Files and Case Preservation + +## P1: Pattern-file content replacements + +```console +$ cp -r fixtures/original test4 && repren -p fixtures/patterns-misc test4 +Using 5 patterns: + 'humpty' -> 'dumpty' + 'dumpty' -> 'humpty' + 'beech' -> 'BEECH' + 'Asia' -> 'Asia!' + 'Europe' -> 'Europe!' +Found 12 files in: test4 +- modify: test4/stuff/trees/beech.txt: 8 matches +- modify: test4/stuff/trees/maple.txt: 3 matches +- modify: test4/stuff/trees/oak.txt: 3 matches +Read 12 files (3810 chars), found 14 matches (0 skipped due to overlaps) +Changed 3 files (3 rewritten and 0 renamed) +? 0 +``` + +```console +$ find test4/stuff/words -maxdepth 1 -type f | sort +test4/stuff/words/.hidden.txt +test4/stuff/words/Asia +test4/stuff/words/Europe +test4/stuff/words/Mexico +test4/stuff/words/United +test4/stuff/words/genetic +test4/stuff/words/genus +test4/stuff/words/oak +test4/stuff/words/second +? 0 +``` + +```console +$ sed -n '1,2p' test4/stuff/trees/beech.txt && echo +Beech (Fagus) is a genus of deciduous trees in the family Fagaceae, native to temperate Europe!, Asia! and North America. Recent classification systems of the genus recognize ten to thirteen species in two distinct subgenera, Engleriana and Fagus.[1][2] The Engleriana subgenus is found only in East Asia!, and is notably distinct from the Fagus subgenus in that these BEECHes are low-branching trees, often made up of several major trunks with yellowish bark. Further differentiating characteristics include the whitish bloom on the underside of the leaves, the visible tertiary leaf veins, and a long, smooth cupule-peduncle. Fagus japonica, Fagus engleriana, and the species F. okamotoi, proposed by the bontanist Chung-Fu Shen in 1992, comprise this subgenus.[2] The better known Fagus subgenus BEECHes are high-branching with tall, stout trunks and smooth silver-grey bark. This group includes Fagus sylvatica, Fagus grandifolia, Fagus crenata, Fagus lucida, Fagus longipetiolata, and Fagus hayatae.[2] The classification of the Europe!an BEECH, Fagus sylvatica is complex, with a variety of different names proposed for different species and subspecies within this region (for example Fagus taurica, Fagus orientalis, and Fagus moesica[3]). Research suggests that BEECHes in Eurasia differentiated fairly late in evolutionary history, during the Miocene. The populations in this area represent a range of often overlapping morphotypes, though genetic analysis does not clearly support separate species.[4] +? 0 +``` + +## P2: Pattern-file full mode (content + renames) + +```console +$ cp -r fixtures/original test5 && repren --full -i -p fixtures/patterns-misc test5 +Using 5 patterns: + 'humpty' IGNORECASE -> 'dumpty' + 'dumpty' IGNORECASE -> 'humpty' + 'beech' IGNORECASE -> 'BEECH' + 'Asia' IGNORECASE -> 'Asia!' + 'Europe' IGNORECASE -> 'Europe!' +Found 12 files in: test5 +- modify: test5/humpty-dumpty.txt: 6 matches +- rename: test5/humpty-dumpty.txt -> test5/dumpty-humpty.txt +- modify: test5/stuff/trees/beech.txt: 10 matches +- rename: test5/stuff/trees/beech.txt -> test5/stuff/trees/BEECH.txt +- modify: test5/stuff/trees/maple.txt: 3 matches +- modify: test5/stuff/trees/oak.txt: 3 matches +- rename: test5/stuff/words/Asia -> test5/stuff/words/Asia! +- rename: test5/stuff/words/Europe -> test5/stuff/words/Europe! +Read 12 files (3810 chars), found 22 matches (0 skipped due to overlaps) +Changed 6 files (4 rewritten and 4 renamed) +? 0 +``` + +```console +$ find test5/stuff -maxdepth 2 -type f | sort +test5/stuff/trees/BEECH.txt +test5/stuff/trees/beech.txt.orig +test5/stuff/trees/maple.txt +test5/stuff/trees/maple.txt.orig +test5/stuff/trees/oak.txt +test5/stuff/trees/oak.txt.orig +test5/stuff/words/.hidden.txt +test5/stuff/words/Asia! +test5/stuff/words/Asia.orig +test5/stuff/words/Europe! +test5/stuff/words/Europe.orig +test5/stuff/words/Mexico +test5/stuff/words/United +test5/stuff/words/genetic +test5/stuff/words/genus +test5/stuff/words/oak +test5/stuff/words/second +? 0 +``` + +## P3: Preserve-case rotation over full tree + +```console +$ cp -r fixtures/original test6 && repren --full --preserve-case -p fixtures/patterns-rotate-abc test6 +Using 6 patterns: + 'A' -> 'B' + 'a' -> 'b' + 'B' -> 'C' + 'b' -> 'c' + 'C' -> 'A' + 'c' -> 'a' +Found 12 files in: test6 +- modify: test6/humpty-dumpty.txt: 40 matches +- modify: test6/stuff/trees/beech.txt: 180 matches +- rename: test6/stuff/trees/beech.txt -> test6/stuff/trees/ceeah.txt +- modify: test6/stuff/trees/maple.txt: 43 matches +- rename: test6/stuff/trees/maple.txt -> test6/stuff/trees/mbple.txt +- modify: test6/stuff/trees/oak.txt: 161 matches +- rename: test6/stuff/trees/oak.txt -> test6/stuff/trees/obk.txt +- rename: test6/stuff/words/Asia -> test6/stuff/words/Bsib +- rename: test6/stuff/words/Mexico -> test6/stuff/words/Mexiao +- rename: test6/stuff/words/genetic -> test6/stuff/words/genetia +- rename: test6/stuff/words/oak -> test6/stuff/words/obk +- rename: test6/stuff/words/second -> test6/stuff/words/seaond +Read 12 files (3810 chars), found 424 matches (0 skipped due to overlaps) +Changed 9 files (4 rewritten and 8 renamed) +? 0 +``` + +## P4: Two more rotations return to original content + +```console +$ repren --full --preserve-case -p fixtures/patterns-rotate-abc test6 && repren --full --preserve-case -p fixtures/patterns-rotate-abc test6 && find test6 -name '*.orig' -delete && diff -rq fixtures/original test6 +Using 6 patterns: + 'A' -> 'B' + 'a' -> 'b' + 'B' -> 'C' + 'b' -> 'c' + 'C' -> 'A' + 'c' -> 'a' +Skipped 9 file(s) ending in '.orig' (backup files are never processed) +Found 12 files in: test6 +- modify: test6/humpty-dumpty.txt: 40 matches +- modify: test6/stuff/trees/ceeah.txt: 180 matches +- rename: test6/stuff/trees/ceeah.txt -> test6/stuff/trees/aeebh.txt +- modify: test6/stuff/trees/mbple.txt: 43 matches +- rename: test6/stuff/trees/mbple.txt -> test6/stuff/trees/mcple.txt +- modify: test6/stuff/trees/obk.txt: 161 matches +- rename: test6/stuff/trees/obk.txt -> test6/stuff/trees/ock.txt +- rename: test6/stuff/words/Bsib -> test6/stuff/words/Csic +- rename: test6/stuff/words/Mexiao -> test6/stuff/words/Mexibo +- rename: test6/stuff/words/genetia -> test6/stuff/words/genetib +- rename: test6/stuff/words/obk -> test6/stuff/words/ock +- rename: test6/stuff/words/seaond -> test6/stuff/words/sebond +Read 12 files (3810 chars), found 424 matches (0 skipped due to overlaps) +Changed 9 files (4 rewritten and 8 renamed) +Using 6 patterns: + 'A' -> 'B' + 'a' -> 'b' + 'B' -> 'C' + 'b' -> 'c' + 'C' -> 'A' + 'c' -> 'a' +Skipped 17 file(s) ending in '.orig' (backup files are never processed) +Found 12 files in: test6 +- modify: test6/humpty-dumpty.txt: 40 matches +- modify: test6/stuff/trees/aeebh.txt: 180 matches +- rename: test6/stuff/trees/aeebh.txt -> test6/stuff/trees/beech.txt +- modify: test6/stuff/trees/mcple.txt: 43 matches +- rename: test6/stuff/trees/mcple.txt -> test6/stuff/trees/maple.txt +- modify: test6/stuff/trees/ock.txt: 161 matches +- rename: test6/stuff/trees/ock.txt -> test6/stuff/trees/oak.txt +- rename: test6/stuff/words/Csic -> test6/stuff/words/Asia +- rename: test6/stuff/words/Mexibo -> test6/stuff/words/Mexico +- rename: test6/stuff/words/genetib -> test6/stuff/words/genetic +- rename: test6/stuff/words/ock -> test6/stuff/words/oak +- rename: test6/stuff/words/sebond -> test6/stuff/words/second +Read 12 files (3810 chars), found 424 matches (0 skipped due to overlaps) +Changed 9 files (4 rewritten and 8 renamed) +? 0 +``` diff --git a/tests/tryscript/regex-wordbreaks.tryscript.md b/tests/tryscript/regex-wordbreaks.tryscript.md new file mode 100644 index 0000000..875c14a --- /dev/null +++ b/tests/tryscript/regex-wordbreaks.tryscript.md @@ -0,0 +1,54 @@ +--- +sandbox: true +before: | + cp -r $TRYSCRIPT_TEST_DIR/fixtures/. fixtures/ +--- + +# Regex, Word Breaks, and Parse-Only + +## X1: Regex capture-group replacement + +```console +$ cp -r fixtures/original test-regex && printf 'See figure 1 and figure 23 for details.\n' > test-regex/figures.txt && repren --from 'figure ([0-9]+)' --to 'Figure \1' test-regex/figures.txt +Using 1 patterns: + 'figure ([0-9]+)' -> 'Figure \1' +Found 1 files in: test-regex/figures.txt +- modify: test-regex/figures.txt: 2 matches +Read 1 files (40 chars), found 2 matches (0 skipped due to overlaps) +Changed 1 files (1 rewritten and 0 renamed) +? 0 +``` + +```console +$ cat test-regex/figures.txt +See Figure 1 and Figure 23 for details. +? 0 +``` + +## X2: Word-break mode replaces whole words only + +```console +$ mkdir -p wb && printf 'cat catalog Cat scat\n' > wb/word-breaks.txt && repren --word-breaks -i --from cat --to bat wb/word-breaks.txt +Using 1 patterns: + '\bcat\b' IGNORECASE -> 'bat' +Found 1 files in: wb/word-breaks.txt +- modify: wb/word-breaks.txt: 2 matches +Read 1 files (21 chars), found 2 matches (0 skipped due to overlaps) +Changed 1 files (1 rewritten and 0 renamed) +? 0 +``` + +```console +$ cat wb/word-breaks.txt +bat catalog bat scat +? 0 +``` + +## X3: Parse-only emits parsed pattern set + +```console +$ repren --parse-only --from 'foo([0-9]+)' --to 'bar\\1' fixtures/original/humpty-dumpty.txt +Using 1 patterns: + 'foo([0-9]+)' -> 'bar\\1' +? 0 +``` diff --git a/tests/tryscript/renames-and-full.tryscript.md b/tests/tryscript/renames-and-full.tryscript.md new file mode 100644 index 0000000..3500a8a --- /dev/null +++ b/tests/tryscript/renames-and-full.tryscript.md @@ -0,0 +1,94 @@ +--- +sandbox: true +before: | + cp -r $TRYSCRIPT_TEST_DIR/fixtures/. fixtures/ +--- + +# Rename and Full Mode + +## N1: Dry-run rename-only + +```console +$ cp -r fixtures/original test2 && repren -n --renames --from humpty --to dumpty test2 +Dry run: No files will be changed +Using 1 patterns: + 'humpty' -> 'dumpty' +Found 12 files in: test2 +- rename: test2/humpty-dumpty.txt -> test2/dumpty-dumpty.txt +Read 1 files (0 chars), found 0 matches (0 skipped due to overlaps) +Dry run: Would have changed 1 files (0 rewritten and 1 renamed) +? 0 +``` + +## N2: Rename-only mutation + +```console +$ repren --renames --from humpty --to dumpty test2 +Using 1 patterns: + 'humpty' -> 'dumpty' +Found 12 files in: test2 +- rename: test2/humpty-dumpty.txt -> test2/dumpty-dumpty.txt +Read 1 files (0 chars), found 0 matches (0 skipped due to overlaps) +Changed 1 files (0 rewritten and 1 renamed) +? 0 +``` + +```console +$ find test2 -maxdepth 2 -type f | sort +test2/dumpty-dumpty.txt +? 0 +``` + +## N3: Full mode rewrites and renames + +```console +$ cp -r fixtures/original test3 && repren --full -i --from humpty --to dumpty test3 +Using 1 patterns: + 'humpty' IGNORECASE -> 'dumpty' +Found 12 files in: test3 +- modify: test3/humpty-dumpty.txt: 3 matches +- rename: test3/humpty-dumpty.txt -> test3/dumpty-dumpty.txt +Read 12 files (3810 chars), found 3 matches (0 skipped due to overlaps) +Changed 1 files (1 rewritten and 1 renamed) +? 0 +``` + +```console +$ find test3 -maxdepth 2 -type f | sort +test3/dumpty-dumpty.txt +test3/humpty-dumpty.txt.orig +? 0 +``` + +## N4: Path-based move across directories + +```console +$ cp -r fixtures/original test-move && mkdir -p test-move/relocated && repren --renames --from 'stuff/trees' --to 'relocated' test-move +Using 1 patterns: + 'stuff/trees' -> 'relocated' +Found 12 files in: test-move +- rename: test-move/stuff/trees/beech.txt -> test-move/relocated/beech.txt +- rename: test-move/stuff/trees/maple.txt -> test-move/relocated/maple.txt +- rename: test-move/stuff/trees/oak.txt -> test-move/relocated/oak.txt +Read 3 files (0 chars), found 0 matches (0 skipped due to overlaps) +Changed 3 files (0 rewritten and 3 renamed) +? 0 +``` + +```console +$ find test-move -maxdepth 3 -type f | sort +test-move/humpty-dumpty.txt +test-move/relocated/beech.txt +test-move/relocated/maple.txt +test-move/relocated/oak.txt +test-move/stuff/words/.hidden.txt +test-move/stuff/words/Asia +test-move/stuff/words/Europe +test-move/stuff/words/Mexico +test-move/stuff/words/United +test-move/stuff/words/genetic +test-move/stuff/words/genus +test-move/stuff/words/oak +test-move/stuff/words/second +? 0 +``` diff --git a/tests/tryscript/replacements.tryscript.md b/tests/tryscript/replacements.tryscript.md new file mode 100644 index 0000000..b590bac --- /dev/null +++ b/tests/tryscript/replacements.tryscript.md @@ -0,0 +1,81 @@ +--- +sandbox: true +before: | + cp -r $TRYSCRIPT_TEST_DIR/fixtures/. fixtures/ +--- + +# Replacement Basics + +## R1: Dry run shows intended rewrite + +```console +$ cp -r fixtures/original test1 && repren -n --from Humpty --to Dumpty test1/humpty-dumpty.txt +Dry run: No files will be changed +Using 1 patterns: + 'Humpty' -> 'Dumpty' +Found 1 files in: test1/humpty-dumpty.txt +- modify: test1/humpty-dumpty.txt: 3 matches +Read 1 files (513 chars), found 3 matches (0 skipped due to overlaps) +Dry run: Would have changed 1 files (1 rewritten and 0 renamed) +? 0 +``` + +## R2: Dry run does not modify files + +```console +$ diff -rq fixtures/original test1 +``` + +## R3: Case-sensitive no-op replacement + +```console +$ repren --from humpty --to dumpty test1/humpty-dumpty.txt +Using 1 patterns: + 'humpty' -> 'dumpty' +Found 1 files in: test1/humpty-dumpty.txt +Read 1 files (513 chars), found 0 matches (0 skipped due to overlaps) +Changed 0 files (0 rewritten and 0 renamed) +? 0 +``` + +## R4: Actual replacement rewrites and creates backup + +```console +$ repren --from Humpty --to Dumpty test1/humpty-dumpty.txt +Using 1 patterns: + 'Humpty' -> 'Dumpty' +Found 1 files in: test1/humpty-dumpty.txt +- modify: test1/humpty-dumpty.txt: 3 matches +Read 1 files (513 chars), found 3 matches (0 skipped due to overlaps) +Changed 1 files (1 rewritten and 0 renamed) +? 0 +``` + +```console +$ sed -n '1,5p' test1/humpty-dumpty.txt +Dumpty Dumpty smiled contemptuously. 'Of course you don't — till I tell you. I meant "there's a nice knock-down argument for you!"' +'But "glory" doesn't mean "a nice knock-down argument",' Alice objected. +'When I use a word,' Dumpty Dumpty said, in rather a scornful tone, 'it means just what I choose it to mean — neither more nor less.' +'The question is,' said Alice, 'whether you can make words mean so many different things.' +'The question is,' said Dumpty Dumpty, 'which is to be master — that's all.' +? 0 +``` + +```console +$ test -f test1/humpty-dumpty.txt.orig && echo "backup exists" +backup exists +? 0 +``` + +## R5: Backup files are skipped on directory traversal + +```console +$ repren --from humpty --to dumpty test1 +Using 1 patterns: + 'humpty' -> 'dumpty' +Skipped 1 file(s) ending in '.orig' (backup files are never processed) +Found 12 files in: test1 +Read 12 files (3810 chars), found 0 matches (0 skipped due to overlaps) +Changed 0 files (0 rewritten and 0 renamed) +? 0 +``` diff --git a/tests/tryscript/stdin-collision-overlap-validation.tryscript.md b/tests/tryscript/stdin-collision-overlap-validation.tryscript.md new file mode 100644 index 0000000..a0223ee --- /dev/null +++ b/tests/tryscript/stdin-collision-overlap-validation.tryscript.md @@ -0,0 +1,147 @@ +--- +sandbox: true +before: | + cp -r $TRYSCRIPT_TEST_DIR/fixtures/. fixtures/ +--- + +# Stdin, Overlap, Collision, and Validation Paths + +## S1: stdin replacement works in quiet mode + +```console +$ printf 'line one\nline two\n' | repren --quiet --from line --to row +row one +row two +? 0 +``` + +## S2: stdin supports cross-line replacement with `--dotall --at-once` + +```console +$ printf 'A\n\nB\n' | repren --quiet --dotall --at-once --from 'A.*B' --to JOIN +JOIN +? 0 +``` + +## S3: stdin rejects `--renames` + +```console +$ set +e; out=$(printf 'x\n' | repren --renames --from x --to y 2>&1); rc=$?; set -e; echo "rc=$rc"; printf '%s\n' "$out" | grep -F "error: can't specify --renames on stdin; give filename arguments" +rc=2 +repren: error: can't specify --renames on stdin; give filename arguments +? 0 +``` + +## S4: stdin rejects `--dry-run` + +```console +$ set +e; out=$(printf 'x\n' | repren --dry-run --from x --to y 2>&1); rc=$?; set -e; echo "rc=$rc"; printf '%s\n' "$out" | grep -F "error: can't specify --dry-run on stdin; give filename arguments" +rc=2 +repren: error: can't specify --dry-run on stdin; give filename arguments +? 0 +``` + +## S5: stdin rejects JSON mode + +```console +$ set +e; out=$(printf 'x\n' | repren --format json --from x --to y 2>&1); rc=$?; set -e; echo "rc=$rc"; printf '%s\n' "$out" | grep -F "error: can't specify --format json on stdin; give filename arguments" +rc=2 +repren: error: can't specify --format json on stdin; give filename arguments +? 0 +``` + +## S6: overlapping matches are logged and only one replacement is applied + +```console +$ mkdir -p ov && printf 'abcdef\n' > ov/input.txt && printf 'abc\tX\nbcd\tY\n' > ov/patterns.tsv && repren --patterns ov/patterns.tsv ov/input.txt +Using 2 patterns: + 'abc' -> 'X' + 'bcd' -> 'Y' +Found 1 files in: ov/input.txt +- ov/input.txt: Skipping overlapping match 'bcd' of 'bcd' that overlaps 'abc' of 'abc' on its left +- modify: ov/input.txt: 2 matches +Read 1 files (7 chars), found 1 matches (1 skipped due to overlaps) +Changed 1 files (1 rewritten and 0 renamed) +? 0 +``` + +```console +$ cat ov/input.txt +Xdef +? 0 +``` + +## S7: rename collisions receive numeric suffixes + +```console +$ mkdir -p col && printf 'one\n' > col/foo.txt && printf 'two\n' > col/bar.txt && printf 'foo\tshared\nbar\tshared\n' > col-pat.tsv && repren --renames --patterns col-pat.tsv col +Using 2 patterns: + 'foo' -> 'shared' + 'bar' -> 'shared' +Found 2 files in: col +- rename: col/bar.txt -> col/shared.txt +- rename: col/foo.txt -> col/shared.txt +Read 2 files (0 chars), found 0 matches (0 skipped due to overlaps) +Changed 2 files (0 rewritten and 2 renamed) +? 0 +``` + +```console +$ find col -maxdepth 1 -type f | sort +col/shared.txt +col/shared.txt.1 +? 0 +``` + +```console +$ cat col/shared.txt col/shared.txt.1 | sort +one +two +? 0 +``` + +## S8: undo skips when predicted renamed target is missing + +```console +$ mkdir -p miss && printf 'orig\n' > miss/a.txt.orig && repren --undo --from a --to b miss +Using 1 patterns: + 'a' -> 'b' +- skip: miss/a.txt.orig: expected 'miss/b.txt' not found +Restored 0 file(s), skipped 1 with warnings +? 0 +``` + +## S9: validation rejects `--patterns` combined with `--from/--to` + +```console +$ set +e; out=$(repren --patterns fixtures/patterns-misc --from a --to b fixtures/original 2>&1); rc=$?; set -e; echo "rc=$rc"; printf '%s\n' "$out" | grep -F "error: cannot use both --patterns and --from/--to" +rc=2 +repren: error: cannot use both --patterns and --from/--to +? 0 +``` + +## S10: validation rejects `--insensitive` with `--preserve-case` + +```console +$ set +e; out=$(repren --insensitive --preserve-case --from a --to b fixtures/original/humpty-dumpty.txt 2>&1); rc=$?; set -e; echo "rc=$rc"; printf '%s\n' "$out" | grep -F "error: cannot use --insensitive and --preserve-case at once" +rc=2 +repren: error: cannot use --insensitive and --preserve-case at once +? 0 +``` + +## S11: validation rejects `--clean-backups` with `--from/--to` + +```console +$ set +e; out=$(repren --clean-backups --from a --to b fixtures/original 2>&1); rc=$?; set -e; echo "rc=$rc"; printf '%s\n' "$out" | grep -F "error: --clean-backups cannot be used with --patterns or --from/--to" +rc=2 +repren: error: --clean-backups cannot be used with --patterns or --from/--to +? 0 +``` + +## S12: rewrite works for file in current directory (no parent path component) + +```console +$ mkdir -p cwdcase && (cd cwdcase && printf 'abc\n' > plain.txt && repren --quiet --from a --to A plain.txt && cat plain.txt) +Abc +? 0 +``` diff --git a/tests/tryscript/walk-and-filters.tryscript.md b/tests/tryscript/walk-and-filters.tryscript.md new file mode 100644 index 0000000..7303b27 --- /dev/null +++ b/tests/tryscript/walk-and-filters.tryscript.md @@ -0,0 +1,69 @@ +--- +sandbox: true +before: | + cp -r $TRYSCRIPT_TEST_DIR/fixtures/. fixtures/ +--- + +# Directory Walk and Include/Exclude Filters + +## W1: Walk all files + +```console +$ repren --walk-only fixtures/original +Found 12 files in: fixtures/original +- fixtures/original/humpty-dumpty.txt +- fixtures/original/stuff/trees/beech.txt +- fixtures/original/stuff/trees/maple.txt +- fixtures/original/stuff/trees/oak.txt +- fixtures/original/stuff/words/Asia +- fixtures/original/stuff/words/Europe +- fixtures/original/stuff/words/Mexico +- fixtures/original/stuff/words/United +- fixtures/original/stuff/words/genetic +- fixtures/original/stuff/words/genus +- fixtures/original/stuff/words/oak +- fixtures/original/stuff/words/second +? 0 +``` + +## W2: Include text files only + +```console +$ repren --walk-only --include='.*[.]txt$' fixtures/original +Found 4 files in: fixtures/original +- fixtures/original/humpty-dumpty.txt +- fixtures/original/stuff/trees/beech.txt +- fixtures/original/stuff/trees/maple.txt +- fixtures/original/stuff/trees/oak.txt +? 0 +``` + +## W3: Exclude selected names + +```console +$ repren --walk-only --exclude='beech|maple' fixtures/original +Found 11 files in: fixtures/original +- fixtures/original/humpty-dumpty.txt +- fixtures/original/stuff/trees/oak.txt +- fixtures/original/stuff/words/.hidden.txt +- fixtures/original/stuff/words/Asia +- fixtures/original/stuff/words/Europe +- fixtures/original/stuff/words/Mexico +- fixtures/original/stuff/words/United +- fixtures/original/stuff/words/genetic +- fixtures/original/stuff/words/genus +- fixtures/original/stuff/words/oak +- fixtures/original/stuff/words/second +? 0 +``` + +## W4: Combined include and exclude + +```console +$ repren --walk-only --include='A.*|M.*|oak' --exclude='Mex.*' fixtures/original +Found 3 files in: fixtures/original +- fixtures/original/stuff/trees/oak.txt +- fixtures/original/stuff/words/Asia +- fixtures/original/stuff/words/oak +? 0 +``` diff --git a/tryscript.config.js b/tryscript.config.js new file mode 100644 index 0000000..1d35862 --- /dev/null +++ b/tryscript.config.js @@ -0,0 +1,11 @@ +export default { + env: { + NO_COLOR: "1", + LC_ALL: "C", + }, + path: ["$TRYSCRIPT_GIT_ROOT/.venv/bin"], + patterns: { + VERSION: "repren \\d+\\.\\d+\\.\\S+", + }, + timeout: 15_000, +};