Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[submodule "test/bats"]
path = test/bats
url = https://github.com/bats-core/bats-core.git
[submodule "test/test_helper/bats-support"]
path = test/test_helper/bats-support
url = https://github.com/bats-core/bats-support.git
[submodule "test/test_helper/bats-assert"]
path = test/test_helper/bats-assert
url = https://github.com/bats-core/bats-assert.git
13 changes: 12 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,18 @@ Thanks for your interest in contributing!

## Testing

Tests are not yet implemented. Contributions to add [bats-core](https://github.com/bats-core/bats-core) tests are welcome!
Tests use [BATS](https://github.com/bats-core/bats-core) (Bash Automated Testing System). See [`test/README.md`](test/README.md) for full details on the test architecture, how to write new tests, and tips.

```bash
# Initialize submodules (first time only)
git submodule update --init --recursive

# Run all tests
./test/bats/bin/bats test/unit/ test/integration/ test/e2e/

# Run a single test file
./test/bats/bin/bats test/integration/wt-add.bats
```

## Submitting Changes

Expand Down
9 changes: 7 additions & 2 deletions bin/wt-add
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,13 @@ install_metadata_for_worktree() {
}

# Helper: install Bazel output symlinks (bazel-out, bazel-bin, etc.) for a created worktree
# These symlinks point to Bazel's cached build artifacts in a temp directory.
# Sharing them across worktrees significantly speeds up IntelliJ project sync.
#
# Bazel stores build outputs in a central cache directory (typically under /private/var/tmp).
# The symlinks (bazel-out, bazel-bin, etc.) in the repo root point to this shared cache.
# By copying these symlinks to new worktrees, we enable:
# 1. Faster IntelliJ/IDE sync (no rebuild required)
# 2. Shared build cache across worktrees
# 3. Immediate access to compiled artifacts
install_bazel_symlinks_for_worktree() {
local worktree_path="$1"

Expand Down
26 changes: 22 additions & 4 deletions bin/wt-list
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ else
exit 1
fi

usage() {
cat <<EOF
Usage: $(basename "$0") [-v|--verbose]

List all git worktrees with status indicators.

Options:
-v, --verbose Show dirty/ahead/behind status (slower)
-h, --help Show this help message
EOF
}

# Parse arguments
VERBOSE=false
while [[ $# -gt 0 ]]; do
Expand All @@ -51,8 +63,13 @@ while [[ $# -gt 0 ]]; do
VERBOSE=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Usage: wt-list [-v|--verbose]" >&2
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
esac
Expand All @@ -67,8 +84,9 @@ if [[ ! -d "$WT_MAIN_REPO_ROOT" ]]; then
exit 1
fi

# Get absolute path of main repo
MAIN_REPO_ABS="$(cd "$WT_MAIN_REPO_ROOT" && pwd)"
# Get absolute path of main repo (use pwd -P to resolve symlinks for consistent comparison)
# Note: git worktree list stores physical paths, so we need to match that behavior
MAIN_REPO_ABS="$(cd "$WT_MAIN_REPO_ROOT" && pwd -P)"

# Get currently linked worktree (if any)
LINKED_WORKTREE="$(wt_get_linked_worktree)"
Expand All @@ -88,7 +106,7 @@ LINKED_WORKTREE="$(wt_get_linked_worktree)"
case "$line" in
worktree\ *)
wt="${line#worktree }"
wt_abs="$(cd "$wt" 2>/dev/null && pwd)" || continue
wt_abs="$(cd "$wt" 2>/dev/null && pwd -P)" || continue

# Determine prefix (linked indicator)
prefix=" "
Expand Down
16 changes: 11 additions & 5 deletions lib/wt-common
Original file line number Diff line number Diff line change
Expand Up @@ -239,21 +239,27 @@ wt_source() {

# Get the currently linked worktree path (resolves symlink)
# Usage: wt_get_linked_worktree
# Outputs: absolute path to linked worktree, or empty if none
# Outputs: absolute physical path to linked worktree, or empty if none
#
# Returns physical paths (pwd -P) to ensure consistent comparison with
# git worktree list output, which also uses physical paths. This handles
# macOS symlink quirks like /var -> /private/var.
wt_get_linked_worktree() {
local link_path="$WT_ACTIVE_WORKTREE"

# Normalize link path
# Normalize link path to physical path for consistent comparison
if [[ -d "$(dirname "$link_path")" ]]; then
link_path="$(cd "$(dirname "$link_path")" && pwd)/$(basename "$link_path")"
link_path="$(cd "$(dirname "$link_path")" && pwd -P)/$(basename "$link_path")"
fi

if [[ -L "$link_path" ]]; then
local target
target="$(readlink "$link_path")"
# Normalize relative symlinks to absolute
# Resolve to physical path whether target is relative or absolute
if [[ "$target" != /* ]]; then
target="$(cd "$(dirname "$link_path")" && cd "$(dirname "$target")" && pwd)/$(basename "$target")"
target="$(cd "$(dirname "$link_path")" && cd "$(dirname "$target")" && pwd -P)/$(basename "$target")"
else
target="$(cd "$(dirname "$target")" && pwd -P)/$(basename "$target")"
fi
echo "$target"
fi
Expand Down
212 changes: 212 additions & 0 deletions test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# Testing the Worktree Toolkit

This directory contains the full test suite for `wt`, built on [BATS](https://github.com/bats-core/bats-core) (Bash Automated Testing System).

## Test Directory Structure

```
test/
├── README.md # This file
├── bats/ # BATS core (git submodule)
├── test_helper/
│ ├── common.bash # Shared setup, teardown, helpers, custom assertions
│ ├── bats-assert/ # Assertion library (git submodule)
│ └── bats-support/ # Support library (git submodule)
├── unit/ # Library function tests (lib/wt-*)
│ ├── wt-common.bats # Output helpers, git helpers, prompt, config loading
│ ├── wt-context.bats # Context CRUD, listing, switching, config loading
│ └── wt-context-setup.bats # Path expansion, branch detection, path derivation, metadata detection
├── integration/ # Per-command script tests (bin/wt-*)
│ ├── wt-add.bats # Worktree creation (existing branch, -b, path traversal, state restore)
│ ├── wt-cd.bats # Path output, validation, spaces in paths
│ ├── wt-context.bats # Context listing, switching, add subcommand
│ ├── wt-list.bats # Worktree listing, indicators, verbose mode
│ ├── wt-metadata-export.bats # Symlink creation, deduplication, patterns
│ ├── wt-metadata-import.bats # Copy behavior, symlink resolution, overwrite
│ ├── wt-remove.bats # Removal, main repo protection, --merged, dirty handling
│ └── wt-switch.bats # Symlink switching, safety (non-symlink protection)
└── e2e/ # Full workflow tests
└── workflow.bats # Lifecycle, multi-worktree, context switching, error recovery
```

## Running Tests

### Prerequisites

- Bash 4.0+
- Git

No installation needed — BATS and its helper libraries are included as git submodules.

### Initialize submodules (first time only)

```bash
git submodule update --init --recursive
```

### Run all tests

```bash
./test/bats/bin/bats test/unit/ test/integration/ test/e2e/
```

### Run a specific test layer

```bash
./test/bats/bin/bats test/unit/ # Library functions only
./test/bats/bin/bats test/integration/ # Per-command scripts
./test/bats/bin/bats test/e2e/ # Full workflows
```

### Run a single test file

```bash
./test/bats/bin/bats test/unit/wt-common.bats
./test/bats/bin/bats test/integration/wt-add.bats
```

### Run a single test by name

```bash
./test/bats/bin/bats test/unit/wt-common.bats --filter "echoerr outputs"
```

## Test Infrastructure

### Isolation model

Every test runs in a fully isolated environment:

- `setup_test_env()` creates a temporary `$HOME` under `$BATS_TEST_TMPDIR/home`
- All `~/.wt/` state (configs, libs, bins) is copied fresh from the project root
- `$PATH` is prepended with the test `bin/` directory
- `$HOME`, `$LIB_DIR`, and `$_WT_ROOT` are overridden
- `teardown_test_env()` restores the original `$PATH`

BATS automatically cleans up `$BATS_TEST_TMPDIR` after each test. No test can affect another or leave persistent side effects.

### Test helper (`test/test_helper/common.bash`)

The shared helper provides:

**Environment setup:**
- `setup_test_env` / `teardown_test_env` — Isolated `$HOME` with `.wt/` structure
- `create_test_context <name> <repo_path>` — Create a context `.conf` file and set it as current
- `load_test_context <name>` — Source the `.conf` and export all `WT_*` variables

**Git helpers:**
- `create_mock_repo [path]` — Initialize a git repo with one commit, return normalized path
- `create_mock_repo_with_remote [path]` — Same but with a bare "remote" repo and `origin` configured
- `create_branch <repo> <name>` — Create a branch with a commit, return to `main`
- `create_worktree <repo> <path> <branch>` — Create a git worktree at normalized path
- `make_repo_dirty <repo>` — Append to `file.txt` to create uncommitted changes
- `stage_changes <repo>` — `git add -A`

**Metadata helpers:**
- `create_metadata_dirs <repo> [patterns...]` — Create `.idea`, `.ijwb`, etc. with a `config.xml`

**Custom assertions:**
- `assert_symlink_target <path> <expected>` — Verify a symlink exists and points to the expected target
- `assert_is_worktree <path>` — Verify a path is a git worktree (has `.git` file with `gitdir:`)

### Unit vs. integration vs. e2e

| Layer | What it tests | How it works |
|---|---|---|
| **Unit** (`test/unit/`) | Individual library functions from `lib/` | Sources the library directly, calls functions, checks return values and output |
| **Integration** (`test/integration/`) | Each `bin/wt-*` script end-to-end | Runs the script as a subprocess via `run "$TEST_HOME/.wt/bin/wt-add" ...`, checks exit codes, output, and filesystem state |
| **E2E** (`test/e2e/`) | Multi-step workflows across multiple commands | Chains multiple commands (add → switch → work → remove) and verifies the system stays consistent throughout |

### How scripts find their config in tests

Each `bin/wt-*` script re-sources `wt-common` on startup using its own `LIB_DIR` (derived from `SCRIPT_DIR/../lib`). Since tests copy all files to `$TEST_HOME/.wt/`, the scripts find the test copies of `lib/wt-common` and `lib/wt-context`, which then read the test context config from `$TEST_HOME/.wt/repos/<name>.conf`.

This means **updating `WT_METADATA_PATTERNS` requires editing the `.conf` file** (via `sed`), not just exporting the environment variable — the script will overwrite the export when it re-sources `wt-common`.

## Writing New Tests

### Adding a test to an existing file

Each `.bats` file is organized with section headers. Add your test to the appropriate section:

```bash
@test "wt-add handles my new edge case" {
# Arrange: set up state
create_branch "$REPO" "my-branch"

# Act: run the command
run "$TEST_HOME/.wt/bin/wt-add" "my-branch"

# Assert: check outcomes
assert_success
assert_is_worktree "$WT_WORKTREES_BASE/my-branch"
}
```

### Adding a new unit test file

For a new library (e.g., `lib/wt-foo`):

```bash
#!/usr/bin/env bats

# Unit tests for lib/wt-foo

setup() {
load '../test_helper/common'
setup_test_env

# Source the library under test (and its dependencies)
source "$TEST_HOME/.wt/lib/wt-common"
source "$TEST_HOME/.wt/lib/wt-foo"
}

teardown() {
teardown_test_env
}

@test "my_function returns expected value" {
run my_function "input"
assert_success
assert_output "expected"
}
```

### Adding a new integration test file

For a new command (e.g., `bin/wt-foo`):

```bash
#!/usr/bin/env bats

# Integration tests for bin/wt-foo

setup() {
load '../test_helper/common'
setup_test_env

# Create mock repo and context
REPO=$(create_mock_repo "$BATS_TEST_TMPDIR/repo")
create_test_context "test" "$REPO"
load_test_context "test"
}

teardown() {
teardown_test_env
}

@test "wt-foo does the thing" {
run "$TEST_HOME/.wt/bin/wt-foo" --flag arg
assert_success
assert_output --partial "expected text"
}
```

### Tips

- **Use `run --separate-stderr`** when testing commands that send UI to stderr and data to stdout (e.g., `wt-cd`). Captures `$output` (stdout) and `$stderr` separately.
- **Use `run bash -c 'echo "input" | command'`** to test interactive prompts (e.g., confirmation dialogs).
- **Edit `.conf` via `sed`** to change `WT_METADATA_PATTERNS` — environment exports are overwritten when scripts re-source `wt-common`.
- **Always check filesystem state**, not just output text. For example, after `wt-add`, verify the worktree directory exists, has a `.git` file, and is on the correct branch.
- **Use `assert_is_worktree`** instead of just `assert [ -d "$path" ]` — it validates the `.git` file contains a `gitdir:` reference, which distinguishes true worktrees from regular directories or main repos.
- **Normalize paths with `pwd -P`** when comparing against `git worktree list` output or `wt_get_linked_worktree` results.
1 change: 1 addition & 0 deletions test/bats
Submodule bats added at 5f12b3
Loading