Files
cobol-java-v3/scripts/test_agent_mode.py
T

580 lines
18 KiB
Python

# ============================================================================
# test_agent_mode.py - Unified Automated Test for feat/agent-mode
# ============================================================================
#
# Single-command execution that:
# 1. Validates environment (Python, cobc, Java, jcl-cobol-data-create path)
# 2. Fixes JCL_ROOT in orchestrator_jcl.py if needed
# 3. Installs Python dependencies if missing
# 4. Runs unit tests (pytest)
# 5. Runs dry-run validation
# 6. Runs full agent mode pipeline with ZAN04MAT
#
# Usage:
# python scripts/test_agent_mode.py
# python scripts/test_agent_mode.py --skip-tests
# python scripts/test_agent_mode.py --skip-pipeline
# python scripts/test_agent_mode.py --dry-run-only
#
# ============================================================================
import argparse
import os
import re
import shutil
import subprocess
import sys
import time
from pathlib import Path
# ============================================================================
# Constants
# ============================================================================
PROJECT_ROOT = Path(__file__).resolve().parent.parent
JCL_ROOT_CANDIDATES = [
Path(r"C:\Users\marye\Desktop\2026技术大赛\jcl-cobol-data-create"),
Path(r"D:\jcl-cobol-data-create"),
]
COBOL_PROJECT = Path(r"C:\Users\marye\Desktop\2026技术大赛\cobol-tna-system")
PROGRAM = "ZAN04MAT"
TEST_INPUTS = {
"design": COBOL_PROJECT / "詳細設計書" / f"詳細設計書_{PROGRAM}.md",
"cobol-src": COBOL_PROJECT / "src" / f"{PROGRAM}.cbl",
"file-db-md": COBOL_PROJECT / "詳細設計書" / "COPY句定義書.md",
"cpy": COBOL_PROJECT / "cpy",
"db-md": COBOL_PROJECT / "詳細設計書" / "DB定義書.md",
}
DEFAULT_API_KEY = "sk-6156cccdc9c14d949cf5bfc5afc67a03"
DEFAULT_API_MODEL = "deepseek-v4-flash"
ORCHESTRATOR_JCL = PROJECT_ROOT / "orchestrator_jcl.py"
# ============================================================================
# Helpers
# ============================================================================
class Colors:
RESET = "\033[0m"
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
CYAN = "\033[96m"
BOLD = "\033[1m"
def header(text):
print(f"\n{Colors.CYAN}{Colors.BOLD}{'=' * 70}")
print(f" {text}")
print(f"{'=' * 70}{Colors.RESET}\n")
def step(text):
print(f" {Colors.GREEN}[STEP]{Colors.RESET} {text}")
def info(text):
print(f" {Colors.BLUE}[INFO]{Colors.RESET} {text}")
def warn(text):
print(f" {Colors.YELLOW}[WARN]{Colors.RESET} {text}")
def fail(text):
print(f" {Colors.RED}[FAIL]{Colors.RESET} {text}")
def ok(text):
print(f" {Colors.GREEN}[ OK ]{Colors.RESET} {text}")
def run_cmd(cmd, cwd=None, env=None, timeout=120, capture=True):
return subprocess.run(
cmd,
cwd=cwd or str(PROJECT_ROOT),
env=env,
timeout=timeout,
capture_output=capture,
text=True,
encoding="utf-8",
errors="replace",
)
# ============================================================================
# Phase 1: Environment Check
# ============================================================================
def check_environment():
header("Phase 1: Environment Check")
all_ok = True
# --- Python version ---
step("Python version")
ver = sys.version_info
if ver >= (3, 9):
ok(f"Python {ver.major}.{ver.minor}.{ver.micro}")
else:
fail(f"Python {ver.major}.{ver.minor}.{ver.micro} (need >= 3.9)")
all_ok = False
# --- cobc (GnuCOBOL) ---
step("GnuCOBOL (cobc)")
cobc_path = shutil.which("cobc")
if cobc_path:
ok(f"cobc: {cobc_path}")
try:
r = run_cmd(["cobc", "--version"], timeout=10)
first_line = (r.stdout or r.stderr or "").strip().splitlines()[:1]
if first_line:
info(first_line[0])
except Exception:
pass
else:
fail("cobc not found in PATH")
all_ok = False
# --- Java ---
step("Java")
java_path = shutil.which("java")
if java_path:
ok(f"java: {java_path}")
else:
warn("java not found in PATH (COBOL-only mode, Java comparison skipped)")
# --- jcl-cobol-data-create ---
step("jcl-cobol-data-create")
jcl_root = _find_jcl_root()
if jcl_root:
ok(f"Found at: {jcl_root}")
else:
fail("jcl-cobol-data-create not found in any candidate location")
all_ok = False
# --- Test input files ---
step("Test input files for ZAN04MAT")
for label, path in TEST_INPUTS.items():
if path.exists():
ok(f"{label}: {path}")
else:
fail(f"{label}: {path} (NOT FOUND)")
all_ok = False
# --- Project structure ---
step("Project structure")
for d in ["cobol_testgen", "runners", "comparator", "agents", "tests", "config"]:
p = PROJECT_ROOT / d
if p.exists():
ok(f"{d}/")
else:
fail(f"{d}/ (missing)")
all_ok = False
return all_ok
def _find_jcl_root():
for candidate in JCL_ROOT_CANDIDATES:
if candidate.exists() and (candidate / "agent" / "__init__.py").exists():
return candidate
return None
# ============================================================================
# Phase 2: Fix JCL_ROOT Path
# ============================================================================
def fix_jcl_root():
header("Phase 2: Fix JCL_ROOT in orchestrator_jcl.py")
if not ORCHESTRATOR_JCL.exists():
fail(f"orchestrator_jcl.py not found at {ORCHESTRATOR_JCL}")
return False
content = ORCHESTRATOR_JCL.read_text(encoding="utf-8")
# Find current JCL_ROOT value
match = re.search(r'^JCL_ROOT\s*=\s*r?"([^"]*)"', content, re.MULTILINE)
if not match:
warn("Could not parse JCL_ROOT from orchestrator_jcl.py")
return False
current_path_str = match.group(1)
current_path = Path(current_path_str)
if current_path.exists() and (current_path / "agent" / "__init__.py").exists():
ok(f"JCL_ROOT already correct: {current_path}")
return True
# Find the actual path
actual_root = _find_jcl_root()
if not actual_root:
fail("Cannot find jcl-cobol-data-create to fix the path")
return False
actual_str = str(actual_root).replace("\\", "\\\\")
new_line = f'JCL_ROOT = r"{actual_str}"'
# Replace in content
new_content = re.sub(
r'^JCL_ROOT\s*=.*$',
new_line,
content,
count=1,
flags=re.MULTILINE,
)
ORCHESTRATOR_JCL.write_text(new_content, encoding="utf-8")
ok(f"Fixed JCL_ROOT: {current_path_str} -> {actual_str}")
return True
# ============================================================================
# Phase 3: Install Dependencies
# ============================================================================
def install_dependencies():
header("Phase 3: Install Python Dependencies")
req_main = PROJECT_ROOT / "requirements.txt"
req_jcl = _find_jcl_root()
if req_jcl:
req_jcl = req_jcl / "requirements.txt"
# Check what's installed
step("Checking installed packages")
required = {
"httpx": "httpx",
"pyyaml": "pyyaml",
"pytest": "pytest",
"fastapi": "fastapi",
"uvicorn": "uvicorn",
"lark": "lark",
"requests": "requests",
}
missing = []
for pkg, import_name in required.items():
try:
__import__(import_name)
ok(f"{pkg}")
except ImportError:
warn(f"{pkg} -- MISSING")
missing.append(pkg)
if missing:
step(f"Installing missing packages: {', '.join(missing)}")
# Install from requirements.txt
if req_main.exists():
r = run_cmd(
[sys.executable, "-m", "pip", "install", "-r", str(req_main)],
timeout=120,
)
if r.returncode == 0:
ok("Main requirements installed")
else:
warn(f"pip install had issues: {(r.stderr or '')[:200]}")
# Install lark separately if missing (not in requirements.txt)
if "lark" in missing:
r = run_cmd(
[sys.executable, "-m", "pip", "install", "lark>=1.1.0"],
timeout=60,
)
if r.returncode == 0:
ok("lark installed")
# Install jcl-cobol-data-create requirements
if req_jcl and req_jcl.exists():
r = run_cmd(
[sys.executable, "-m", "pip", "install", "-r", str(req_jcl)],
timeout=60,
)
if r.returncode == 0:
ok("jcl-cobol-data-create requirements installed")
# Verify again
step("Verifying installation")
still_missing = []
for pkg, import_name in required.items():
try:
__import__(import_name)
ok(f"{pkg}")
except ImportError:
fail(f"{pkg} still missing")
still_missing.append(pkg)
if still_missing:
fail(f"Could not install: {', '.join(still_missing)}")
return False
else:
ok("All required packages are installed")
return True
# ============================================================================
# Phase 4: Run Unit Tests (pytest)
# ============================================================================
def run_unit_tests():
header("Phase 4: Unit Tests (pytest)")
step("Running pytest...")
r = run_cmd(
[sys.executable, "-m", "pytest", "tests/", "-v", "--tb=short", "-x",
"--ignore=tests/test_biz_e2e.py", "--ignore=tests/test_web_e2e.py"],
timeout=300,
)
print(r.stdout[-3000:] if len(r.stdout) > 3000 else r.stdout)
if r.returncode == 0:
ok("All unit tests passed")
return True
else:
# Count pass/fail from output
output = r.stdout + (r.stderr or "")
pass_count = output.count(" PASSED")
fail_count = output.count(" FAILED")
error_count = output.count(" ERROR")
warn(f"Results: {pass_count} passed, {fail_count} failed, {error_count} errors")
if fail_count <= 3:
warn("Minor failures detected -- continuing with pipeline")
return True
else:
fail("Too many test failures")
return False
# ============================================================================
# Phase 5: Dry-Run Validation
# ============================================================================
def run_dry_run():
header("Phase 5: Dry-Run Validation")
step("Checking all inputs via main.py --dry-run...")
r = run_cmd(
[
sys.executable, "main.py",
"--mode", "agent",
"--dry-run",
"--design", str(TEST_INPUTS["design"]),
"--cobol-src", str(TEST_INPUTS["cobol-src"]),
"--file-db-md", str(TEST_INPUTS["file-db-md"]),
"--cpy", str(TEST_INPUTS["cpy"]),
"--db-md", str(TEST_INPUTS["db-md"]),
],
timeout=30,
)
output = (r.stdout or "") + "\n" + (r.stderr or "")
print(output)
if "DRY-RUN: all inputs OK" in output or r.returncode == 0:
ok("Dry-run validation passed")
return True
else:
fail("Dry-run validation failed")
return False
# ============================================================================
# Phase 6: Full Agent Mode Pipeline
# ============================================================================
def run_full_pipeline():
header("Phase 6: Full Agent Mode Pipeline (ZAN04MAT)")
output_dir = PROJECT_ROOT / "output" / "test_agent_mode"
if output_dir.exists():
shutil.rmtree(str(output_dir))
output_dir.mkdir(parents=True, exist_ok=True)
step(f"Output directory: {output_dir}")
step(f"Program: {PROGRAM}")
step(f"API model: {DEFAULT_API_MODEL}")
info("DeepSeek API calls will be made -- ensure network connectivity")
t0 = time.time()
cmd = [
sys.executable, "main.py",
"--mode", "agent",
"--design", str(TEST_INPUTS["design"]),
"--cobol-src", str(TEST_INPUTS["cobol-src"]),
"--file-db-md", str(TEST_INPUTS["file-db-md"]),
"--cpy", str(TEST_INPUTS["cpy"]),
"--db-md", str(TEST_INPUTS["db-md"]),
"--output", str(output_dir),
"--api-key", DEFAULT_API_KEY,
"--api-model", DEFAULT_API_MODEL,
"--verbose",
]
step("Executing pipeline (this may take several minutes)...")
info(f"Command: {' '.join(cmd)}")
try:
# Use a larger timeout since API calls + compilation can be slow
r = run_cmd(cmd, timeout=600)
elapsed = time.time() - t0
print(r.stdout[-5000:] if len(r.stdout) > 5000 else r.stdout)
if r.stderr:
print(r.stderr[-2000:] if len(r.stderr) > 2000 else r.stderr)
info(f"Pipeline completed in {elapsed:.0f}s")
# Check results
if r.returncode == 0:
ok("Pipeline completed successfully")
elif r.returncode == 2:
warn("Pipeline completed with BLOCKED status (compile or input issue)")
elif r.returncode == 3:
warn("Pipeline completed with ERROR status")
else:
warn(f"Pipeline returned exit code {r.returncode}")
# Check output files
step("Checking output artifacts")
_check_output_artifacts(output_dir)
# Check reports
reports_dir = PROJECT_ROOT / "reports" / PROGRAM
if reports_dir.exists():
step("Reports generated")
for f in reports_dir.rglob("*"):
if f.is_file():
info(f" {f.relative_to(PROJECT_ROOT)}")
else:
info("No reports directory found (reports may be in output dir)")
return True
except subprocess.TimeoutExpired:
elapsed = time.time() - t0
fail(f"Pipeline timed out after {elapsed:.0f}s")
return False
except Exception as e:
fail(f"Pipeline error: {e}")
return False
def _check_output_artifacts(output_dir):
prog_dir = output_dir / PROGRAM
if not prog_dir.exists():
info(f"No output for {PROGRAM} at {prog_dir}")
return
for item in sorted(prog_dir.iterdir()):
if item.is_dir():
info(f" {item.name}/")
for f in item.rglob("*"):
if f.is_file():
size = f.stat().st_size
info(f" {f.name} ({size} bytes)")
else:
info(f" {item.name} ({item.stat().st_size} bytes)")
# ============================================================================
# Main
# ============================================================================
def main():
parser = argparse.ArgumentParser(
description="Unified automated test for feat/agent-mode",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--skip-tests", action="store_true",
help="Skip unit tests (pytest)")
parser.add_argument("--skip-pipeline", action="store_true",
help="Skip full agent mode pipeline")
parser.add_argument("--dry-run-only", action="store_true",
help="Only check environment and dry-run")
parser.add_argument("--api-key", default=DEFAULT_API_KEY,
help="DeepSeek API key")
parser.add_argument("--api-model", default=DEFAULT_API_MODEL,
help="API model name")
args = parser.parse_args()
global DEFAULT_API_KEY, DEFAULT_API_MODEL
DEFAULT_API_KEY = args.api_key
DEFAULT_API_MODEL = args.api_model
# Make sure we're in the project root
os.chdir(str(PROJECT_ROOT))
print(f"\n{Colors.BOLD}{'#' * 70}")
print(f"# COBOL-Java Agent Mode -- Unified Test Script")
print(f"# Branch: feat/agent-mode")
print(f"# Program: {PROGRAM}")
print(f"# Project: {PROJECT_ROOT}")
print(f"{'#' * 70}{Colors.RESET}\n")
results = {}
t_start = time.time()
# Phase 1: Environment
results["env"] = check_environment()
if not results["env"]:
fail("\nEnvironment check failed. Please fix the issues above and retry.")
sys.exit(1)
# Phase 2: Fix JCL_ROOT
results["jcl_root"] = fix_jcl_root()
# Phase 3: Dependencies
results["deps"] = install_dependencies()
if not results["deps"]:
fail("\nDependency installation failed.")
sys.exit(1)
# Phase 4: Unit tests
if not args.skip_tests and not args.dry_run_only:
results["tests"] = run_unit_tests()
else:
info("\nSkipping unit tests (--skip-tests or --dry-run-only)")
# Phase 5: Dry-run
results["dry_run"] = run_dry_run()
# Phase 6: Full pipeline
if not args.skip_pipeline and not args.dry_run_only:
results["pipeline"] = run_full_pipeline()
else:
info("\nSkipping full pipeline (--skip-pipeline or --dry-run-only)")
# Summary
elapsed_total = time.time() - t_start
header("Summary")
for phase, passed in results.items():
status = f"{Colors.GREEN}PASS{Colors.RESET}" if passed else f"{Colors.RED}FAIL{Colors.RESET}"
print(f" {phase:<15} {status}")
print(f"\n Total time: {elapsed_total:.0f}s")
print(f" Output dir: {PROJECT_ROOT / 'output' / 'test_agent_mode'}")
print(f" Reports: {PROJECT_ROOT / 'reports' / PROGRAM}")
print()
all_pass = all(results.values())
if all_pass:
ok("All phases completed successfully!")
else:
failed_phases = [p for p, v in results.items() if not v]
warn(f"Failed phases: {', '.join(failed_phases)}")
sys.exit(0 if all_pass else 1)
if __name__ == "__main__":
main()