#!/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