394 lines
12 KiB
Bash
Executable File
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 "$@"
|