This directory contains automated tests for Hug SCM using the BATS (Bash Automated Testing System) framework.
tests/
├── test_helper.bash # Common test utilities and setup functions
├── unit/ # Unit tests for individual commands
│ ├── test_status_staging.bats # Tests for s*, a*, us* commands
│ ├── test_working_dir.bats # Tests for w* commands
│ └── test_head.bats # Tests for h* commands
├── lib/ # Unit tests for library code (git-config/lib/)
│ └── test_hug-fs.bats # Tests for filesystem utilities
├── integration/ # Integration tests for workflows
│ └── test_workflows.bats # End-to-end workflow tests
└── fixtures/ # Test data and sample repositories
The project uses a self-contained test dependency system. Run once (or whenever you need an update):
make test-deps-installBy default, this installs BATS and its helper libraries into $HOME/.hug-deps.
To install dependencies in a different location, you can set the DEPS_DIR environment variable. Similarly, the vhs dependency location can be overridden with the VHS_DEPS_DIR environment variable.
DEPS_DIR=/path/to/your/deps VHS_DEPS_DIR=/path/to/your/vhs-deps make test-deps-install vhs-deps-installThe test runner (./tests/run-tests.sh) will automatically install or update dependencies if they're missing, so you can also just run make test and let it bootstrap everything.
If you prefer to install BATS system-wide:
# Install BATS core
sudo apt-get update
sudo apt-get install -y bats
# Install helper libraries
sudo apt-get install -y bats-assert bats-support bats-file
# Alternative: manual installation
sudo mkdir -p /usr/lib/bats-support /usr/lib/bats-assert /usr/lib/bats-file
git clone https://github.com/bats-core/bats-support.git /tmp/bats-support
git clone https://github.com/bats-core/bats-assert.git /tmp/bats-assert
git clone https://github.com/bats-core/bats-file.git /tmp/bats-file
sudo cp -r /tmp/bats-support/src/* /usr/lib/bats-support/
sudo cp -r /tmp/bats-assert/src/* /usr/lib/bats-assert/
sudo cp -r /tmp/bats-file/src/* /usr/lib/bats-file/brew install bats-core
brew tap kaos/shell
brew install bats-assert bats-file bats-support# Create BATS library directory
mkdir -p ~/.bats-libs
# Clone helper libraries
git clone https://github.com/bats-core/bats-core.git ~/.bats-libs/bats-core
git clone https://github.com/bats-core/bats-support.git ~/.bats-libs/bats-support
git clone https://github.com/bats-core/bats-assert.git ~/.bats-libs/bats-assert
git clone https://github.com/bats-core/bats-file.git ~/.bats-libs/bats-file
# Add BATS to PATH
export PATH="$HOME/.bats-libs/bats-core/bin:$PATH"
# Update test_helper.bash to use custom paths
# Change load paths to use $HOME/.bats-libs/...IMPORTANT: Always use make targets from the project root, NOT direct bats or ./tests/run-tests.sh invocation.
The Makefile provides the recommended interface for running all tests:
make test (ALL TESTS)
├── make test-bash (ALL BATS TESTS)
│ ├── make test-unit (tests/unit/*.bats)
│ ├── make test-integration (tests/integration/*.bats)
│ └── make test-lib (tests/lib/*.bats)
└── make test-lib-py (git-config/lib/python/tests/)
make test # ALL tests (BATS + pytest)
make test-bash # All BATS tests (unit + integration + lib)
make test-lib-py # Python library tests only (pytest)
make test-lib-py-coverage # Python tests with coverage reportmake test-unit # BATS unit tests (tests/unit/)
make test-integration # BATS integration tests (tests/integration/)
make test-lib # BATS library tests (tests/lib/)# Supports basename or full path
make test-unit TEST_FILE=test_status_staging.bats
make test-unit TEST_FILE=test_working_dir.bats
make test-unit TEST_FILE=test_head.bats
make test-lib TEST_FILE=test_hug-fs.bats
make test-integration TEST_FILE=test_workflows.bats
make test-bash TEST_FILE=test_head.bats # Also works with test-bash# BATS tests
make test-unit TEST_FILTER="hug s shows status"
make test-lib TEST_FILTER="is_symlink"
make test-bash TEST_FILTER="hug w"
# Python tests (pytest -k)
make test-lib-py TEST_FILTER="test_analyze"Default behavior: BATS tests show only failing tests by default
make test-unit # Shows only failing unit tests
make test-bash # Shows only failing BATS tests
make test-unit TEST_FILE=test_head.bats # Shows only failing tests in this fileShow all test results (including passing)
make test-unit TEST_SHOW_ALL_RESULTS=1
make test-bash TEST_SHOW_ALL_RESULTS=1
make test-unit TEST_FILE=test_head.bats TEST_SHOW_ALL_RESULTS=1make test-unit TEST_FILE=test_status_staging.bats TEST_FILTER="hug s"
make test-bash TEST_FILTER="working directory"
# Show all results with filters
make test-unit TEST_FILE=test_status_staging.bats TEST_FILTER="hug s" TEST_SHOW_ALL_RESULTS=1
make test-bash TEST_FILTER="working directory" TEST_SHOW_ALL_RESULTS=1make test-check # Verify BATS setup without running testsBefore committing, ensure tests pass ShellCheck:
make sanitize-check # Includes ShellCheck + formatting + type checkingSee TESTING.md for BATS-specific ShellCheck patterns (SC2314, SC2315).
Only use direct commands for features not exposed by the Makefile:
# From project root (after running `make test-check` once)
./tests/run-tests.sh -j 4 # Parallel BATS execution
./tests/run-tests.sh --install-deps # Install BATS dependencies
# Direct bats (if you have system-wide BATS)
bats --tap tests/ # TAP format output
bats --verbose-run tests/ # Show commands as they runFor command tests (unit/integration):
#!/usr/bin/env bats
# Tests for [feature description]
# Load test helpers
load '../test_helper'
setup() {
require_hug
TEST_REPO=$(create_test_repo_with_changes)
cd "$TEST_REPO"
}
teardown() {
cleanup_test_repo
}
@test "descriptive test name" {
# Arrange: Set up test conditions
echo "test content" > test.txt
# Act: Run the command
run hug command args
# Assert: Verify results
assert_success
assert_output --partial "expected output"
assert_file_exists "test.txt"
}For library tests (lib/):
#!/usr/bin/env bats
# Tests for [library description]
load '../test_helper'
# Load the library (use 'load' for BATS compatibility and path reliability)
load '../../../git-config/lib/hug-fs' # Adjust path for your lib
@test "descriptive test name" {
# Act: Call library function
run is_symlink "path/to/symlink"
# Assert: Verify results
assert_success
}From test_helper.bash:
Repository Creation:
Phase 2: All fixtures now use deterministic commits for reproducibility
Demo Repositories (Externally-Built, Comprehensive)
create_demo_repo_simple()- Full-featured demo repo with 9 commits, 2 branches, remote- 3 initial commits (README, app.js, .gitignore)
- 4 commits with overlapping files (for dependency testing: file1.txt, file2.txt, file3.txt, file4.txt)
- 2 commits on feature/search branch
- All commits have fixed timestamps (year 2000) for reproducible commit hashes
- Includes bare remote repository
- Perfect for testing commands that analyze commit history, dependencies, or branches
- Use when: You need a realistic repo structure with remote and branches
create_demo_repo_full()- Comprehensive demo repo with 70+ commits, 15+ branches, 4 contributors- Use when you need complex scenarios (tags, upstream tracking, WIP states, etc.)
- Much slower than simple demo repo (use sparingly)
Test Fixtures (Built In-Process, Lightweight)
create_test_repo()- Minimal git repository with 1 deterministic commit- Uses
git_commit_deterministic()for reproducible hashes - Perfect for tests that only need a clean git repo as a starting point
- Uses
create_test_repo_with_history()- Repo with 3 deterministic commits- Commits: "Initial commit", "Add feature 1", "Add feature 2"
- All use fixed timestamps starting from year 2000
- Use when: Testing HEAD manipulation, history traversal
create_test_repo_with_changes()- Repo with 1 commit + uncommitted changes- Staged file (staged.txt), unstaged changes (README.md), untracked file (untracked.txt)
- Use when: Testing staging/unstaging commands (hug a, hug us, hug sl)
create_test_repo_with_head_mixed_state()- Complex fixture for HEAD operation testing- 4 commits with overlapping edits to tracked.txt
- Staged changes, unstaged changes, untracked file, ignored file
- Use when: Testing hug h* commands with complex working tree states
create_test_repo_with_head_conflict_state()- Fixture for conflict testing- Local changes conflict with HEAD commits
- Use when: Testing git reset --keep behavior (hug h rollback)
create_test_repo_with_dated_commits()- Commits at specific dates- 5 commits at specific 2024 dates (Day 1, Day 5, Day 10, Day 15, Day 20)
- Use when: Testing temporal filtering (--since, --until)
Deterministic Timestamp System:
All fixtures now use git_commit_deterministic() from tests/lib/deterministic_git.bash:
- Fixed starting epoch: 2000-01-01 00:00:00 UTC
- Default increment: 1 hour between commits
- Ensures reproducible commit hashes across test runs
- Call
reset_fake_clock()at start of fixture for consistency
Cleanup:
cleanup_test_repo()- Cleans up test repository (works with all repo types)
Assertions (from bats-assert):
assert_success- Command exit code is 0assert_failure- Command exit code is non-zeroassert_output "text"- Output matches exactlyassert_output --partial "text"- Output contains textassert_output --regexp "pattern"- Output matches regexrefute_output "text"- Output does not contain text
File Assertions (from bats-file):
assert_file_exists "path"- File existsassert_file_not_exists "path"- File does not existassert_dir_exists "path"- Directory exists
Custom Helpers:
assert_git_clean()- Git status is clean (no changes)require_hug()- Skip test if hug not installedrequire_git_version "X.Y"- Skip test if git too old
Library Testing Notes:
- Load library files with
load "$RELATIVE_PATH"(e.g.,load '../../../git-config/lib/hug-fs'from tests/lib/). Useload(notsource) for BATS best practices—it handles paths relative to the .bats file and integrates with test isolation. - Place after loading test_helper, once per file (not inside @test blocks).
- If path issues arise, verify with
echo "$(pwd)"in a test; PROJECT_ROOT is for absolute paths if needed. - Use temporary directories for file/symlink tests:
mktemp -d - Clean up temps in each test (no global setup/teardown needed for pure lib tests)
- Test files:
test_<feature>.bats - Test descriptions:
"hug <command>: <behavior>"(commands) or"hug-fs: <function>: <behavior>"(libraries) - Be specific and descriptive
- Each test should verify one behavior
- Isolation: Each test should be independent
- Cleanup: Use
setup()andteardown()appropriately - Clear Intent: Test names should describe what they verify
- Fast Tests: Keep tests fast; avoid unnecessary operations
- Comprehensive: Test happy path, edge cases, and error conditions
- Mock When Needed: Use test repositories, not real repos
- Deterministic: Tests should always produce same results
- Prefer Demo Repos: Use
create_demo_repo_simple()instead ofcreate_test_repo*()for tests that rely on commit history or analyze dependencies- Demo repos have fixed timestamps → reproducible commit hashes
- Demo repos have realistic commit patterns → more thorough testing
- Demo repos are battle-tested → fewer surprises
- ALWAYS Use Gum Mock for Interactive Tests: Never pipe empty input to gum filter commands
- WRONG:
run bash -c "echo '' | hug bdel 2>&1" - RIGHT: Use
setup_gum_mock+export HUG_TEST_GUM_INPUT_RETURN_CODE=1+teardown_gum_mock
- WRONG:
CRITICAL: Interactive commands that use gum filter or gum choose must use the gum mock infrastructure.
# WRONG - This fails in TTY environments
run bash -c "echo '' | hug bdel 2>&1"
# Error: "unable to run filter: could not open a new TTY: open /dev/tty: no such device"Gum filter opens /dev/tty directly (not stdin), causing:
- Non-TTY (CI): "no such device or address" error
- TTY environments: Test hangs waiting for input
@test "hug bdel: interactive mode cancellation" {
# Create test branches...
git checkout -q -b feature-1
git commit --allow-empty -m "Feature 1"
git checkout -q main
# Use gum mock for all interactive tests
setup_gum_mock
export HUG_TEST_GUM_INPUT_RETURN_CODE=1 # Simulate Ctrl+C/ESC
run hug bdel
assert_success # Graceful cancellation
assert_output --partial "No branches selected."
teardown_gum_mock
}| Variable | Purpose | Example |
|---|---|---|
HUG_TEST_GUM_INPUT_RETURN_CODE |
Simulate cancellation (1) or success (0) | export HUG_TEST_GUM_INPUT_RETURN_CODE=1 |
HUG_TEST_GUM_SELECTION_INDEX |
Select Nth item from gum filter (0-indexed) | export HUG_TEST_GUM_SELECTION_INDEX=2 |
HUG_TEST_GUM_CONFIRM |
Auto-answer confirm prompts | export HUG_TEST_GUM_CONFIRM=yes |
| Scenario | Approach |
|---|---|
gum filter / gum choose menus |
ALWAYS use setup_gum_mock |
| Simple yes/no confirmations | Input piping OK: echo "y" | hug command |
gum input text prompts |
Use gum mock OR input piping |
See tests/bin/README.md for complete gum mock documentation.
You can override the dependencies directory by setting the DEPS_DIR environment variable before running tests:
DEPS_DIR=/custom/path ./tests/run-tests.shThis is useful for custom installations or CI environments with restricted paths.
Tests run automatically in GitHub Actions on every push and pull request. The workflow caches test dependencies to speed up runs.
See .github/workflows/test.yml for the CI configuration.
bats --verbose-run --print-output-on-failure tests/unit/test_status_staging.bats# Add debugging to your test
@test "my test" {
echo "Debug: variable=$variable" >&3 # stderr visible in verbose mode
run hug command
echo "Output: $output" >&3
echo "Status: $status" >&3
assert_success
}# Run test in debug mode
bats --verbose-run tests/unit/test_status_staging.bats
# Or add `set -x` to the test
@test "my test" {
set -x # Enable bash tracing
run hug command
assert_success
}# In teardown, don't cleanup for debugging
teardown() {
echo "Test repo at: $TEST_REPO" >&3
# Comment out: cleanup_test_repo
}Currently, we have tests for:
- ✅ Status and staging commands (s*, a*, us*)
- ✅ Working directory commands (w*)
- ✅ HEAD operations (h*)
- ✅ Library: filesystem utilities (hug-fs)
- ✅ Common workflows (integration tests)
To add:
- Branch operations (b*)
- Commit commands (c*)
- Logging commands (l*)
- Tagging commands (t*)
- File inspection (f*)
- Rebase and merge (r*, m*)
- Additional libraries (hug-confirm, hug-output, etc.)
When adding new commands or features:
- Write tests first (TDD) or alongside the feature
- For library code in git-config/lib/, add tests to tests/lib/
- Ensure tests pass:
bats tests/ - Add integration tests for complex workflows
- Update this README if adding new test utilities