codeflash/tests/scripts/end_to_end_test_utilities_js.py
2026-01-29 01:39:48 -08:00

178 lines
6.2 KiB
Python

"""End-to-end test utilities for JavaScript/TypeScript optimization testing.
Similar to end_to_end_test_utilities.py but adapted for JS/TS projects.
"""
import logging
import os
import pathlib
import re
import shutil
import subprocess
import time
from dataclasses import dataclass
from typing import Optional
@dataclass
class JSTestConfig:
"""Configuration for a JavaScript/TypeScript e2e test."""
# Path to the source file to optimize (relative to project root)
file_path: pathlib.Path
# Function name to optimize (optional - if not specified, optimizes all in file)
function_name: Optional[str] = None
# Minimum improvement multiplier (e.g., 0.5 = 50% faster)
min_improvement_x: float = 0.1
# Expected improvement percentage (optimization must exceed this)
expected_improvement_pct: int = 10
# Expected number of test files discovered
expected_test_files: Optional[int] = None
def clear_codeflash_directory(cwd: pathlib.Path) -> None:
"""Clear the .codeflash directory to avoid stale state."""
codeflash_dir = cwd / ".codeflash"
if codeflash_dir.exists():
shutil.rmtree(codeflash_dir)
def install_npm_dependencies(cwd: pathlib.Path) -> bool:
"""Install npm dependencies if needed."""
node_modules = cwd / "node_modules"
if not node_modules.exists():
logging.info(f"Installing npm dependencies in {cwd}")
result = subprocess.run(["npm", "install"], cwd=str(cwd), capture_output=True, text=True)
if result.returncode != 0:
logging.error(f"npm install failed: {result.stderr}")
return False
return True
def build_js_command(cwd: pathlib.Path, config: JSTestConfig) -> list[str]:
"""Build the codeflash CLI command for JS/TS optimization."""
# JS projects are at code_to_optimize/js/code_to_optimize_*, which is 3 levels deep
# So we need ../../../codeflash/main.py to get to the root
python_path = "../../../codeflash/main.py"
base_command = ["uv", "run", "--no-project", python_path, "--file", str(config.file_path), "--no-pr"]
if config.function_name:
base_command.extend(["--function", config.function_name])
return base_command
def validate_js_output(stdout: str, return_code: int, config: JSTestConfig) -> bool:
"""Validate the output of a JS/TS optimization run."""
if return_code != 0:
logging.error(f"Command returned exit code {return_code} instead of 0")
return False
if "⚡️ Optimization successful! 📄 " not in stdout:
logging.error("Failed to find performance improvement message")
return False
improvement_match = re.search(r"📈 ([\d,]+)% (?:(\w+) )?improvement", stdout)
if not improvement_match:
logging.error("Could not find improvement percentage in output")
return False
improvement_pct = int(improvement_match.group(1).replace(",", ""))
improvement_x = float(improvement_pct) / 100
logging.info(f"Performance improvement: {improvement_pct}%; Rate: {improvement_x}x")
if improvement_pct <= config.expected_improvement_pct:
logging.error(f"Performance improvement {improvement_pct}% not above {config.expected_improvement_pct}%")
return False
if improvement_x <= config.min_improvement_x:
logging.error(f"Performance improvement rate {improvement_x}x not above {config.min_improvement_x}x")
return False
if config.expected_test_files is not None:
# Look for "Instrumented X existing unit test files" (the actual file count)
test_files_match = re.search(r"Instrumented (\d+) existing unit test files?", stdout)
if not test_files_match:
logging.error("Could not find unit test file count in output")
return False
num_test_files = int(test_files_match.group(1))
if num_test_files < config.expected_test_files:
logging.error(f"Expected at least {config.expected_test_files} test files, found {num_test_files}")
return False
logging.info(f"Success: Performance improvement is {improvement_pct}%")
return True
def run_js_codeflash_command(cwd: pathlib.Path, config: JSTestConfig) -> bool:
"""Run codeflash optimization on a JavaScript/TypeScript project."""
logging.basicConfig(level=logging.INFO)
# Save original file contents for potential revert
path_to_file = cwd / config.file_path
file_contents = path_to_file.read_text("utf-8")
# Clear any stale state
clear_codeflash_directory(cwd)
# Install dependencies if needed
if not install_npm_dependencies(cwd):
return False
# Build and run command
command = build_js_command(cwd, config)
env = os.environ.copy()
env["PYTHONIOENCODING"] = "utf-8"
logging.info(f"Running: {' '.join(command)}")
process = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=str(cwd), env=env, encoding="utf-8"
)
output = []
for line in process.stdout:
logging.info(line.strip())
output.append(line)
return_code = process.wait()
stdout = "".join(output)
validated = validate_js_output(stdout, return_code, config)
if not validated:
# Revert file changes on failure
path_to_file.write_text(file_contents, "utf-8")
logging.info("Codeflash run did not meet expected requirements, reverting file changes.")
return False
return validated
def run_with_retries(test_func, *args, **kwargs) -> int:
"""Run a test function with retries on failure."""
max_retries = int(os.getenv("MAX_RETRIES", 3))
retry_delay = int(os.getenv("RETRY_DELAY", 5))
log = logging.getLogger()
log.setLevel(logging.DEBUG)
for attempt in range(1, max_retries + 1):
logging.info(f"\n=== Attempt {attempt} of {max_retries} ===")
if test_func(*args, **kwargs):
logging.info(f"Test passed on attempt {attempt}")
return 0
logging.error(f"Test failed on attempt {attempt}")
if attempt < max_retries:
logging.info(f"Retrying in {retry_delay} seconds...")
time.sleep(retry_delay)
else:
logging.error("Test failed after all retries")
return 1
return 1