Last active
January 14, 2026 16:36
-
-
Save zelaznik/506fbcfec02ea17ec26282a43cd862fb to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # red-green-refactor-validator.sh | |
| # Validates test-driven development red-green-refactor workflow | |
| # | |
| # Exit codes: | |
| # 0 - Validation successful (tests failed on old code, passed on new code) | |
| # 1 - Validation failed (no tests changed, or tests didn't follow red-green pattern) | |
| # 2 - Precondition failed (unstaged changes exist) | |
| # 3 - Script error (stash/restore failed) | |
| set -euo pipefail | |
| # Exit codes | |
| readonly EXIT_SUCCESS=0 | |
| readonly EXIT_VALIDATION_FAILED=1 | |
| readonly EXIT_UNSTAGED_CHANGES=2 | |
| readonly EXIT_SCRIPT_ERROR=3 | |
| # State tracking for cleanup | |
| STASH_CREATED=false | |
| STASH_REF="" | |
| # Function: Check for unstaged changes in tracked files | |
| check_no_unstaged_changes() { | |
| echo "Checking for unstaged changes in tracked files..." >&2 | |
| # Check for modified tracked files that aren't staged | |
| if ! git diff --quiet; then | |
| echo "" >&2 | |
| echo "ERROR: You have unstaged changes in tracked files." >&2 | |
| echo "Please stage or stash all changes before running validation." >&2 | |
| echo "" >&2 | |
| echo "Unstaged changes:" >&2 | |
| git diff --name-status >&2 | |
| return 1 | |
| fi | |
| echo "✓ No unstaged changes detected" >&2 | |
| return 0 | |
| } | |
| # Function: Get list of changed spec files to run with rspec | |
| get_changed_spec_files() { | |
| echo "Identifying changed spec files..." >&2 | |
| # Get staged spec files ending in _spec.rb (these will be run with rspec) | |
| local spec_files | |
| spec_files=$(git diff --cached --name-status HEAD | \ | |
| egrep -v '^D' | \ | |
| awk '{print $NF}' | \ | |
| egrep '_spec\.rb$' || true) | |
| if [[ -z "$spec_files" ]]; then | |
| echo "" >&2 | |
| echo "ERROR: No spec files changed in staged changes." >&2 | |
| echo "TDD requires writing tests. You must modify at least one _spec.rb file." >&2 | |
| echo "" >&2 | |
| exit "$EXIT_VALIDATION_FAILED" | |
| fi | |
| echo "$spec_files" | |
| } | |
| # Function: Get list of changed application files (non-spec directory files) | |
| get_changed_app_files() { | |
| # Get staged files outside ./spec/ directory (application code to stash) | |
| local app_files | |
| app_files=$(git diff --cached --name-status HEAD | \ | |
| egrep -v '^D' | \ | |
| awk '{print $NF}' | \ | |
| egrep -v '^spec/' || true) | |
| echo "$app_files" | |
| } | |
| # Function: Stash only application code (non-spec files) to restore old code | |
| stash_app_files() { | |
| local app_files="$1" | |
| if [[ -z "$app_files" ]]; then | |
| echo "✓ No application files to stash (spec-only changes)" >&2 | |
| return 0 | |
| fi | |
| echo "Stashing application code changes..." >&2 | |
| echo "$app_files" | sed 's/^/ - /' >&2 | |
| # Create a timestamped stash of application files only | |
| # This keeps spec changes in place so we can test new specs against old app code | |
| local stash_message="red-green-refactor-validation-$(date +%s)" | |
| # Convert newline-separated files to arguments for git stash push | |
| if echo "$app_files" | xargs git stash push -m "$stash_message" --quiet --; then | |
| STASH_CREATED=true | |
| STASH_REF=$(git rev-parse stash@{0}) | |
| echo "✓ Application code stashed: $STASH_REF" >&2 | |
| return 0 | |
| else | |
| echo "ERROR: Failed to stash application code" >&2 | |
| return 1 | |
| fi | |
| } | |
| # Function: Restore application code from stash | |
| restore_app_files() { | |
| if [[ "$STASH_CREATED" == true ]] && [[ -n "$STASH_REF" ]]; then | |
| echo "Restoring application code from stash..." >&2 | |
| # Pop the stash without --index since we kept spec files staged | |
| # This avoids conflicts with the modified staging area | |
| if git stash pop --quiet 2>&1; then | |
| echo "✓ Application code restored" >&2 | |
| # Re-stage all changes (both app files and spec files) | |
| # This ensures everything is properly staged for commit | |
| if git add -u; then | |
| echo "✓ Changes re-staged" >&2 | |
| fi | |
| STASH_CREATED=false | |
| return 0 | |
| else | |
| echo "" >&2 | |
| echo "ERROR: Failed to restore stash." >&2 | |
| echo "Your changes are safe at: $STASH_REF" >&2 | |
| echo "Manually restore with: git stash apply $STASH_REF" >&2 | |
| return 1 | |
| fi | |
| fi | |
| return 0 | |
| } | |
| # Function: Run tests expecting failure (RED phase) | |
| run_tests_expect_failure() { | |
| local test_files="$1" | |
| echo "" >&2 | |
| echo "========================================" >&2 | |
| echo "RED Phase: New specs + Old app code" >&2 | |
| echo "========================================" >&2 | |
| echo "Expecting tests to FAIL..." >&2 | |
| echo "" >&2 | |
| # Run tests and capture exit code | |
| local exit_code=0 | |
| echo "$test_files" | xargs bundle exec rspec || exit_code=$? | |
| if [[ $exit_code -eq 0 ]]; then | |
| echo "" >&2 | |
| echo "ERROR: New specs PASSED with old application code, but should have FAILED." >&2 | |
| echo "This violates red-green-refactor: you must start with failing tests." >&2 | |
| echo "" >&2 | |
| return 1 | |
| else | |
| echo "" >&2 | |
| echo "✓ New specs failed with old app code as expected (RED phase confirmed)" >&2 | |
| return 0 | |
| fi | |
| } | |
| # Function: Run tests expecting success (GREEN phase) | |
| run_tests_expect_success() { | |
| local test_files="$1" | |
| echo "" >&2 | |
| echo "========================================" >&2 | |
| echo "GREEN Phase: New specs + New app code" >&2 | |
| echo "========================================" >&2 | |
| echo "Expecting tests to PASS..." >&2 | |
| echo "" >&2 | |
| # Run tests and capture exit code | |
| if echo "$test_files" | xargs bundle exec rspec; then | |
| echo "" >&2 | |
| echo "✓ New specs passed with new app code as expected (GREEN phase confirmed)" >&2 | |
| return 0 | |
| else | |
| echo "" >&2 | |
| echo "ERROR: New specs FAILED with new application code." >&2 | |
| echo "Fix your code so tests pass before committing." >&2 | |
| echo "" >&2 | |
| return 1 | |
| fi | |
| } | |
| # Function: Cleanup handler to restore state on exit | |
| cleanup() { | |
| local exit_code=$? | |
| # Only run cleanup if stash was created | |
| if [[ "$STASH_CREATED" == true ]]; then | |
| echo "" >&2 | |
| echo "Cleanup: Restoring application code..." >&2 | |
| if ! restore_app_files; then | |
| echo "" >&2 | |
| echo "CRITICAL: Failed to restore changes." >&2 | |
| echo "Stash saved at: $STASH_REF" >&2 | |
| echo "Manually restore with: git stash apply $STASH_REF" >&2 | |
| exit "$EXIT_SCRIPT_ERROR" | |
| fi | |
| fi | |
| exit "$exit_code" | |
| } | |
| # Register cleanup handler for any exit (success, error, interrupt) | |
| trap cleanup EXIT INT TERM | |
| # Main execution | |
| main() { | |
| echo "" >&2 | |
| echo "========================================" >&2 | |
| echo "Red-Green-Refactor Validation" >&2 | |
| echo "========================================" >&2 | |
| echo "" >&2 | |
| # Step 1: Verify preconditions - no unstaged changes | |
| if ! check_no_unstaged_changes; then | |
| exit "$EXIT_UNSTAGED_CHANGES" | |
| fi | |
| echo "" >&2 | |
| # Step 2: Identify changed spec files (to run with rspec) | |
| local spec_files | |
| spec_files=$(get_changed_spec_files) | |
| echo "✓ Found changed spec files:" >&2 | |
| echo "$spec_files" | sed 's/^/ - /' >&2 | |
| echo "" >&2 | |
| # Step 3: Identify changed application files (to stash) | |
| local app_files | |
| app_files=$(get_changed_app_files) | |
| if [[ -n "$app_files" ]]; then | |
| echo "✓ Found changed application files:" >&2 | |
| echo "$app_files" | sed 's/^/ - /' >&2 | |
| echo "" >&2 | |
| fi | |
| # Step 4: Stash application code (keep spec changes in working directory) | |
| if ! stash_app_files "$app_files"; then | |
| exit "$EXIT_SCRIPT_ERROR" | |
| fi | |
| # Step 5: Run tests on old app code with new specs - expecting failure | |
| if ! run_tests_expect_failure "$spec_files"; then | |
| # Cleanup handler will restore application code | |
| exit "$EXIT_VALIDATION_FAILED" | |
| fi | |
| # Step 6: Restore application code (new code) | |
| if ! restore_app_files; then | |
| exit "$EXIT_SCRIPT_ERROR" | |
| fi | |
| # Step 7: Run tests on new code - expecting success | |
| if ! run_tests_expect_success "$spec_files"; then | |
| # Staged changes are already restored | |
| exit "$EXIT_VALIDATION_FAILED" | |
| fi | |
| # Success! | |
| echo "" >&2 | |
| echo "========================================" >&2 | |
| echo "✓ Red-Green-Refactor validation PASSED" >&2 | |
| echo "========================================" >&2 | |
| echo "" >&2 | |
| echo "Your changes follow proper TDD workflow:" >&2 | |
| echo " ✓ New specs failed with old application code (RED)" >&2 | |
| echo " ✓ New specs passed with new application code (GREEN)" >&2 | |
| echo "" >&2 | |
| exit "$EXIT_SUCCESS" | |
| } | |
| # Execute main function | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment