178 lines
6.2 KiB
Python
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
|