BMAD-METHOD/examples/tdd-demo-calculator/bmad-core/scripts/tdd-guard.sh

394 lines
12 KiB
Bash
Executable File

#!/bin/bash
# TDD Guard - Validates that code changes follow TDD discipline
# Part of BMAD Framework TDD integration
set -e
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
CONFIG_FILE="${PROJECT_ROOT}/bmad-core/core-config.yaml"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Default values
TDD_ENABLED="false"
ALLOW_RED_PHASE_FAILURES="true"
EXIT_CODE=0
# Usage information
usage() {
cat << EOF
TDD Guard - Validates TDD discipline in code changes
Usage: $0 [options]
Options:
-h, --help Show this help message
-c, --config PATH Path to BMAD core config file
-b, --base REF Base commit/branch for comparison (default: HEAD~1)
-v, --verbose Verbose output
--phase PHASE Current TDD phase: red|green|refactor
--ci Running in CI mode (affects exit behavior)
--dry-run Show what would be checked without failing
Examples:
$0 # Check changes against HEAD~1
$0 --base main # Check changes against main branch
$0 --phase green # Validate green phase rules
$0 --ci --phase red # CI mode, red phase (allows failures)
Exit Codes:
0 No TDD violations found
1 TDD violations found (in green phase)
2 Configuration error
3 Git/repository error
EOF
}
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
# Parse command line arguments
BASE_REF="HEAD~1"
VERBOSE=false
TDD_PHASE=""
CI_MODE=false
DRY_RUN=false
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
-c|--config)
CONFIG_FILE="$2"
shift 2
;;
-b|--base)
BASE_REF="$2"
shift 2
;;
-v|--verbose)
VERBOSE=true
shift
;;
--phase)
TDD_PHASE="$2"
shift 2
;;
--ci)
CI_MODE=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
*)
log_error "Unknown option: $1"
usage
exit 2
;;
esac
done
# Check if we're in a git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
log_error "Not in a git repository"
exit 3
fi
# Load configuration
load_config() {
if [[ ! -f "$CONFIG_FILE" ]]; then
if [[ "$VERBOSE" == true ]]; then
log_warn "Config file not found: $CONFIG_FILE"
log_info "Assuming TDD disabled"
fi
return 0
fi
# Extract TDD settings from YAML (basic parsing)
if command -v yq > /dev/null 2>&1; then
TDD_ENABLED=$(yq e '.tdd.enabled // false' "$CONFIG_FILE" 2>/dev/null || echo "false")
ALLOW_RED_PHASE_FAILURES=$(yq e '.tdd.allow_red_phase_ci_failures // true' "$CONFIG_FILE" 2>/dev/null || echo "true")
else
# Fallback: basic grep parsing
if grep -q "tdd:" "$CONFIG_FILE" && grep -A 10 "tdd:" "$CONFIG_FILE" | grep -q "enabled: true"; then
TDD_ENABLED="true"
fi
fi
if [[ "$VERBOSE" == true ]]; then
log_info "TDD enabled: $TDD_ENABLED"
log_info "Allow red phase failures: $ALLOW_RED_PHASE_FAILURES"
fi
}
# Detect TDD phase from commit messages or branch name
detect_tdd_phase() {
if [[ -n "$TDD_PHASE" ]]; then
return 0
fi
# Check recent commit messages for TDD phase indicators
RECENT_COMMITS=$(git log --oneline -5 "$BASE_REF".."HEAD" 2>/dev/null || echo "")
if echo "$RECENT_COMMITS" | grep -qi "\[RED\]"; then
TDD_PHASE="red"
elif echo "$RECENT_COMMITS" | grep -qi "\[GREEN\]"; then
TDD_PHASE="green"
elif echo "$RECENT_COMMITS" | grep -qi "\[REFACTOR\]"; then
TDD_PHASE="refactor"
else
# Try to detect from branch name
BRANCH_NAME=$(git branch --show-current 2>/dev/null || echo "")
if echo "$BRANCH_NAME" | grep -qi "tdd"; then
TDD_PHASE="green" # Default assumption
fi
fi
if [[ "$VERBOSE" == true ]]; then
log_info "Detected TDD phase: ${TDD_PHASE:-unknown}"
fi
}
# Get changed files between base and current
get_changed_files() {
# Get list of changed files
CHANGED_FILES=$(git diff --name-only "$BASE_REF"..."HEAD" 2>/dev/null || echo "")
if [[ -z "$CHANGED_FILES" ]]; then
if [[ "$VERBOSE" == true ]]; then
log_info "No changed files detected"
fi
return 0
fi
# Separate source and test files
SOURCE_FILES=""
TEST_FILES=""
while IFS= read -r file; do
if [[ -f "$file" ]]; then
if is_test_file "$file"; then
TEST_FILES="$TEST_FILES$file"$'\n'
elif is_source_file "$file"; then
SOURCE_FILES="$SOURCE_FILES$file"$'\n'
fi
fi
done <<< "$CHANGED_FILES"
if [[ "$VERBOSE" == true ]]; then
log_info "Source files changed: $(echo "$SOURCE_FILES" | wc -l | tr -d ' ')"
log_info "Test files changed: $(echo "$TEST_FILES" | wc -l | tr -d ' ')"
fi
}
# Check if file is a test file
is_test_file() {
local file="$1"
# Common test file patterns
if [[ "$file" =~ \.(test|spec)\.(js|ts|py|go|java|cs)$ ]] || \
[[ "$file" =~ _test\.(py|go)$ ]] || \
[[ "$file" =~ Test\.(java|cs)$ ]] || \
[[ "$file" =~ tests?/ ]] || \
[[ "$file" =~ spec/ ]]; then
return 0
fi
return 1
}
# Check if file is a source file
is_source_file() {
local file="$1"
# Common source file patterns (excluding test files)
if [[ "$file" =~ \.(js|ts|py|go|java|cs|rb|php|cpp|c|h)$ ]] && ! is_test_file "$file"; then
return 0
fi
return 1
}
# Check if commit message indicates refactoring
is_refactor_commit() {
local commits=$(git log --oneline "$BASE_REF".."HEAD" 2>/dev/null || echo "")
if echo "$commits" | grep -qi "\[refactor\]"; then
return 0
fi
return 1
}
# Validate TDD rules
validate_tdd_rules() {
local violations=0
if [[ -z "$SOURCE_FILES" && -z "$TEST_FILES" ]]; then
if [[ "$VERBOSE" == true ]]; then
log_info "No relevant source or test files changed"
fi
return 0
fi
case "$TDD_PHASE" in
"red")
# Red phase: Tests should be added/modified, minimal or no source changes
if [[ -n "$SOURCE_FILES" ]] && [[ -z "$TEST_FILES" ]]; then
log_warn "RED phase violation: Source code changed without corresponding test changes"
log_warn "In TDD Red phase, tests should be written first"
if [[ "$ALLOW_RED_PHASE_FAILURES" == "false" ]] || [[ "$CI_MODE" == "false" ]]; then
violations=$((violations + 1))
fi
fi
;;
"green")
# Green phase: Source changes must have corresponding test changes
if [[ -n "$SOURCE_FILES" ]] && [[ -z "$TEST_FILES" ]]; then
log_error "GREEN phase violation: Source code changed without corresponding tests"
log_error "In TDD Green phase, implementation should only make existing tests pass"
log_error "Source files modified:"
echo "$SOURCE_FILES" | while IFS= read -r file; do
[[ -n "$file" ]] && log_error " - $file"
done
violations=$((violations + 1))
fi
# Check for large changes (potential feature creep)
if [[ -n "$SOURCE_FILES" ]]; then
local large_changes=0
while IFS= read -r file; do
if [[ -n "$file" ]] && [[ -f "$file" ]]; then
local additions=$(git diff --numstat "$BASE_REF" "$file" | cut -f1)
if [[ "$additions" =~ ^[0-9]+$ ]] && [[ "$additions" -gt 50 ]]; then
log_warn "Large change detected in $file: $additions lines added"
log_warn "Consider smaller, more focused changes in TDD Green phase"
large_changes=$((large_changes + 1))
fi
fi
done <<< "$SOURCE_FILES"
if [[ "$large_changes" -gt 0 ]]; then
log_warn "Consider breaking large changes into smaller TDD cycles"
fi
fi
;;
"refactor")
# Refactor phase: Source changes allowed, tests should remain stable
if is_refactor_commit; then
if [[ "$VERBOSE" == true ]]; then
log_info "Refactor phase: Changes detected with proper [REFACTOR] tag"
fi
else
if [[ -n "$SOURCE_FILES" ]] && [[ -z "$TEST_FILES" ]]; then
log_warn "Potential refactor phase: Consider tagging commits with [REFACTOR]"
fi
fi
;;
*)
# Unknown or no TDD phase
if [[ "$TDD_ENABLED" == "true" ]]; then
log_warn "TDD enabled but phase not detected"
log_warn "Consider tagging commits with [RED], [GREEN], or [REFACTOR]"
if [[ -n "$SOURCE_FILES" ]] && [[ -z "$TEST_FILES" ]]; then
log_warn "Source changes without test changes - may violate TDD discipline"
fi
fi
;;
esac
return $violations
}
# Main execution
main() {
if [[ "$VERBOSE" == true ]]; then
log_info "TDD Guard starting..."
log_info "Base reference: $BASE_REF"
log_info "Config file: $CONFIG_FILE"
fi
load_config
if [[ "$TDD_ENABLED" != "true" ]]; then
if [[ "$VERBOSE" == true ]]; then
log_info "TDD not enabled, skipping validation"
fi
exit 0
fi
detect_tdd_phase
get_changed_files
if [[ "$DRY_RUN" == true ]]; then
log_info "DRY RUN - Would check:"
log_info " TDD Phase: ${TDD_PHASE:-unknown}"
log_info " Source files: $(echo "$SOURCE_FILES" | grep -c . || echo 0)"
log_info " Test files: $(echo "$TEST_FILES" | grep -c . || echo 0)"
exit 0
fi
validate_tdd_rules
local violations=$?
if [[ "$violations" -eq 0 ]]; then
log_success "TDD validation passed"
exit 0
else
log_error "$violations TDD violation(s) found"
# Provide helpful suggestions
echo ""
echo "💡 TDD Suggestions:"
case "$TDD_PHASE" in
"green")
echo " - Ensure all source changes have corresponding failing tests first"
echo " - Consider running QA agent's *write-failing-tests command"
echo " - Keep implementation minimal - only make tests pass"
;;
"red")
echo " - Write failing tests before implementation"
echo " - Use QA agent to create test cases first"
;;
*)
echo " - Follow TDD Red-Green-Refactor cycle"
echo " - Tag commits with [RED], [GREEN], or [REFACTOR]"
echo " - Enable TDD workflow in BMAD configuration"
;;
esac
echo ""
if [[ "$TDD_PHASE" == "red" ]] && [[ "$ALLOW_RED_PHASE_FAILURES" == "true" ]] && [[ "$CI_MODE" == "true" ]]; then
log_warn "Red phase violations allowed in CI mode"
exit 0
fi
exit 1
fi
}
# Run main function
main "$@"