From b6f297c11b31ef312eba7195ff394d7c68b4eebd Mon Sep 17 00:00:00 2001 From: amacsmith Date: Tue, 13 Jan 2026 22:58:22 -0500 Subject: [PATCH 1/2] Add comprehensive test automation pipeline with CI/CD - Add BATS test infrastructure with isolated test environment - test_helper.bash with HOME override for safe testing - Unit tests for apply.sh, backup.sh, restore.sh (25 tests) - Integration tests for theme workflows (10 tests) - Add GitHub Actions CI pipeline with parallel execution - Stage 1: ShellCheck, JSON validation, YAML validation (parallel) - Stage 2: Unit tests via matrix strategy (3 parallel jobs) - Stage 3: Integration tests (sequential after unit) - Stage 4: Theme validation - BATS caching for faster CI runs - Add local development tooling - scripts/lint.sh for shellcheck with severity filtering - scripts/test.sh orchestrator with --parallel, --fast options - Makefile for convenient development commands - Update scripts for testability - Add THEMER_DIR env var override for custom installs - Add THEMER_BACKUP_DIR env var for test isolation Co-Authored-By: Claude Opus 4.5 --- .github/workflows/CLAUDE.md | 11 ++ .github/workflows/ci.yml | 206 ++++++++++++++++++++++++++ .shellcheckrc | 18 +++ Makefile | 126 ++++++++++++++++ scripts/apply.sh | 3 +- scripts/backup.sh | 6 +- scripts/lint.sh | 61 ++++++++ scripts/restore.sh | 4 +- scripts/test.sh | 192 ++++++++++++++++++++++++ tests/CLAUDE.md | 11 ++ tests/integration/CLAUDE.md | 11 ++ tests/integration/theme_workflow.bats | 149 +++++++++++++++++++ tests/test_helper.bash | 106 +++++++++++++ tests/unit/CLAUDE.md | 11 ++ tests/unit/apply.bats | 119 +++++++++++++++ tests/unit/backup.bats | 86 +++++++++++ tests/unit/restore.bats | 82 ++++++++++ 17 files changed, 1198 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/CLAUDE.md create mode 100644 .github/workflows/ci.yml create mode 100644 .shellcheckrc create mode 100644 Makefile create mode 100755 scripts/lint.sh create mode 100755 scripts/test.sh create mode 100644 tests/CLAUDE.md create mode 100644 tests/integration/CLAUDE.md create mode 100644 tests/integration/theme_workflow.bats create mode 100644 tests/test_helper.bash create mode 100644 tests/unit/CLAUDE.md create mode 100644 tests/unit/apply.bats create mode 100644 tests/unit/backup.bats create mode 100644 tests/unit/restore.bats diff --git a/.github/workflows/CLAUDE.md b/.github/workflows/CLAUDE.md new file mode 100644 index 0000000..ed1ea43 --- /dev/null +++ b/.github/workflows/CLAUDE.md @@ -0,0 +1,11 @@ + +# Recent Activity + + + +### Jan 13, 2026 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #167 | 10:49 PM | 🟣 | Test Automation Orchestrator for Themer-Up | ~817 | + \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d3b8752 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,206 @@ +name: CI Pipeline + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +# Cancel in-progress runs for the same branch +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + BATS_VERSION: "1.10.0" + +jobs: + # ============================================ + # Stage 1: Static Analysis (runs in parallel) + # ============================================ + + shellcheck: + name: ShellCheck Linting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@master + with: + scandir: './scripts' + format: gcc + severity: warning + + validate-json: + name: Validate JSON Configs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate theme JSON files + run: | + echo "Validating JSON files..." + for file in themes/*/iterm2.json themes/*/vscode.json themes/*/theme.json; do + if [[ -f "$file" ]]; then + echo "Checking $file" + python3 -m json.tool "$file" > /dev/null || exit 1 + fi + done + echo "All JSON files valid!" + + validate-yaml: + name: Validate YAML Configs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install yamllint + run: pip install yamllint + + - name: Validate YAML files + run: | + for file in themes/*/*.yaml; do + if [[ -f "$file" ]]; then + echo "Checking $file" + yamllint -d relaxed "$file" || exit 1 + fi + done + echo "All YAML files valid!" + + # ============================================ + # Stage 2: Unit Tests (parallel by script) + # ============================================ + + unit-tests: + name: Unit Tests + runs-on: macos-latest + needs: [shellcheck] # Only run if linting passes + strategy: + fail-fast: false + matrix: + test-file: + - backup + - restore + - apply + steps: + - uses: actions/checkout@v4 + + - name: Cache BATS installation + uses: actions/cache@v4 + id: bats-cache + with: + path: ~/.local/bats + key: bats-${{ env.BATS_VERSION }}-macos + + - name: Install BATS + if: steps.bats-cache.outputs.cache-hit != 'true' + run: | + git clone https://github.com/bats-core/bats-core.git + cd bats-core + ./install.sh ~/.local/bats + + - name: Add BATS to PATH + run: echo "$HOME/.local/bats/bin" >> "$GITHUB_PATH" + + - name: Run unit tests + env: + TEST_FILE: ${{ matrix.test-file }} + run: | + bats "tests/unit/${TEST_FILE}.bats" --tap + + # ============================================ + # Stage 3: Integration Tests (after unit tests) + # ============================================ + + integration-tests: + name: Integration Tests + runs-on: macos-latest + needs: [unit-tests] # Only run if unit tests pass + steps: + - uses: actions/checkout@v4 + + - name: Cache BATS installation + uses: actions/cache@v4 + id: bats-cache + with: + path: ~/.local/bats + key: bats-${{ env.BATS_VERSION }}-macos + + - name: Install BATS + if: steps.bats-cache.outputs.cache-hit != 'true' + run: | + git clone https://github.com/bats-core/bats-core.git + cd bats-core + ./install.sh ~/.local/bats + + - name: Add BATS to PATH + run: echo "$HOME/.local/bats/bin" >> "$GITHUB_PATH" + + - name: Run integration tests + run: | + bats tests/integration/*.bats --tap + + # ============================================ + # Stage 4: Theme Validation (parallel check) + # ============================================ + + theme-validation: + name: Validate Themes + runs-on: ubuntu-latest + needs: [validate-json, validate-yaml] + steps: + - uses: actions/checkout@v4 + + - name: Check theme completeness + run: | + REQUIRED_FILES=("iterm2.json" "p10k.zsh" "mprocs.yaml" "theme.json") + + for theme_dir in themes/*/; do + theme_name=$(basename "$theme_dir") + echo "Validating theme: $theme_name" + + for file in "${REQUIRED_FILES[@]}"; do + if [[ ! -f "${theme_dir}${file}" ]]; then + echo "ERROR: Missing $file in $theme_name" + exit 1 + fi + done + + echo " All required files present" + done + + # ============================================ + # Final: Summary Job + # ============================================ + + ci-success: + name: CI Success + runs-on: ubuntu-latest + needs: [shellcheck, validate-json, validate-yaml, unit-tests, integration-tests, theme-validation] + if: always() + steps: + - name: Check all jobs passed + env: + SHELLCHECK_RESULT: ${{ needs.shellcheck.result }} + JSON_RESULT: ${{ needs.validate-json.result }} + YAML_RESULT: ${{ needs.validate-yaml.result }} + UNIT_RESULT: ${{ needs.unit-tests.result }} + INTEGRATION_RESULT: ${{ needs.integration-tests.result }} + THEME_RESULT: ${{ needs.theme-validation.result }} + run: | + if [[ "$SHELLCHECK_RESULT" != "success" ]] || \ + [[ "$JSON_RESULT" != "success" ]] || \ + [[ "$YAML_RESULT" != "success" ]] || \ + [[ "$UNIT_RESULT" != "success" ]] || \ + [[ "$INTEGRATION_RESULT" != "success" ]] || \ + [[ "$THEME_RESULT" != "success" ]]; then + echo "One or more jobs failed" + exit 1 + fi + echo "All CI checks passed!" diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..038a813 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,18 @@ +# ShellCheck configuration for themer-up +# https://www.shellcheck.net/wiki/ + +# Set severity to warning (ignore style/info suggestions) +# Style issues (SC2250, SC2292) are valid suggestions but not errors +severity=warning + +# Exclude warnings that don't apply to our use case +# SC2312: Consider invoking this command separately (we handle exit codes) +# SC2012: Use find instead of ls (our use case is controlled) +# SC2129: Consider using grouped redirects (readability preference) +exclude=SC2312,SC2012,SC2129 + +# Set default shell dialect +shell=bash + +# External sources (for sourced files) +external-sources=true diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b1f1964 --- /dev/null +++ b/Makefile @@ -0,0 +1,126 @@ +# Themer-Up Makefile +# Orchestrates testing, linting, and development tasks + +.PHONY: all test test-unit test-integration lint clean install-deps help + +# Default target +all: lint test + +# ───────────────────────────────────────────────────── +# Testing Targets +# ───────────────────────────────────────────────────── + +## Run all tests (linting + unit + integration) +test: lint test-unit test-integration + +## Run unit tests only (fast feedback) +test-unit: + @echo "Running unit tests..." + @bats tests/unit/*.bats --tap + +## Run integration tests only +test-integration: + @echo "Running integration tests..." + @bats tests/integration/*.bats --tap + +## Run tests in parallel (requires GNU parallel) +test-parallel: + @./scripts/test.sh --parallel + +## Run tests with verbose output +test-verbose: + @./scripts/test.sh --verbose + +## Run fast tests only (skip integration) +test-fast: + @./scripts/test.sh --fast + +# ───────────────────────────────────────────────────── +# Linting Targets +# ───────────────────────────────────────────────────── + +## Run shellcheck on all scripts +lint: + @./scripts/lint.sh + +## Validate all JSON config files +lint-json: + @echo "Validating JSON files..." + @for f in themes/*/iterm2.json themes/*/vscode.json themes/*/theme.json; do \ + if [ -f "$$f" ]; then python3 -m json.tool "$$f" > /dev/null && echo " ✓ $$f"; fi; \ + done + +## Validate all YAML config files +lint-yaml: + @echo "Validating YAML files..." + @for f in themes/*/*.yaml; do \ + if [ -f "$$f" ]; then yamllint -d relaxed "$$f" && echo " ✓ $$f"; fi; \ + done + +# ───────────────────────────────────────────────────── +# Development Targets +# ───────────────────────────────────────────────────── + +## Install development dependencies +install-deps: + @echo "Installing dependencies..." + @which bats > /dev/null || brew install bats-core + @which shellcheck > /dev/null || brew install shellcheck + @which yamllint > /dev/null || pip install yamllint + @echo "Done!" + +## Apply synthwave theme (default) +apply: + @./scripts/apply.sh synthwave + +## Create a backup of current configs +backup: + @./scripts/backup.sh + +## Restore from latest backup +restore: + @./scripts/restore.sh + +## Clean temporary test files +clean: + @echo "Cleaning temporary files..." + @rm -rf /tmp/themer-test-* + @rm -f /tmp/bats_output + @echo "Done!" + +# ───────────────────────────────────────────────────── +# CI Simulation +# ───────────────────────────────────────────────────── + +## Simulate full CI pipeline locally +ci: lint lint-json test-unit test-integration + @echo "" + @echo "✅ CI simulation passed!" + +# ───────────────────────────────────────────────────── +# Help +# ───────────────────────────────────────────────────── + +## Show this help message +help: + @echo "Themer-Up Development Commands" + @echo "══════════════════════════════" + @echo "" + @grep -E '^##' Makefile | sed 's/## / /' + @echo "" + @echo "Usage: make " + @echo "" + @echo "Testing:" + @echo " make test - Run all tests" + @echo " make test-unit - Run unit tests only" + @echo " make test-fast - Skip integration tests" + @echo " make test-parallel - Run tests in parallel" + @echo "" + @echo "Linting:" + @echo " make lint - Run shellcheck" + @echo " make lint-json - Validate JSON files" + @echo "" + @echo "Development:" + @echo " make install-deps - Install dev dependencies" + @echo " make apply - Apply synthwave theme" + @echo " make ci - Simulate CI locally" diff --git a/scripts/apply.sh b/scripts/apply.sh index 3516368..ba8b732 100755 --- a/scripts/apply.sh +++ b/scripts/apply.sh @@ -3,7 +3,8 @@ set -e -THEMER_DIR="$HOME/dev/themer-up" +# Allow override via environment variable for testing and custom installs +THEMER_DIR="${THEMER_DIR:-$HOME/dev/themer-up}" THEME_NAME="${1:-synthwave}" THEME_DIR="$THEMER_DIR/themes/$THEME_NAME" diff --git a/scripts/backup.sh b/scripts/backup.sh index 6907fc3..b262ea8 100755 --- a/scripts/backup.sh +++ b/scripts/backup.sh @@ -1,7 +1,9 @@ #!/bin/bash # Backup current theme configs before applying new theme -BACKUP_DIR="$HOME/.themer-up-backup/$(date +%Y%m%d_%H%M%S)" +# Support custom backup location via environment variable +BACKUP_BASE="${THEMER_BACKUP_DIR:-$HOME/.themer-up-backup}" +BACKUP_DIR="${BACKUP_BASE}/$(date +%Y%m%d_%H%M%S)" mkdir -p "$BACKUP_DIR" echo "Backing up to: $BACKUP_DIR" @@ -43,6 +45,6 @@ if [ -f "$HOME/.tmux.conf" ]; then fi # Keep only last 5 backups -ls -dt "$HOME/.themer-up-backup"/*/ 2>/dev/null | tail -n +6 | xargs rm -rf 2>/dev/null || true +ls -dt "${BACKUP_BASE}"/*/ 2>/dev/null | tail -n +6 | xargs rm -rf 2>/dev/null || true echo "Backup complete." diff --git a/scripts/lint.sh b/scripts/lint.sh new file mode 100755 index 0000000..0511f60 --- /dev/null +++ b/scripts/lint.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# Run shellcheck on all shell scripts + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +# Colors +GREEN='\033[0;32m' +RED='\033[0;31m' +CYAN='\033[0;36m' +NC='\033[0m' + +echo -e "${CYAN}Running ShellCheck on themer-up scripts...${NC}" +echo "" + +# Find all shell scripts +scripts=( + "$PROJECT_DIR/scripts/apply.sh" + "$PROJECT_DIR/scripts/backup.sh" + "$PROJECT_DIR/scripts/restore.sh" +) + +# Also check test files +test_files=() +while IFS= read -r -d '' file; do + test_files+=("$file") +done < <(find "$PROJECT_DIR/tests" -name "*.bash" -print0 2>/dev/null) + +all_files=("${scripts[@]}" "${test_files[@]}") + +failed=0 +passed=0 + +for file in "${all_files[@]}"; do + if [[ -f "$file" ]]; then + echo -n "Checking $(basename "$file")... " + # Use -S warning to only fail on warnings/errors, not style/info + if shellcheck -S warning "$file" 2>/dev/null; then + echo -e "${GREEN}OK${NC}" + ((passed++)) + else + echo -e "${RED}FAILED${NC}" + ((failed++)) + # Show details + shellcheck -S warning "$file" || true + fi + fi +done + +echo "" +echo "────────────────────────────" +echo -e "Passed: ${GREEN}${passed}${NC}" +echo -e "Failed: ${RED}${failed}${NC}" + +if [[ $failed -gt 0 ]]; then + exit 1 +fi + +echo -e "${GREEN}All checks passed!${NC}" diff --git a/scripts/restore.sh b/scripts/restore.sh index caee899..3e9e882 100755 --- a/scripts/restore.sh +++ b/scripts/restore.sh @@ -1,7 +1,9 @@ #!/bin/bash # Restore from the most recent backup -BACKUP_DIR=$(ls -dt "$HOME/.themer-up-backup"/*/ 2>/dev/null | head -1) +# Support custom backup location via environment variable +BACKUP_BASE="${THEMER_BACKUP_DIR:-$HOME/.themer-up-backup}" +BACKUP_DIR=$(ls -dt "${BACKUP_BASE}"/*/ 2>/dev/null | head -1) if [ -z "$BACKUP_DIR" ]; then echo "No backups found." diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..d3814f7 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,192 @@ +#!/bin/bash +# Test orchestration script - runs tests with intelligent execution + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +TESTS_DIR="$PROJECT_DIR/tests" + +# Colors +GREEN='\033[0;32m' +RED='\033[0;31m' +CYAN='\033[0;36m' +YELLOW='\033[0;33m' +NC='\033[0m' + +# Parse arguments +PARALLEL=false +COVERAGE=false +FAST=false +VERBOSE=false + +while [[ $# -gt 0 ]]; do + case $1 in + -p|--parallel) + PARALLEL=true + shift + ;; + -c|--coverage) + COVERAGE=true + shift + ;; + -f|--fast) + FAST=true + shift + ;; + -v|--verbose) + VERBOSE=true + shift + ;; + -h|--help) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " -p, --parallel Run unit tests in parallel" + echo " -f, --fast Skip integration tests (unit tests only)" + echo " -v, --verbose Verbose output" + echo " -h, --help Show this help message" + exit 0 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +# Check for BATS +if ! command -v bats &> /dev/null; then + echo -e "${RED}Error: BATS not found${NC}" + echo "Install with: brew install bats-core" + exit 1 +fi + +echo -e "${CYAN}╔════════════════════════════════════════╗${NC}" +echo -e "${CYAN}║ Themer-Up Test Orchestrator ║${NC}" +echo -e "${CYAN}╚════════════════════════════════════════╝${NC}" +echo "" + +# Track timing +START_TIME=$(date +%s) + +# ───────────────────────────────────────────────────── +# Stage 1: Static Analysis +# ───────────────────────────────────────────────────── +echo -e "${CYAN}Stage 1: Static Analysis${NC}" +echo "────────────────────────────────────────" + +if command -v shellcheck &> /dev/null; then + echo -n " ShellCheck... " + if "$SCRIPT_DIR/lint.sh" > /dev/null 2>&1; then + echo -e "${GREEN}PASS${NC}" + else + echo -e "${RED}FAIL${NC}" + if $VERBOSE; then + "$SCRIPT_DIR/lint.sh" + fi + exit 1 + fi +else + echo -e " ShellCheck... ${YELLOW}SKIPPED${NC} (not installed)" +fi + +# Validate JSON +echo -n " JSON validation... " +json_errors=0 +for file in "$PROJECT_DIR"/themes/*/iterm2.json "$PROJECT_DIR"/themes/*/vscode.json "$PROJECT_DIR"/themes/*/theme.json; do + if [[ -f "$file" ]]; then + if ! python3 -m json.tool "$file" > /dev/null 2>&1; then + ((json_errors++)) + fi + fi +done +if [[ $json_errors -eq 0 ]]; then + echo -e "${GREEN}PASS${NC}" +else + echo -e "${RED}FAIL${NC} ($json_errors files)" + exit 1 +fi + +echo "" + +# ───────────────────────────────────────────────────── +# Stage 2: Unit Tests +# ───────────────────────────────────────────────────── +echo -e "${CYAN}Stage 2: Unit Tests${NC}" +echo "────────────────────────────────────────" + +BATS_OPTS="" +if $VERBOSE; then + BATS_OPTS="--verbose-run" +fi + +unit_tests=( + "$TESTS_DIR/unit/backup.bats" + "$TESTS_DIR/unit/restore.bats" + "$TESTS_DIR/unit/apply.bats" +) + +if $PARALLEL && command -v parallel &> /dev/null; then + echo " Running in parallel mode..." + printf '%s\n' "${unit_tests[@]}" | parallel -j 3 "bats {} --tap" 2>&1 | while read -r line; do + echo " $line" + done +else + for test_file in "${unit_tests[@]}"; do + if [[ -f "$test_file" ]]; then + test_name=$(basename "$test_file" .bats) + echo -n " $test_name... " + if bats "$test_file" --tap > /tmp/bats_output 2>&1; then + passed=$(grep -c "^ok" /tmp/bats_output || echo 0) + echo -e "${GREEN}PASS${NC} ($passed tests)" + else + failed=$(grep -c "^not ok" /tmp/bats_output || echo 0) + echo -e "${RED}FAIL${NC} ($failed failed)" + if $VERBOSE; then + cat /tmp/bats_output + fi + exit 1 + fi + fi + done +fi + +echo "" + +# ───────────────────────────────────────────────────── +# Stage 3: Integration Tests +# ───────────────────────────────────────────────────── +if ! $FAST; then + echo -e "${CYAN}Stage 3: Integration Tests${NC}" + echo "────────────────────────────────────────" + + if [[ -f "$TESTS_DIR/integration/theme_workflow.bats" ]]; then + echo -n " theme_workflow... " + if bats "$TESTS_DIR/integration/theme_workflow.bats" --tap > /tmp/bats_output 2>&1; then + passed=$(grep -c "^ok" /tmp/bats_output || echo 0) + echo -e "${GREEN}PASS${NC} ($passed tests)" + else + failed=$(grep -c "^not ok" /tmp/bats_output || echo 0) + echo -e "${RED}FAIL${NC} ($failed failed)" + if $VERBOSE; then + cat /tmp/bats_output + fi + exit 1 + fi + fi + + echo "" +fi + +# ───────────────────────────────────────────────────── +# Summary +# ───────────────────────────────────────────────────── +END_TIME=$(date +%s) +DURATION=$((END_TIME - START_TIME)) + +echo -e "${GREEN}╔════════════════════════════════════════╗${NC}" +echo -e "${GREEN}║ All Tests Passed! ║${NC}" +echo -e "${GREEN}╚════════════════════════════════════════╝${NC}" +echo "" +echo "Duration: ${DURATION}s" diff --git a/tests/CLAUDE.md b/tests/CLAUDE.md new file mode 100644 index 0000000..ed1ea43 --- /dev/null +++ b/tests/CLAUDE.md @@ -0,0 +1,11 @@ + +# Recent Activity + + + +### Jan 13, 2026 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #167 | 10:49 PM | 🟣 | Test Automation Orchestrator for Themer-Up | ~817 | + \ No newline at end of file diff --git a/tests/integration/CLAUDE.md b/tests/integration/CLAUDE.md new file mode 100644 index 0000000..ed1ea43 --- /dev/null +++ b/tests/integration/CLAUDE.md @@ -0,0 +1,11 @@ + +# Recent Activity + + + +### Jan 13, 2026 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #167 | 10:49 PM | 🟣 | Test Automation Orchestrator for Themer-Up | ~817 | + \ No newline at end of file diff --git a/tests/integration/theme_workflow.bats b/tests/integration/theme_workflow.bats new file mode 100644 index 0000000..8144b5d --- /dev/null +++ b/tests/integration/theme_workflow.bats @@ -0,0 +1,149 @@ +#!/usr/bin/env bats +# Integration tests for complete theme application workflow + +load '../test_helper' + +setup() { + setup_test_home + export THEMER_DIR="${BATS_TEST_DIRNAME}/../.." + + # Create realistic initial state + echo '{"Name": "Default", "Guid": "default-profile"}' > "$TEST_HOME/Library/Application Support/iTerm2/DynamicProfiles/themer-up.json" + echo "# Original zshrc content" > "$TEST_HOME/.zshrc" + echo '" Original vimrc' > "$TEST_HOME/.vimrc" +} + +teardown() { + teardown_test_home +} + +# --- Full Workflow Tests --- + +@test "workflow: apply -> backup -> apply different -> restore returns to first" { + # Step 1: Apply synthwave + run "$SCRIPTS_DIR/apply.sh" "synthwave" + [ "$status" -eq 0 ] + + # Capture what was applied + original_iterm=$(cat "$TEST_HOME/Library/Application Support/iTerm2/DynamicProfiles/themer-up.json") + + # Step 2: Manually modify to simulate "different theme state" + echo '{"modified": true}' > "$TEST_HOME/Library/Application Support/iTerm2/DynamicProfiles/themer-up.json" + + # Step 3: Backup current state + run "$SCRIPTS_DIR/backup.sh" + [ "$status" -eq 0 ] + + # Step 4: Apply synthwave again (simulating re-apply) + run "$SCRIPTS_DIR/apply.sh" "synthwave" + [ "$status" -eq 0 ] + + # Step 5: Restore should bring back the modified state + run "$SCRIPTS_DIR/restore.sh" + [ "$status" -eq 0 ] + + restored=$(cat "$TEST_HOME/Library/Application Support/iTerm2/DynamicProfiles/themer-up.json") + [[ "$restored" == *"modified"* ]] +} + +@test "workflow: multiple applies don't duplicate zshrc entries" { + run "$SCRIPTS_DIR/apply.sh" "synthwave" + [ "$status" -eq 0 ] + + run "$SCRIPTS_DIR/apply.sh" "synthwave" + [ "$status" -eq 0 ] + + run "$SCRIPTS_DIR/apply.sh" "synthwave" + [ "$status" -eq 0 ] + + # Should only have one source line for p10k-themer + count=$(grep -c "p10k-themer.zsh" "$TEST_HOME/.zshrc" || echo 0) + [ "$count" -eq 1 ] +} + +@test "workflow: backup rotation preserves recent backups" { + # Create initial state and apply + run "$SCRIPTS_DIR/apply.sh" "synthwave" + [ "$status" -eq 0 ] + + # Run backup multiple times + for i in {1..7}; do + sleep 0.2 # Ensure unique timestamps + run "$SCRIPTS_DIR/backup.sh" + [ "$status" -eq 0 ] + done + + # Should have at most 5 backups + backup_count=$(find "$TEST_BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ') + [ "$backup_count" -le 5 ] +} + +# --- Theme Validation Tests --- + +@test "theme: synthwave contains all required config files" { + theme_dir="${THEMER_DIR}/themes/synthwave" + + assert_file_exists "${theme_dir}/iterm2.json" + assert_file_exists "${theme_dir}/p10k.zsh" + assert_file_exists "${theme_dir}/mprocs.yaml" + assert_file_exists "${theme_dir}/vscode.json" + assert_file_exists "${theme_dir}/theme.json" + assert_file_exists "${theme_dir}/vim.vim" + assert_file_exists "${theme_dir}/tmux.conf" +} + +@test "theme: iterm2.json is valid JSON" { + theme_dir="${THEMER_DIR}/themes/synthwave" + + run cat "${theme_dir}/iterm2.json" + [ "$status" -eq 0 ] + + # Validate JSON + run bash -c "cat '${theme_dir}/iterm2.json' | python3 -m json.tool > /dev/null 2>&1" + [ "$status" -eq 0 ] +} + +@test "theme: vscode.json is valid JSON" { + theme_dir="${THEMER_DIR}/themes/synthwave" + + run bash -c "cat '${theme_dir}/vscode.json' | python3 -m json.tool > /dev/null 2>&1" + [ "$status" -eq 0 ] +} + +@test "theme: theme.json contains required color definitions" { + theme_dir="${THEMER_DIR}/themes/synthwave" + + assert_file_contains "${theme_dir}/theme.json" "background" + assert_file_contains "${theme_dir}/theme.json" "foreground" + assert_file_contains "${theme_dir}/theme.json" "accent1" +} + +# --- Error Handling Tests --- + +@test "error: apply fails gracefully with missing theme directory" { + run "$SCRIPTS_DIR/apply.sh" "this-theme-does-not-exist" + [ "$status" -eq 1 ] + [[ "$output" == *"not found"* ]] +} + +@test "error: restore fails gracefully with no backups" { + rm -rf "$TEST_BACKUP_DIR"/* + + run "$SCRIPTS_DIR/restore.sh" + [ "$status" -eq 1 ] + [[ "$output" == *"No backups"* ]] +} + +# --- Cross-Tool Consistency Tests --- + +@test "consistency: applied theme creates coherent environment" { + run "$SCRIPTS_DIR/apply.sh" "synthwave" + [ "$status" -eq 0 ] + + # All expected files should exist after apply + assert_file_exists "$TEST_HOME/Library/Application Support/iTerm2/DynamicProfiles/themer-up.json" + assert_file_exists "$TEST_HOME/.p10k-themer.zsh" + assert_file_exists "$TEST_HOME/.config/mprocs/claude.yaml" + assert_file_exists "$TEST_HOME/.vim/colors/synthwave.vim" + assert_file_exists "$TEST_HOME/.config/nvim/colors/synthwave.vim" +} diff --git a/tests/test_helper.bash b/tests/test_helper.bash new file mode 100644 index 0000000..2a6e00d --- /dev/null +++ b/tests/test_helper.bash @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# BATS test helper - shared setup and utilities for all tests + +# Project paths - resolve from this helper file's location +_HELPER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +THEMER_DIR="$(cd "${_HELPER_DIR}/.." && pwd)" +export THEMER_DIR +export SCRIPTS_DIR="${THEMER_DIR}/scripts" +export THEMES_DIR="${THEMER_DIR}/themes" + +# Test fixture paths (isolated from real user configs) +export TEST_HOME="${BATS_TMPDIR}/themer-test-home" +export TEST_BACKUP_DIR="${TEST_HOME}/.themer-up-backup" + +# Override HOME for isolated testing +setup_test_home() { + export ORIGINAL_HOME="$HOME" + export HOME="$TEST_HOME" + + # Export THEMER_DIR so scripts can find theme files + export THEMER_DIR + + # Export backup dir override so scripts use test location + export THEMER_BACKUP_DIR="$TEST_BACKUP_DIR" + + # Create mock directory structure + mkdir -p "$TEST_HOME/Library/Application Support/iTerm2/DynamicProfiles" + mkdir -p "$TEST_HOME/Library/Application Support/Code/User" + mkdir -p "$TEST_HOME/.config/mprocs" + mkdir -p "$TEST_HOME/.config/nvim/colors" + mkdir -p "$TEST_HOME/.vim/colors" + mkdir -p "$TEST_HOME/.themer-up-backup" + + # Create minimal .zshrc and .vimrc + touch "$TEST_HOME/.zshrc" + touch "$TEST_HOME/.vimrc" +} + +teardown_test_home() { + export HOME="$ORIGINAL_HOME" + unset THEMER_BACKUP_DIR + rm -rf "$TEST_HOME" +} + +# Assertions +assert_file_exists() { + local file="$1" + if [[ ! -f "$file" ]]; then + echo "Expected file to exist: $file" >&2 + return 1 + fi +} + +assert_file_not_exists() { + local file="$1" + if [[ -f "$file" ]]; then + echo "Expected file NOT to exist: $file" >&2 + return 1 + fi +} + +assert_file_contains() { + local file="$1" + local pattern="$2" + if ! grep -q "$pattern" "$file" 2>/dev/null; then + echo "Expected file $file to contain: $pattern" >&2 + return 1 + fi +} + +assert_dir_exists() { + local dir="$1" + if [[ ! -d "$dir" ]]; then + echo "Expected directory to exist: $dir" >&2 + return 1 + fi +} + +# Theme validation helpers +theme_file_count() { + local theme="$1" + find "${THEMES_DIR}/${theme}" -type f | wc -l | tr -d ' ' +} + +validate_theme_structure() { + local theme="$1" + local theme_dir="${THEMES_DIR}/${theme}" + + [[ -f "${theme_dir}/theme.json" ]] || return 1 + [[ -f "${theme_dir}/iterm2.json" ]] || return 1 +} + +# Mock external commands for isolation +create_mock_commands() { + local mock_bin="${TEST_HOME}/bin" + mkdir -p "$mock_bin" + export PATH="${mock_bin}:$PATH" + + # Mock code command + cat > "${mock_bin}/code" << 'EOF' +#!/bin/bash +echo "Mock: code $@" +exit 0 +EOF + chmod +x "${mock_bin}/code" +} diff --git a/tests/unit/CLAUDE.md b/tests/unit/CLAUDE.md new file mode 100644 index 0000000..ed1ea43 --- /dev/null +++ b/tests/unit/CLAUDE.md @@ -0,0 +1,11 @@ + +# Recent Activity + + + +### Jan 13, 2026 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #167 | 10:49 PM | 🟣 | Test Automation Orchestrator for Themer-Up | ~817 | + \ No newline at end of file diff --git a/tests/unit/apply.bats b/tests/unit/apply.bats new file mode 100644 index 0000000..90280cb --- /dev/null +++ b/tests/unit/apply.bats @@ -0,0 +1,119 @@ +#!/usr/bin/env bats +# Unit tests for apply.sh + +load '../test_helper' + +setup() { + setup_test_home + export THEMER_DIR="${BATS_TEST_DIRNAME}/../.." + + # Create a mock backup script that doesn't fail + mkdir -p "$TEST_HOME/scripts" + cat > "$TEST_HOME/scripts/backup.sh" << 'EOF' +#!/bin/bash +mkdir -p "$HOME/.themer-up-backup/mock_backup" +echo "Mock backup" +EOF + chmod +x "$TEST_HOME/scripts/backup.sh" + + # Temporarily patch the apply script to use our mock backup + # We'll test with the real script but mock external dependencies +} + +teardown() { + teardown_test_home +} + +@test "apply.sh exits with error for non-existent theme" { + run "$SCRIPTS_DIR/apply.sh" "nonexistent-theme" + [ "$status" -eq 1 ] + [[ "$output" == *"not found"* ]] +} + +@test "apply.sh shows available themes when theme not found" { + run "$SCRIPTS_DIR/apply.sh" "nonexistent-theme" + [[ "$output" == *"Available themes"* ]] || [[ "$output" == *"synthwave"* ]] +} + +@test "apply.sh uses synthwave as default theme" { + # The script should reference synthwave if no arg provided + # We can verify by checking the script's default behavior + run bash -c "source $SCRIPTS_DIR/apply.sh 2>&1 || true" <<< "" + # Script will try to apply synthwave by default + [[ "$output" == *"synthwave"* ]] || [ "$status" -eq 0 ] +} + +@test "apply.sh creates backup before applying" { + run "$SCRIPTS_DIR/apply.sh" "synthwave" + + # Backup should have been attempted (directory created) + assert_dir_exists "$TEST_BACKUP_DIR" +} + +@test "apply.sh copies iTerm2 profile" { + run "$SCRIPTS_DIR/apply.sh" "synthwave" + [ "$status" -eq 0 ] + + iterm_profile="$TEST_HOME/Library/Application Support/iTerm2/DynamicProfiles/themer-up.json" + assert_file_exists "$iterm_profile" +} + +@test "apply.sh copies p10k theme file" { + run "$SCRIPTS_DIR/apply.sh" "synthwave" + [ "$status" -eq 0 ] + + assert_file_exists "$TEST_HOME/.p10k-themer.zsh" +} + +@test "apply.sh adds p10k source line to .zshrc if not present" { + run "$SCRIPTS_DIR/apply.sh" "synthwave" + [ "$status" -eq 0 ] + + assert_file_contains "$TEST_HOME/.zshrc" "p10k-themer.zsh" +} + +@test "apply.sh does not duplicate p10k source line" { + # Pre-add the source line + echo "[[ -f ~/.p10k-themer.zsh ]] && source ~/.p10k-themer.zsh" >> "$TEST_HOME/.zshrc" + + run "$SCRIPTS_DIR/apply.sh" "synthwave" + [ "$status" -eq 0 ] + + # Count occurrences - should be exactly 1 + count=$(grep -c "p10k-themer.zsh" "$TEST_HOME/.zshrc" || echo 0) + [ "$count" -eq 1 ] +} + +@test "apply.sh copies mprocs config" { + run "$SCRIPTS_DIR/apply.sh" "synthwave" + [ "$status" -eq 0 ] + + assert_file_exists "$TEST_HOME/.config/mprocs/claude.yaml" +} + +@test "apply.sh installs vim colorscheme" { + run "$SCRIPTS_DIR/apply.sh" "synthwave" + [ "$status" -eq 0 ] + + assert_file_exists "$TEST_HOME/.vim/colors/synthwave.vim" + assert_file_exists "$TEST_HOME/.config/nvim/colors/synthwave.vim" +} + +@test "apply.sh adds colorscheme to vimrc" { + run "$SCRIPTS_DIR/apply.sh" "synthwave" + [ "$status" -eq 0 ] + + assert_file_contains "$TEST_HOME/.vimrc" "colorscheme synthwave" +} + +@test "apply.sh outputs success message" { + run "$SCRIPTS_DIR/apply.sh" "synthwave" + [ "$status" -eq 0 ] + [[ "$output" == *"applied"* ]] +} + +@test "apply.sh shows restart instructions" { + run "$SCRIPTS_DIR/apply.sh" "synthwave" + [ "$status" -eq 0 ] + [[ "$output" == *"Restart iTerm2"* ]] || [[ "$output" == *"Actions needed"* ]] +} diff --git a/tests/unit/backup.bats b/tests/unit/backup.bats new file mode 100644 index 0000000..3b83135 --- /dev/null +++ b/tests/unit/backup.bats @@ -0,0 +1,86 @@ +#!/usr/bin/env bats +# Unit tests for backup.sh + +load '../test_helper' + +setup() { + setup_test_home + + # Patch THEMER_DIR in the script for testing + export THEMER_DIR="${BATS_TEST_DIRNAME}/../.." + + # Create files that would normally exist in user's home + echo '{"theme": "old"}' > "$TEST_HOME/Library/Application Support/iTerm2/DynamicProfiles/themer-up.json" + echo 'old p10k config' > "$TEST_HOME/.p10k-themer.zsh" + echo 'old mprocs config' > "$TEST_HOME/.config/mprocs/claude.yaml" +} + +teardown() { + teardown_test_home +} + +@test "backup.sh creates timestamped backup directory" { + run "$SCRIPTS_DIR/backup.sh" + [ "$status" -eq 0 ] + + # Check that a backup directory was created + backup_count=$(find "$TEST_BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ') + [ "$backup_count" -ge 1 ] +} + +@test "backup.sh copies iTerm2 config when present" { + run "$SCRIPTS_DIR/backup.sh" + [ "$status" -eq 0 ] + + # Find the latest backup + latest_backup=$(ls -dt "$TEST_BACKUP_DIR"/*/ 2>/dev/null | head -1) + + assert_file_exists "${latest_backup}iterm2.json" + assert_file_contains "${latest_backup}iterm2.json" "old" +} + +@test "backup.sh copies p10k config when present" { + run "$SCRIPTS_DIR/backup.sh" + [ "$status" -eq 0 ] + + latest_backup=$(ls -dt "$TEST_BACKUP_DIR"/*/ 2>/dev/null | head -1) + assert_file_exists "${latest_backup}p10k.zsh" +} + +@test "backup.sh copies mprocs config when present" { + run "$SCRIPTS_DIR/backup.sh" + [ "$status" -eq 0 ] + + latest_backup=$(ls -dt "$TEST_BACKUP_DIR"/*/ 2>/dev/null | head -1) + assert_file_exists "${latest_backup}mprocs.yaml" +} + +@test "backup.sh handles missing files gracefully" { + # Remove all config files + rm -f "$TEST_HOME/Library/Application Support/iTerm2/DynamicProfiles/themer-up.json" + rm -f "$TEST_HOME/.p10k-themer.zsh" + rm -f "$TEST_HOME/.config/mprocs/claude.yaml" + + run "$SCRIPTS_DIR/backup.sh" + [ "$status" -eq 0 ] + + # Backup dir should still be created (possibly empty) + assert_dir_exists "$TEST_BACKUP_DIR" +} + +@test "backup.sh keeps only last 5 backups" { + # Create 6 old backup directories + for i in {1..6}; do + old_backup="$TEST_BACKUP_DIR/2024010${i}_120000" + mkdir -p "$old_backup" + touch "$old_backup/iterm2.json" + sleep 0.1 # Ensure different timestamps + done + + run "$SCRIPTS_DIR/backup.sh" + [ "$status" -eq 0 ] + + # Should have at most 5 backup directories (5 old + 1 new = 6, then prune to 5) + backup_count=$(find "$TEST_BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ') + [ "$backup_count" -le 5 ] +} diff --git a/tests/unit/restore.bats b/tests/unit/restore.bats new file mode 100644 index 0000000..79e7953 --- /dev/null +++ b/tests/unit/restore.bats @@ -0,0 +1,82 @@ +#!/usr/bin/env bats +# Unit tests for restore.sh + +load '../test_helper' + +setup() { + setup_test_home + export THEMER_DIR="${BATS_TEST_DIRNAME}/../.." + + # Create a mock backup to restore from + export MOCK_BACKUP="$TEST_BACKUP_DIR/20240115_120000" + mkdir -p "$MOCK_BACKUP" + + # Create backup files + echo '{"theme": "backup_theme"}' > "$MOCK_BACKUP/iterm2.json" + echo 'backup p10k config' > "$MOCK_BACKUP/p10k.zsh" + echo 'backup mprocs config' > "$MOCK_BACKUP/mprocs.yaml" +} + +teardown() { + teardown_test_home +} + +@test "restore.sh exits with error when no backups exist" { + rm -rf "$TEST_BACKUP_DIR"/* + + run "$SCRIPTS_DIR/restore.sh" + [ "$status" -eq 1 ] + [[ "$output" == *"No backups found"* ]] +} + +@test "restore.sh restores iTerm2 config" { + run "$SCRIPTS_DIR/restore.sh" + [ "$status" -eq 0 ] + + restored="$TEST_HOME/Library/Application Support/iTerm2/DynamicProfiles/themer-up.json" + assert_file_exists "$restored" + assert_file_contains "$restored" "backup_theme" +} + +@test "restore.sh restores p10k config" { + run "$SCRIPTS_DIR/restore.sh" + [ "$status" -eq 0 ] + + assert_file_exists "$TEST_HOME/.p10k-themer.zsh" + assert_file_contains "$TEST_HOME/.p10k-themer.zsh" "backup p10k" +} + +@test "restore.sh restores mprocs config" { + run "$SCRIPTS_DIR/restore.sh" + [ "$status" -eq 0 ] + + assert_file_exists "$TEST_HOME/.config/mprocs/claude.yaml" + assert_file_contains "$TEST_HOME/.config/mprocs/claude.yaml" "backup mprocs" +} + +@test "restore.sh uses most recent backup when multiple exist" { + # Create an older backup with different content + older_backup="$TEST_BACKUP_DIR/20240110_100000" + mkdir -p "$older_backup" + echo '{"theme": "older_theme"}' > "$older_backup/iterm2.json" + + # Create a newer backup + newer_backup="$TEST_BACKUP_DIR/20240120_140000" + mkdir -p "$newer_backup" + echo '{"theme": "newer_theme"}' > "$newer_backup/iterm2.json" + + run "$SCRIPTS_DIR/restore.sh" + [ "$status" -eq 0 ] + + # Should restore from newer backup + restored="$TEST_HOME/Library/Application Support/iTerm2/DynamicProfiles/themer-up.json" + assert_file_contains "$restored" "newer_theme" +} + +@test "restore.sh outputs success messages" { + run "$SCRIPTS_DIR/restore.sh" + [ "$status" -eq 0 ] + [[ "$output" == *"iTerm2 restored"* ]] + [[ "$output" == *"Powerlevel10k restored"* ]] + [[ "$output" == *"mprocs restored"* ]] +} From 598b08fcce6003afaefec0146aaa81b427541028 Mon Sep 17 00:00:00 2001 From: amacsmith Date: Tue, 13 Jan 2026 23:05:53 -0500 Subject: [PATCH 2/2] fix: remove unused variables from test orchestrator - Remove unused COVERAGE variable and -c/--coverage flag - Use bats_opts variable consistently in all bats invocations - Add shellcheck disable comments for intentional word splitting Fixes CI pipeline ShellCheck warnings (SC2034) Co-Authored-By: Claude Opus 4.5 --- scripts/test.sh | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/scripts/test.sh b/scripts/test.sh index d3814f7..b0def14 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -16,7 +16,6 @@ NC='\033[0m' # Parse arguments PARALLEL=false -COVERAGE=false FAST=false VERBOSE=false @@ -26,10 +25,6 @@ while [[ $# -gt 0 ]]; do PARALLEL=true shift ;; - -c|--coverage) - COVERAGE=true - shift - ;; -f|--fast) FAST=true shift @@ -116,9 +111,10 @@ echo "" echo -e "${CYAN}Stage 2: Unit Tests${NC}" echo "────────────────────────────────────────" -BATS_OPTS="" +# Build bats options based on flags +bats_opts="--tap" if $VERBOSE; then - BATS_OPTS="--verbose-run" + bats_opts="--tap --verbose-run" fi unit_tests=( @@ -129,7 +125,7 @@ unit_tests=( if $PARALLEL && command -v parallel &> /dev/null; then echo " Running in parallel mode..." - printf '%s\n' "${unit_tests[@]}" | parallel -j 3 "bats {} --tap" 2>&1 | while read -r line; do + printf '%s\n' "${unit_tests[@]}" | parallel -j 3 "bats {} $bats_opts" 2>&1 | while read -r line; do echo " $line" done else @@ -137,7 +133,8 @@ else if [[ -f "$test_file" ]]; then test_name=$(basename "$test_file" .bats) echo -n " $test_name... " - if bats "$test_file" --tap > /tmp/bats_output 2>&1; then + # shellcheck disable=SC2086 + if bats "$test_file" $bats_opts > /tmp/bats_output 2>&1; then passed=$(grep -c "^ok" /tmp/bats_output || echo 0) echo -e "${GREEN}PASS${NC} ($passed tests)" else @@ -163,7 +160,8 @@ if ! $FAST; then if [[ -f "$TESTS_DIR/integration/theme_workflow.bats" ]]; then echo -n " theme_workflow... " - if bats "$TESTS_DIR/integration/theme_workflow.bats" --tap > /tmp/bats_output 2>&1; then + # shellcheck disable=SC2086 + if bats "$TESTS_DIR/integration/theme_workflow.bats" $bats_opts > /tmp/bats_output 2>&1; then passed=$(grep -c "^ok" /tmp/bats_output || echo 0) echo -e "${GREEN}PASS${NC} ($passed tests)" else