Loading...
No commits yet
Not committed History
Blame
sync_tests_with_source.sh • 16.9 KB
#!/bin/bash
# -*- coding: utf-8 -*-
# Timestamp: "2026-01-09 14:00:00 (ywatanabe)"
# File: ./tests/sync_tests_with_source.sh

# =============================================================================
# Test Synchronization Script for scitex-writer
# =============================================================================
#
# PURPOSE:
#   Synchronizes test file structure with source code structure, ensuring
#   every source file has a corresponding test file with embedded source
#   code for reference.
#
# BEHAVIOR:
#   1. Mirrors scripts/ directory structure to tests/
#      - scripts/shell/ -> tests/shell/
#      - scripts/python/ -> tests/python/
#   2. For each source file:
#      - Shell: scripts/shell/foo.sh -> tests/shell/test_foo.sh
#      - Python: scripts/python/bar.py -> tests/python/test_bar.py
#   3. Preserves existing test code (before source block)
#   4. Updates commented source code block at file end
#   5. Identifies "stale" tests (tests without matching source files)
#   6. With -m flag: moves stale tests to .old-{timestamp}/ directories
#
# USAGE:
#   ./tests/sync_tests_with_source.sh          # Dry run - report stale & placeholder files
#   ./tests/sync_tests_with_source.sh -m       # Move stale files to .old/
#   ./tests/sync_tests_with_source.sh -j 16    # Use 16 parallel jobs
#
# =============================================================================

set -euo pipefail

ORIG_DIR="$(pwd)"
THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(realpath "$THIS_DIR/..")"
LOG_PATH="$THIS_DIR/.$(basename "$0").log"
echo "" >"$LOG_PATH"

# Color scheme
GRAY='\033[0;90m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
NC='\033[0m'

echo_info() { echo -e "${GRAY}INFO: $1${NC}"; }
echo_success() { echo -e "${GREEN}SUCC: $1${NC}"; }
echo_warning() { echo -e "${YELLOW}WARN: $1${NC}"; }
echo_error() { echo -e "${RED}ERRO: $1${NC}"; }
echo_header() { echo_info "=== $1 ==="; }

########################################
# Usage & Argument Parser
########################################
DO_MOVE=false
SCRIPTS_DIR="$ROOT_DIR/scripts"
TESTS_DIR="$ROOT_DIR/tests"
CPU_COUNT=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
PARALLEL_JOBS=$((CPU_COUNT / 2 > 0 ? CPU_COUNT / 2 : 1))

usage() {
    echo "Usage: $0 [options]"
    echo
    echo "Synchronizes test files with source files in scripts/, maintaining test code while updating source references."
    echo "Reports stale tests and placeholder-only tests by default."
    echo
    echo "Options:"
    echo "  -m, --move         Move stale test files to .old directory (default: $DO_MOVE)"
    echo "  -j, --jobs N       Number of parallel jobs (default: $PARALLEL_JOBS)"
    echo "  -h, --help         Display this help message"
    echo
    echo "Directory Mapping:"
    echo "  scripts/shell/     -> tests/shell/"
    echo "  scripts/python/    -> tests/python/"
    echo
    exit 1
}

while [[ $# -gt 0 ]]; do
    case $1 in
    -m | --move)
        DO_MOVE=true
        shift
        ;;
    -j | --jobs)
        PARALLEL_JOBS="$2"
        shift 2
        ;;
    -h | --help) usage ;;
    *)
        echo "Unknown option: $1"
        usage
        ;;
    esac
done

########################################
# Blacklist patterns
########################################
EXCLUDE_PATTERNS=(
    "*/.*"
    "*/__pycache__/*"
    "*/archive/*"
    "*/.old*"
    "*/deprecated/*"
)

construct_find_excludes() {
    FIND_EXCLUDES=()
    for pattern in "${EXCLUDE_PATTERNS[@]}"; do
        FIND_EXCLUDES+=(-not -path "$pattern")
    done
}

########################################
# Source Code Block Generators
########################################
get_shell_source_block() {
    local src_file=$1
    echo ""
    echo "# --------------------------------------------------------------------------------"
    echo "# Start of Source Code from: $src_file"
    echo "# --------------------------------------------------------------------------------"
    sed 's/^/# /' "$src_file"
    echo ""
    echo "# --------------------------------------------------------------------------------"
    echo "# End of Source Code from: $src_file"
    echo "# --------------------------------------------------------------------------------"
}

get_python_source_block() {
    local src_file=$1
    echo ""
    echo "# --------------------------------------------------------------------------------"
    echo "# Start of Source Code from: $src_file"
    echo "# --------------------------------------------------------------------------------"
    sed 's/^/# /' "$src_file"
    echo ""
    echo "# --------------------------------------------------------------------------------"
    echo "# End of Source Code from: $src_file"
    echo "# --------------------------------------------------------------------------------"
}

########################################
# Test File Templates
########################################
get_shell_test_template() {
    # shellcheck disable=SC2034
    local src_name=$1 # Used via sed substitution on SOURCE_NAME
    cat <<'TEMPLATE'
#!/bin/bash
# -*- coding: utf-8 -*-
# Test file for: SOURCE_NAME

THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(realpath "$THIS_DIR/../..")"

# Test counter
TESTS_RUN=0
TESTS_PASSED=0
TESTS_FAILED=0

# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'

assert_success() {
    local cmd="$1"
    local desc="${2:-$cmd}"
    ((TESTS_RUN++))
    if eval "$cmd" > /dev/null 2>&1; then
        echo -e "${GREEN}✓${NC} $desc"
        ((TESTS_PASSED++))
    else
        echo -e "${RED}✗${NC} $desc"
        ((TESTS_FAILED++))
    fi
}

assert_file_exists() {
    local file="$1"
    ((TESTS_RUN++))
    if [ -f "$file" ]; then
        echo -e "${GREEN}✓${NC} File exists: $file"
        ((TESTS_PASSED++))
    else
        echo -e "${RED}✗${NC} File missing: $file"
        ((TESTS_FAILED++))
    fi
}

# Add your tests here
test_placeholder() {
    echo "TODO: Add tests for SOURCE_NAME"
}

# Run tests
main() {
    echo "Testing: SOURCE_NAME"
    echo "========================================"

    test_placeholder

    echo "========================================"
    echo "Results: $TESTS_PASSED/$TESTS_RUN passed"
    [ $TESTS_FAILED -gt 0 ] && exit 1
    exit 0
}

main "$@"
TEMPLATE
}

get_python_test_template() {
    # shellcheck disable=SC2034
    local src_name=$1 # Used via sed substitution on SOURCE_NAME
    cat <<'TEMPLATE'
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Test file for: SOURCE_NAME

import os
import sys
from pathlib import Path

# Add scripts/python to path for imports
ROOT_DIR = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(ROOT_DIR / "scripts" / "python"))


# Add your tests here
def test_placeholder():
    """TODO: Add tests for SOURCE_NAME"""
    pass


if __name__ == "__main__":
    import pytest
    pytest.main([os.path.abspath(__file__), "-v"])
TEMPLATE
}

########################################
# Extract existing test code
########################################
extract_test_code() {
    local test_file=$1
    local temp_file
    temp_file=$(mktemp)

    if grep -q "# Start of Source Code from:" "$test_file"; then
        sed -n '/# Start of Source Code from:/q;p' "$test_file" >"$temp_file"
    else
        cat "$test_file" >"$temp_file"
    fi

    # Remove trailing blank lines
    if [ -s "$temp_file" ]; then
        sed -i -e :a -e '/^\n*$/{$d;N;ba' -e '}' "$temp_file" 2>/dev/null || true
        cat "$temp_file"
    fi
    rm -f "$temp_file"
}

########################################
# Process Shell Scripts
########################################
process_shell_file() {
    local src_file="$1"
    local scripts_dir="$2"
    local tests_dir="$3"

    local rel="${src_file#"$scripts_dir"/shell/}"
    local rel_dir
    rel_dir=$(dirname "$rel")
    # Handle files in root of shell/ directory
    [ "$rel_dir" = "." ] && rel_dir=""
    local src_base
    src_base=$(basename "$rel" .sh)

    local test_subdir="$tests_dir/shell${rel_dir:+/$rel_dir}"
    mkdir -p "$test_subdir"
    local test_file="$test_subdir/test_${src_base}.sh"

    if [ ! -f "$test_file" ]; then
        echo_info "Creating: $test_file"
        get_shell_test_template "$src_base.sh" | sed "s/SOURCE_NAME/$src_base.sh/g" >"$test_file"
        get_shell_source_block "$src_file" >>"$test_file"
        chmod +x "$test_file"
    else
        local temp_file
        temp_file=$(mktemp)
        local test_code
        test_code=$(extract_test_code "$test_file")

        if [ -n "$test_code" ]; then
            echo "$test_code" >"$temp_file"
        else
            get_shell_test_template "$src_base.sh" | sed "s/SOURCE_NAME/$src_base.sh/g" >"$temp_file"
        fi

        get_shell_source_block "$src_file" >>"$temp_file"
        mv "$temp_file" "$test_file"
        chmod +x "$test_file"
    fi
}
export -f process_shell_file get_shell_source_block get_shell_test_template extract_test_code echo_info
export GRAY GREEN YELLOW RED NC

########################################
# Process Python Scripts
########################################
process_python_file() {
    local src_file="$1"
    local scripts_dir="$2"
    local tests_dir="$3"

    # Skip __init__.py and private modules
    local file_basename
    file_basename=$(basename "$src_file")
    [[ "$file_basename" == "__init__.py" ]] && return
    [[ "$file_basename" == _*.py ]] && return

    local rel="${src_file#"$scripts_dir"/python/}"
    local rel_dir
    rel_dir=$(dirname "$rel")
    # Handle files in root of python/ directory
    [ "$rel_dir" = "." ] && rel_dir=""
    local src_base
    src_base=$(basename "$rel" .py)

    local test_subdir="$tests_dir/python${rel_dir:+/$rel_dir}"
    mkdir -p "$test_subdir"
    local test_file="$test_subdir/test_${src_base}.py"

    if [ ! -f "$test_file" ]; then
        echo_info "Creating: $test_file"
        get_python_test_template "$src_base.py" | sed "s/SOURCE_NAME/$src_base.py/g" >"$test_file"
        get_python_source_block "$src_file" >>"$test_file"
        chmod +x "$test_file"
    else
        local temp_file
        temp_file=$(mktemp)
        local test_code
        test_code=$(extract_test_code "$test_file")

        if [ -n "$test_code" ]; then
            echo "$test_code" >"$temp_file"
        else
            get_python_test_template "$src_base.py" | sed "s/SOURCE_NAME/$src_base.py/g" >"$temp_file"
        fi

        get_python_source_block "$src_file" >>"$temp_file"
        mv "$temp_file" "$test_file"
        chmod +x "$test_file"
    fi
}
export -f process_python_file get_python_source_block get_python_test_template

########################################
# Stale Test Detection
########################################
find_stale_tests() {
    local tests_dir="$1"
    local scripts_dir="$2"
    local stale_files=()

    # Check shell tests
    if [ -d "$tests_dir/shell" ]; then
        while IFS= read -r test_file; do
            [[ "$test_file" =~ \.old ]] && continue
            local rel="${test_file#"$tests_dir"/shell/}"
            local test_base
            test_base=$(basename "$rel")
            local src_base="${test_base#test_}"
            local src_dir
            src_dir=$(dirname "$rel")
            [ "$src_dir" = "." ] && src_dir=""
            local src_file="$scripts_dir/shell${src_dir:+/$src_dir}/$src_base"

            [ ! -f "$src_file" ] && stale_files+=("$test_file")
        done < <(find "$tests_dir/shell" -name "test_*.sh" 2>/dev/null)
    fi

    # Check python tests
    if [ -d "$tests_dir/python" ]; then
        while IFS= read -r test_file; do
            [[ "$test_file" =~ \.old ]] && continue
            local rel="${test_file#"$tests_dir"/python/}"
            local test_base
            test_base=$(basename "$rel")
            local src_base="${test_base#test_}"
            local src_dir
            src_dir=$(dirname "$rel")
            [ "$src_dir" = "." ] && src_dir=""
            local src_file="$scripts_dir/python${src_dir:+/$src_dir}/$src_base"

            [ ! -f "$src_file" ] && stale_files+=("$test_file")
        done < <(find "$tests_dir/python" -name "test_*.py" 2>/dev/null)
    fi

    printf '%s\n' "${stale_files[@]}"
}

move_stale_tests() {
    local timestamp
    timestamp=$(date +%Y%m%d_%H%M%S)
    local stale_count=0
    local moved_count=0

    while IFS= read -r stale_file; do
        [ -z "$stale_file" ] && continue
        ((stale_count++)) || true

        local rel_path="${stale_file#"$TESTS_DIR"/}"
        if [ "$DO_MOVE" = "true" ]; then
            local stale_dir
            stale_dir=$(dirname "$stale_file")
            local old_dir="$stale_dir/.old-$timestamp"
            mkdir -p "$old_dir"
            mv "$stale_file" "$old_dir/"
            echo_success "  [MOVED] $rel_path"
            ((moved_count++)) || true
        else
            echo_warning "  [STALE] $rel_path"
        fi
    done < <(find_stale_tests "$TESTS_DIR" "$SCRIPTS_DIR")

    if [ "$stale_count" -gt 0 ]; then
        echo ""
        if [ "$DO_MOVE" = "false" ]; then
            echo_info "To move stale files, run: $0 -m"
        else
            echo_success "Moved $moved_count stale test files"
        fi
    fi
}

########################################
# Placeholder Detection
########################################
is_placeholder_only() {
    local test_file="$1"
    local ext="${test_file##*.}"

    local content
    if grep -q "# Start of Source Code from:" "$test_file"; then
        content=$(sed -n '/# Start of Source Code from:/q;p' "$test_file")
    else
        content=$(cat "$test_file")
    fi

    if [ "$ext" = "sh" ]; then
        # Check for actual test functions (not just test_placeholder)
        if echo "$content" | grep -qE "^test_[a-z]" | grep -v "test_placeholder"; then
            return 1
        fi
    elif [ "$ext" = "py" ]; then
        # Check for actual test functions
        if echo "$content" | grep -qE "^\s*def test_" | grep -v "test_placeholder"; then
            return 1
        fi
    fi

    return 0
}

report_placeholder_tests() {
    local placeholder_count=0

    echo ""
    echo_header "Placeholder Test Files"
    echo ""

    # Check shell tests
    if [ -d "$TESTS_DIR/shell" ]; then
        while IFS= read -r test_file; do
            [[ "$test_file" =~ \.old ]] && continue
            if is_placeholder_only "$test_file"; then
                local rel="${test_file#"$TESTS_DIR"/}"
                echo_warning "  [PLACEHOLDER] $rel"
                ((placeholder_count++)) || true
            fi
        done < <(find "$TESTS_DIR/shell" -name "test_*.sh" 2>/dev/null)
    fi

    # Check python tests
    if [ -d "$TESTS_DIR/python" ]; then
        while IFS= read -r test_file; do
            [[ "$test_file" =~ \.old ]] && continue
            if is_placeholder_only "$test_file"; then
                local rel="${test_file#"$TESTS_DIR"/}"
                echo_warning "  [PLACEHOLDER] $rel"
                ((placeholder_count++)) || true
            fi
        done < <(find "$TESTS_DIR/python" -name "test_*.py" 2>/dev/null)
    fi

    echo ""
    if [ "$placeholder_count" -eq 0 ]; then
        echo_success "No placeholder-only test files found"
    else
        echo_info "Found $placeholder_count placeholder test files needing implementation"
    fi
}

########################################
# Main
########################################
main() {
    local start_time
    start_time=$(date +%s)

    echo ""
    echo_header "Test Synchronization for scitex-writer"
    echo ""
    echo_info "Scripts:   $SCRIPTS_DIR"
    echo_info "Tests:     $TESTS_DIR"
    echo_info "Jobs:      $PARALLEL_JOBS"
    echo ""

    construct_find_excludes

    # Create test directories mirroring scripts structure
    echo_info "Preparing test structure..."
    mkdir -p "$TESTS_DIR/shell"
    mkdir -p "$TESTS_DIR/python"

    # Process shell scripts
    echo_info "Synchronizing shell script tests..."
    local shell_count=0
    if [ -d "$SCRIPTS_DIR/shell" ]; then
        while IFS= read -r src_file; do
            process_shell_file "$src_file" "$SCRIPTS_DIR" "$TESTS_DIR"
            ((shell_count++)) || true
        done < <(find "$SCRIPTS_DIR/shell" -name "*.sh" "${FIND_EXCLUDES[@]}" -type f 2>/dev/null)
    fi
    echo_success "Processed $shell_count shell scripts"

    # Process Python scripts
    echo_info "Synchronizing Python script tests..."
    local python_count=0
    if [ -d "$SCRIPTS_DIR/python" ]; then
        while IFS= read -r src_file; do
            process_python_file "$src_file" "$SCRIPTS_DIR" "$TESTS_DIR"
            ((python_count++)) || true
        done < <(find "$SCRIPTS_DIR/python" -name "*.py" "${FIND_EXCLUDES[@]}" -type f 2>/dev/null)
    fi
    echo_success "Processed $python_count Python scripts"

    # Handle stale tests
    echo ""
    echo_header "Stale Test Files"
    echo ""
    move_stale_tests

    # Report placeholder tests
    report_placeholder_tests

    local end_time
    end_time=$(date +%s)
    local elapsed=$((end_time - start_time))

    echo ""
    echo_header "Summary"
    echo_success "Completed in ${elapsed}s"
    echo ""

    # Log tree structure
    { tree "$TESTS_DIR" || ls -laR "$TESTS_DIR"; } >>"$LOG_PATH" 2>&1
}

main "$@"
cd "$ORIG_DIR"

# EOF