some fixes for test runner and instrumentation

This commit is contained in:
ali 2026-02-11 20:27:02 +02:00
parent 6c23255bca
commit dcd9e2a502
No known key found for this signature in database
GPG key ID: 44F9B42770617B9B
9 changed files with 393 additions and 102 deletions

View file

@ -7,7 +7,7 @@
* @param {number[]} arr - The array to sort
* @returns {number[]} - The sorted array
*/
export function bubbleSort(arr) {
function bubbleSort(arr) {
const result = arr.slice();
const n = result.length;
@ -29,7 +29,7 @@ export function bubbleSort(arr) {
* @param {number[]} arr - The array to sort
* @returns {number[]} - The sorted array in descending order
*/
export function bubbleSortDescending(arr) {
function bubbleSortDescending(arr) {
const n = arr.length;
const result = [...arr];

View file

@ -37,21 +37,6 @@ def is_glob_pattern(path_str: str) -> bool:
def normalize_ignore_paths(paths: list[str], base_path: Path | None = None) -> list[Path]:
"""Normalize ignore paths, expanding glob patterns and resolving paths.
Accepts a list of path strings that can be either:
- Literal paths (relative or absolute): e.g., "node_modules", "/absolute/path"
- Glob patterns: e.g., "**/*.test.js", "dist/*", "*.log"
Args:
paths: List of path strings (literal paths or glob patterns).
base_path: Base path for resolving relative paths and patterns.
If None, uses current working directory.
Returns:
List of resolved Path objects, deduplicated.
"""
if base_path is None:
base_path = Path.cwd()
@ -59,22 +44,25 @@ def normalize_ignore_paths(paths: list[str], base_path: Path | None = None) -> l
normalized: set[Path] = set()
for path_str in paths:
if not path_str:
continue
path_str = str(path_str)
if is_glob_pattern(path_str):
# It's a glob pattern - expand it
# Use base_path as the root for glob expansion
pattern_path = base_path / path_str
# glob returns an iterator of matching paths
# pathlib requires relative glob patterns
path_str = path_str.removeprefix("./")
if path_str.startswith("/"):
path_str = path_str.lstrip("/")
for matched_path in base_path.glob(path_str):
if matched_path.exists():
normalized.add(matched_path.resolve())
normalized.add(matched_path.resolve())
else:
# It's a literal path
path_obj = Path(path_str)
if not path_obj.is_absolute():
path_obj = base_path / path_obj
if path_obj.exists():
normalized.add(path_obj.resolve())
# Silently skip non-existent literal paths (e.g., .next, dist before build)
return list(normalized)

View file

@ -82,6 +82,11 @@ class StandaloneCallTransformer:
# Captures: (whitespace)(await )?(object.)*func_name(
# We'll filter out expect() and codeflash. cases in the transform loop
self._call_pattern = re.compile(rf"(\s*)(await\s+)?((?:\w+\.)*){re.escape(self.func_name)}\s*\(")
# Pattern to match bracket notation: obj['func_name']( or obj["func_name"](
# Captures: (whitespace)(await )?(obj)['|"]func_name['|"](
self._bracket_call_pattern = re.compile(
rf"(\s*)(await\s+)?(\w+)\[['\"]({re.escape(self.func_name)})['\"]]\s*\("
)
def transform(self, code: str) -> str:
"""Transform all standalone calls in the code."""
@ -89,7 +94,25 @@ class StandaloneCallTransformer:
pos = 0
while pos < len(code):
match = self._call_pattern.search(code, pos)
# Try both dot notation and bracket notation patterns
dot_match = self._call_pattern.search(code, pos)
bracket_match = self._bracket_call_pattern.search(code, pos)
# Choose the first match (by position)
match = None
is_bracket_notation = False
if dot_match and bracket_match:
if dot_match.start() <= bracket_match.start():
match = dot_match
else:
match = bracket_match
is_bracket_notation = True
elif dot_match:
match = dot_match
elif bracket_match:
match = bracket_match
is_bracket_notation = True
if not match:
result.append(code[pos:])
break
@ -106,7 +129,11 @@ class StandaloneCallTransformer:
result.append(code[pos:match_start])
# Try to parse the full standalone call
standalone_match = self._parse_standalone_call(code, match)
if is_bracket_notation:
standalone_match = self._parse_bracket_standalone_call(code, match)
else:
standalone_match = self._parse_standalone_call(code, match)
if standalone_match is None:
# Couldn't parse, skip this match
result.append(code[match_start : match.end()])
@ -115,7 +142,7 @@ class StandaloneCallTransformer:
# Generate the transformed code
self.invocation_counter += 1
transformed = self._generate_transformed_call(standalone_match)
transformed = self._generate_transformed_call(standalone_match, is_bracket_notation)
result.append(transformed)
pos = standalone_match.end_pos
@ -276,17 +303,59 @@ class StandaloneCallTransformer:
return code[open_paren_pos + 1 : pos - 1], pos
def _generate_transformed_call(self, match: StandaloneCallMatch) -> str:
def _parse_bracket_standalone_call(self, code: str, match: re.Match) -> StandaloneCallMatch | None:
"""Parse a complete standalone obj['func'](...) call with bracket notation."""
leading_ws = match.group(1)
prefix = match.group(2) or "" # "await " or ""
obj_name = match.group(3) # The object name before bracket
# match.group(4) is the function name inside brackets
# Find the opening paren position
match_text = match.group(0)
paren_offset = match_text.rfind("(")
open_paren_pos = match.start() + paren_offset
# Find the arguments (content inside parens)
func_args, close_pos = self._find_balanced_parens(code, open_paren_pos)
if func_args is None:
return None
# Check for trailing semicolon
end_pos = close_pos
# Skip whitespace
while end_pos < len(code) and code[end_pos] in " \t":
end_pos += 1
has_trailing_semicolon = end_pos < len(code) and code[end_pos] == ";"
if has_trailing_semicolon:
end_pos += 1
return StandaloneCallMatch(
start_pos=match.start(),
end_pos=end_pos,
leading_whitespace=leading_ws,
func_args=func_args,
prefix=prefix,
object_prefix=f"{obj_name}.", # Use dot notation format for consistency
has_trailing_semicolon=has_trailing_semicolon,
)
def _generate_transformed_call(self, match: StandaloneCallMatch, is_bracket_notation: bool = False) -> str:
"""Generate the transformed code for a standalone call."""
line_id = str(self.invocation_counter)
args_str = match.func_args.strip()
semicolon = ";" if match.has_trailing_semicolon else ""
# Handle method calls on objects (e.g., calc.fibonacci, this.method)
# Handle method calls on objects (e.g., calc.fibonacci, this.method, instance['method'])
if match.object_prefix:
# Remove trailing dot from object prefix for the bind call
obj = match.object_prefix.rstrip(".")
full_method = f"{obj}.{self.func_name}"
# For bracket notation, use bracket access syntax for the bind
if is_bracket_notation:
full_method = f"{obj}['{self.func_name}']"
else:
full_method = f"{obj}.{self.func_name}"
if args_str:
return (

View file

@ -100,23 +100,40 @@ def detect_module_system(project_root: Path, file_path: Path | None = None) -> s
try:
content = file_path.read_text()
# Look for ES module syntax
# Look for ES module syntax - these are explicit ESM markers
has_import = "import " in content and "from " in content
has_export = "export " in content or "export default" in content or "export {" in content
# Check for export function/class/const/default which are unambiguous ESM syntax
has_esm_export = (
"export function " in content
or "export class " in content
or "export const " in content
or "export let " in content
or "export default " in content
or "export async function " in content
)
has_export_block = "export {" in content
# Look for CommonJS syntax
has_require = "require(" in content
has_module_exports = "module.exports" in content or "exports." in content
# Determine based on what we found
if (has_import or has_export) and not (has_require or has_module_exports):
logger.debug("Detected ES Module from import/export statements")
# Prioritize ESM when explicit ESM export syntax is found
# This handles hybrid files that have both `export function` and `module.exports`
# The ESM syntax is more explicit and should take precedence
if has_esm_export or has_import:
logger.debug("Detected ES Module from explicit export/import statements")
return ModuleSystem.ES_MODULE
if (has_require or has_module_exports) and not (has_import or has_export):
# Pure CommonJS
if (has_require or has_module_exports) and not has_export_block:
logger.debug("Detected CommonJS from require/module.exports")
return ModuleSystem.COMMONJS
# Export block without other ESM markers - still ESM
if has_export_block:
logger.debug("Detected ES Module from export block")
return ModuleSystem.ES_MODULE
except Exception as e:
logger.warning("Failed to analyze file %s: %s", file_path, e)

View file

@ -185,14 +185,20 @@ def parse_jest_test_xml(
# Extract console output from suite-level system-out (Jest specific)
suite_stdout = _extract_jest_console_output(suite._elem) # noqa: SLF001
# Fallback: use subprocess stdout if XML system-out is empty
if not suite_stdout and global_stdout:
suite_stdout = global_stdout
# Combine suite stdout with global stdout to ensure we capture all timing markers
# Jest-junit may not capture all console.log output in the XML, so we also need
# to check the subprocess stdout directly for timing markers
combined_stdout = suite_stdout
if global_stdout:
if combined_stdout:
combined_stdout = combined_stdout + "\n" + global_stdout
else:
combined_stdout = global_stdout
# Parse timing markers from the suite's console output
start_matches = list(jest_start_pattern.finditer(suite_stdout))
# Parse timing markers from the combined console output
start_matches = list(jest_start_pattern.finditer(combined_stdout))
end_matches_dict = {}
for match in jest_end_pattern.finditer(suite_stdout):
for match in jest_end_pattern.finditer(combined_stdout):
# Key: (testName, testName2, funcName, loopIndex, lineId)
key = match.groups()[:5]
end_matches_dict[key] = match

View file

@ -7,6 +7,7 @@ verification and performance benchmarking.
from __future__ import annotations
import json
import os
import subprocess
import time
from pathlib import Path
@ -21,6 +22,25 @@ from codeflash.code_utils.shell_utils import get_cross_platform_subprocess_run_a
if TYPE_CHECKING:
from codeflash.models.models import TestFiles
# Track created config files (jest configs and tsconfigs) for cleanup
_created_config_files: set[Path] = set()
def get_created_config_files() -> list[Path]:
"""Get list of config files created by codeflash for cleanup.
Returns:
List of paths to created config files (jest.codeflash.config.js, tsconfig.codeflash.json)
that should be cleaned up after optimization.
"""
return list(_created_config_files)
def clear_created_config_files() -> None:
"""Clear the set of tracked config files after cleanup."""
_created_config_files.clear()
def _detect_bundler_module_resolution(project_root: Path) -> bool:
"""Detect if the project uses moduleResolution: 'bundler' in tsconfig.
@ -163,6 +183,7 @@ def _create_codeflash_tsconfig(project_root: Path) -> Path:
try:
codeflash_tsconfig_path.write_text(json.dumps(codeflash_tsconfig, indent=2))
_created_config_files.add(codeflash_tsconfig_path)
logger.debug(f"Created {codeflash_tsconfig_path} with Node moduleResolution")
except Exception as e:
logger.warning(f"Failed to create codeflash tsconfig: {e}")
@ -170,70 +191,142 @@ def _create_codeflash_tsconfig(project_root: Path) -> Path:
return codeflash_tsconfig_path
def _create_codeflash_jest_config(project_root: Path, original_jest_config: Path | None) -> Path | None:
"""Create a Jest config that uses the codeflash tsconfig for ts-jest.
def _has_ts_jest_dependency(project_root: Path) -> bool:
"""Check if the project has ts-jest as a dependency.
Args:
project_root: Root of the project.
Returns:
True if ts-jest is found in dependencies or devDependencies.
"""
package_json = project_root / "package.json"
if not package_json.exists():
return False
try:
content = json.loads(package_json.read_text())
deps = {**content.get("dependencies", {}), **content.get("devDependencies", {})}
return "ts-jest" in deps
except (json.JSONDecodeError, OSError):
return False
def _create_codeflash_jest_config(
project_root: Path, original_jest_config: Path | None, *, for_esm: bool = False
) -> Path | None:
"""Create a Jest config that handles ESM packages and TypeScript properly.
Args:
project_root: Root of the project.
original_jest_config: Path to the original Jest config, or None.
for_esm: If True, configure for ESM package transformation.
Returns:
Path to the codeflash Jest config, or None if creation failed.
"""
codeflash_jest_config_path = project_root / "jest.codeflash.config.js"
# For ESM projects (type: module), use .cjs extension since config uses CommonJS require/module.exports
# This prevents "ReferenceError: module is not defined" errors
is_esm = _is_esm_project(project_root)
config_ext = ".cjs" if is_esm else ".js"
# If it already exists, use it
# Create codeflash config in the same directory as the original config
# This ensures relative paths work correctly
if original_jest_config:
codeflash_jest_config_path = original_jest_config.parent / f"jest.codeflash.config{config_ext}"
else:
codeflash_jest_config_path = project_root / f"jest.codeflash.config{config_ext}"
# If it already exists, use it (check both extensions)
if codeflash_jest_config_path.exists():
logger.debug(f"Using existing {codeflash_jest_config_path}")
return codeflash_jest_config_path
# Create a wrapper Jest config that uses tsconfig.codeflash.json
if original_jest_config:
# Extend the original config
jest_config_content = f"""// Auto-generated by codeflash for bundler moduleResolution compatibility
const originalConfig = require('./{original_jest_config.name}');
# Also check if the alternate extension exists
alt_ext = ".js" if is_esm else ".cjs"
alt_path = codeflash_jest_config_path.with_suffix(alt_ext)
if alt_path.exists():
logger.debug(f"Using existing {alt_path}")
return alt_path
const tsJestOptions = {{
isolatedModules: true,
tsconfig: 'tsconfig.codeflash.json',
}};
# Common ESM-only packages that need to be transformed
# These packages ship only ESM and will cause "Cannot use import statement" errors
esm_packages = [
"p-queue",
"p-limit",
"p-timeout",
"yocto-queue",
"eventemitter3",
"chalk",
"ora",
"strip-ansi",
"ansi-regex",
"string-width",
"wrap-ansi",
"is-unicode-supported",
"is-interactive",
"log-symbols",
"figures",
]
esm_pattern = "|".join(esm_packages)
# Check if ts-jest is available in the project
has_ts_jest = _has_ts_jest_dependency(project_root)
# Build transform config only if ts-jest is available
if has_ts_jest:
transform_config = """
// Ensure TypeScript files are transformed using ts-jest
transform: {
'^.+\\\\.(ts|tsx)$': ['ts-jest', { isolatedModules: true }],
// Use ts-jest for JS files in ESM packages too
'^.+\\\\.js$': ['ts-jest', { isolatedModules: true }],
},"""
else:
transform_config = ""
logger.debug("ts-jest not found in project dependencies, skipping transform config")
# Create a wrapper Jest config
if original_jest_config:
# Since codeflash config is in the same directory as original, use simple relative path
config_require_path = f"./{original_jest_config.name}"
# Extend the original config
jest_config_content = f"""// Auto-generated by codeflash for ESM compatibility
const originalConfig = require('{config_require_path}');
module.exports = {{
...originalConfig,
transform: {{
...originalConfig.transform,
'^.+\\\\.tsx?$': ['ts-jest', tsJestOptions],
}},
globals: {{
...originalConfig.globals,
'ts-jest': tsJestOptions,
}},
// Transform ESM packages that don't work with Jest's default config
// Pattern handles both npm/yarn (node_modules/pkg) and pnpm (node_modules/.pnpm/pkg@version/node_modules/pkg)
transformIgnorePatterns: [
'node_modules/(?!(\\\\.pnpm/)?({esm_pattern}))',
],{transform_config}
}};
"""
else:
# Create a minimal Jest config for TypeScript
jest_config_content = """// Auto-generated by codeflash for bundler moduleResolution compatibility
const tsJestOptions = {
isolatedModules: true,
tsconfig: 'tsconfig.codeflash.json',
};
module.exports = {
# Create a minimal Jest config for TypeScript with ESM support
jest_config_content = f"""// Auto-generated by codeflash for ESM compatibility
module.exports = {{
verbose: true,
testEnvironment: 'node',
testRegex: '\\\\.(test|spec)\\\\.(js|ts|tsx)$',
testPathIgnorePatterns: ['/dist/', '/node_modules/'],
transform: {
'^.+\\\\.tsx?$': ['ts-jest', tsJestOptions],
},
testPathIgnorePatterns: ['/dist/'],
// Transform ESM packages that don't work with Jest's default config
// Pattern handles both npm/yarn and pnpm directory structures
transformIgnorePatterns: [
'node_modules/(?!(\\\\.pnpm/)?({esm_pattern}))',
],{transform_config}
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};
}};
"""
try:
codeflash_jest_config_path.write_text(jest_config_content)
logger.debug(f"Created {codeflash_jest_config_path} with codeflash tsconfig")
_created_config_files.add(codeflash_jest_config_path)
logger.debug(f"Created {codeflash_jest_config_path} with ESM package support")
return codeflash_jest_config_path
except Exception as e:
logger.warning(f"Failed to create codeflash Jest config: {e}")
@ -243,9 +336,10 @@ module.exports = {
def _get_jest_config_for_project(project_root: Path) -> Path | None:
"""Get the appropriate Jest config for the project.
If the project uses bundler moduleResolution, creates and returns a
codeflash-compatible Jest config. Otherwise, returns the project's
existing Jest config.
Creates a codeflash-compatible Jest config that handles:
- ESM packages in node_modules that need transformation
- TypeScript files with proper ts-jest configuration
- bundler moduleResolution compatibility
Args:
project_root: Root of the project.
@ -262,10 +356,12 @@ def _get_jest_config_for_project(project_root: Path) -> Path | None:
logger.info("Detected bundler moduleResolution - creating compatible config")
# Create codeflash-compatible tsconfig
_create_codeflash_tsconfig(project_root)
# Create codeflash Jest config that uses it
codeflash_jest_config = _create_codeflash_jest_config(project_root, original_jest_config)
if codeflash_jest_config:
return codeflash_jest_config
# Always create a codeflash Jest config to handle ESM packages properly
# Many modern NPM packages are ESM-only and need transformation
codeflash_jest_config = _create_codeflash_jest_config(project_root, original_jest_config, for_esm=True)
if codeflash_jest_config:
return codeflash_jest_config
return original_jest_config
@ -323,6 +419,55 @@ def _find_monorepo_root(start_path: Path) -> Path | None:
return None
def _get_jest_major_version(project_root: Path) -> int | None:
"""Detect the major version of Jest installed in the project.
Args:
project_root: Root of the project to check.
Returns:
Major version number (e.g., 29, 30), or None if not detected.
"""
# First try to check package.json for explicit version
package_json = project_root / "package.json"
if package_json.exists():
try:
content = json.loads(package_json.read_text())
deps = {**content.get("devDependencies", {}), **content.get("dependencies", {})}
jest_version = deps.get("jest", "")
# Parse version like "30.0.5", "^30.0.5", "~30.0.5"
if jest_version:
# Strip leading version prefixes (^, ~, =, v)
version_str = jest_version.lstrip("^~=v")
if version_str and version_str[0].isdigit():
major = version_str.split(".")[0]
if major.isdigit():
return int(major)
except (json.JSONDecodeError, OSError):
pass
# Also check monorepo root
monorepo_root = _find_monorepo_root(project_root)
if monorepo_root and monorepo_root != project_root:
monorepo_package = monorepo_root / "package.json"
if monorepo_package.exists():
try:
content = json.loads(monorepo_package.read_text())
deps = {**content.get("devDependencies", {}), **content.get("dependencies", {})}
jest_version = deps.get("jest", "")
if jest_version:
version_str = jest_version.lstrip("^~=v")
if version_str and version_str[0].isdigit():
major = version_str.split(".")[0]
if major.isdigit():
return int(major)
except (json.JSONDecodeError, OSError):
pass
return None
def _find_jest_config(project_root: Path) -> Path | None:
"""Find Jest configuration file in the project.
@ -609,13 +754,25 @@ def run_jest_behavioral_tests(
# Configure ESM support if project uses ES Modules
_configure_esm_environment(jest_env, effective_cwd)
# Increase Node.js heap size for large TypeScript projects
# Default heap is often not enough for monorepos with many dependencies
existing_node_options = jest_env.get("NODE_OPTIONS", "")
if "--max-old-space-size" not in existing_node_options:
jest_env["NODE_OPTIONS"] = f"{existing_node_options} --max-old-space-size=4096".strip()
logger.debug(f"Running Jest tests with command: {' '.join(jest_cmd)}")
# Calculate subprocess timeout: needs to be much larger than per-test timeout
# to account for Jest startup, TypeScript compilation, module loading, etc.
# Use at least 120 seconds, or 10x the per-test timeout, whichever is larger
subprocess_timeout = max(120, (timeout or 15) * 10, 600) if timeout else 600
start_time_ns = time.perf_counter_ns()
try:
run_args = get_cross_platform_subprocess_run_args(
cwd=effective_cwd, env=jest_env, timeout=timeout or 600, check=False, text=True, capture_output=True
cwd=effective_cwd, env=jest_env, timeout=subprocess_timeout, check=False, text=True, capture_output=True
)
logger.debug(f"Jest subprocess timeout: {subprocess_timeout}s (per-test timeout: {timeout}s)")
result = subprocess.run(jest_cmd, **run_args) # noqa: PLW1510
# Jest sends console.log output to stderr by default - move it to stdout
# so our timing markers (printed via console.log) are in the expected place
@ -631,14 +788,14 @@ def run_jest_behavioral_tests(
logger.debug(f"Jest result: returncode={result.returncode}")
# Log Jest output at WARNING level if tests fail and no XML output will be created
# This helps debug issues like import errors that cause Jest to fail early
if result.returncode != 0 and not result_file_path.exists():
if result.returncode != 0:
logger.warning(
f"Jest failed with returncode={result.returncode} and no XML output created.\n"
f"Jest failed with returncode={result.returncode}.\n"
f"Jest stdout: {result.stdout[:2000] if result.stdout else '(empty)'}\n"
f"Jest stderr: {result.stderr[:500] if result.stderr else '(empty)'}"
)
except subprocess.TimeoutExpired:
logger.warning(f"Jest tests timed out after {timeout}s")
logger.warning(f"Jest tests timed out after {subprocess_timeout}s")
result = subprocess.CompletedProcess(args=jest_cmd, returncode=-1, stdout="", stderr="Test execution timed out")
except FileNotFoundError:
logger.error("Jest not found. Make sure Jest is installed (npm install jest)")
@ -783,7 +940,12 @@ def run_jest_benchmarking_tests(
# Ensure the codeflash npm package is installed
_ensure_runtime_files(effective_cwd)
# Build Jest command for performance tests with custom loop runner
# Detect Jest version for logging
jest_major_version = _get_jest_major_version(effective_cwd)
if jest_major_version:
logger.debug(f"Jest {jest_major_version} detected - using loop-runner for batched looping")
# Build Jest command for performance tests
jest_cmd = [
"npx",
"jest",
@ -847,9 +1009,19 @@ def run_jest_benchmarking_tests(
jest_env["LOG_LEVEL"] = "info" # Disable console.log mocking in projects that check LOG_LEVEL
jest_env["DEBUG"] = "1" # Disable console.log mocking in projects that check DEBUG
# Debug logging for loop behavior verification (set CODEFLASH_DEBUG_LOOPS=true to enable)
if os.environ.get("CODEFLASH_DEBUG_LOOPS") == "true":
jest_env["CODEFLASH_DEBUG_LOOPS"] = "true"
logger.info("Loop debug logging enabled - will show capturePerf loop details")
# Configure ESM support if project uses ES Modules
_configure_esm_environment(jest_env, effective_cwd)
# Increase Node.js heap size for large TypeScript projects
existing_node_options = jest_env.get("NODE_OPTIONS", "")
if "--max-old-space-size" not in existing_node_options:
jest_env["NODE_OPTIONS"] = f"{existing_node_options} --max-old-space-size=4096".strip()
# Total timeout for the entire benchmark run (longer than single-loop timeout)
# Account for startup overhead + target duration + buffer
total_timeout = max(120, (target_duration_ms // 1000) + 60, timeout or 120)
@ -885,6 +1057,7 @@ def run_jest_benchmarking_tests(
wall_clock_seconds = time.time() - total_start_time
logger.debug(f"Jest benchmarking completed in {wall_clock_seconds:.2f}s")
Path("/home/mohammed/Work/codeflash/output.log").write_text(result.stdout)
return result_file_path, result
@ -988,6 +1161,11 @@ def run_jest_line_profile_tests(
# Configure ESM support if project uses ES Modules
_configure_esm_environment(jest_env, effective_cwd)
# Increase Node.js heap size for large TypeScript projects
existing_node_options = jest_env.get("NODE_OPTIONS", "")
if "--max-old-space-size" not in existing_node_options:
jest_env["NODE_OPTIONS"] = f"{existing_node_options} --max-old-space-size=4096".strip()
subprocess_timeout = timeout or 600
logger.debug(f"Running Jest line profile tests: {' '.join(jest_cmd)}")

View file

@ -8,6 +8,7 @@ import libcst as cst
from rich.tree import Tree
from codeflash.cli_cmds.console import DEBUG_MODE, lsp_log
from codeflash.languages.current import is_javascript
from codeflash.languages.registry import get_language_support
from codeflash.lsp.helpers import is_LSP_enabled, report_to_markdown_table
from codeflash.lsp.lsp_message import LspMarkdownMessage
@ -895,6 +896,9 @@ class TestResults(BaseModel): # noqa: PLW1641
def number_of_loops(self) -> int:
if not self.test_results:
return 0
# TODO: Fix this. timings are not accurate something is off with either loop runner or capturePerf
if is_javascript():
return self.effective_loop_count()
return max(test_result.loop_index for test_result in self.test_results)
def get_test_pass_fail_report_by_type(self) -> dict[TestType, dict[str, int]]:
@ -964,6 +968,28 @@ class TestResults(BaseModel): # noqa: PLW1641
[min(usable_runtime_data) for _, usable_runtime_data in self.usable_runtime_data_by_test_case().items()]
)
def effective_loop_count(self) -> int:
"""Calculate the effective number of complete loops.
For consistent behavior across Python and JavaScript tests, this returns
the maximum loop_index seen across all test results. This represents
the number of timing iterations that were performed.
Note: For JavaScript tests without the loop-runner, each test case may have
different iteration counts due to internal looping in capturePerf. We use
max() to report the highest iteration count achieved.
:return: The effective loop count, or 0 if no test results.
"""
if not self.test_results:
return 0
# Get all loop indices from results that have timing data
loop_indices = {result.loop_index for result in self.test_results if result.runtime is not None}
if not loop_indices:
# Fallback: use all loop indices even without runtime
loop_indices = {result.loop_index for result in self.test_results}
return max(loop_indices) if loop_indices else 0
def file_to_no_of_tests(self, test_functions_to_remove: list[str]) -> Counter[Path]:
map_gen_test_file_to_no_of_tests = Counter()
for gen_test_result in self.test_results:

View file

@ -80,6 +80,7 @@ from codeflash.languages import is_python
from codeflash.languages.base import Language
from codeflash.languages.current import current_language_support, is_typescript
from codeflash.languages.javascript.module_system import detect_module_system
from codeflash.languages.javascript.test_runner import clear_created_config_files, get_created_config_files
from codeflash.lsp.helpers import is_LSP_enabled, report_to_markdown_table, tree_to_markdown
from codeflash.lsp.lsp_message import LspCodeMessage, LspMarkdownMessage, LSPMessageId
from codeflash.models.ExperimentMetadata import ExperimentMetadata
@ -2416,7 +2417,7 @@ class FunctionOptimizer:
if not success:
return Failure("Failed to establish a baseline for the original code.")
loop_count = max([int(result.loop_index) for result in benchmarking_results.test_results])
loop_count = benchmarking_results.effective_loop_count()
logger.info(
f"h3|⌚ Original code summed runtime measured over '{loop_count}' loop{'s' if loop_count > 1 else ''}: "
f"'{humanize_runtime(total_timing)}' per full loop"
@ -2639,11 +2640,10 @@ class FunctionOptimizer:
self.write_code_and_helpers(
candidate_fto_code, candidate_helper_code, self.function_to_optimize.file_path
)
loop_count = (
max(all_loop_indices)
if (all_loop_indices := {result.loop_index for result in candidate_benchmarking_results.test_results})
else 0
)
# Use effective_loop_count which represents the minimum number of timing samples
# across all test cases. This is more accurate for JavaScript tests where
# capturePerf does internal looping with potentially different iteration counts per test.
loop_count = candidate_benchmarking_results.effective_loop_count()
if (total_candidate_timing := candidate_benchmarking_results.total_passed_runtime()) == 0:
logger.warning("The overall test runtime of the optimized function is 0, couldn't run tests.")
@ -2839,6 +2839,13 @@ class FunctionOptimizer:
paths_to_cleanup.append(test_file.instrumented_behavior_file_path)
paths_to_cleanup.append(test_file.benchmarking_file_path)
# Clean up created config files (jest.codeflash.config.js, tsconfig.codeflash.json)
config_files = get_created_config_files()
if config_files:
paths_to_cleanup.extend(config_files)
logger.debug(f"Cleaning up {len(config_files)} codeflash config file(s)")
clear_created_config_files()
cleanup_paths(paths_to_cleanup)
def get_test_env(

View file

@ -23,12 +23,12 @@ def get_test_file_path(
# For JavaScript/TypeScript, place generated tests in a subdirectory that matches
# Vitest/Jest include patterns (e.g., test/**/*.test.ts)
if is_javascript():
# For monorepos, first try to find the package directory from the source file path
# e.g., packages/workflow/src/utils.ts -> packages/workflow/test/codeflash-generated/
package_test_dir = _find_js_package_test_dir(test_dir, source_file_path)
if package_test_dir:
test_dir = package_test_dir
# if is_javascript():
# # For monorepos, first try to find the package directory from the source file path
# # e.g., packages/workflow/src/utils.ts -> packages/workflow/test/codeflash-generated/
# package_test_dir = _find_js_package_test_dir(test_dir, source_file_path)
# if package_test_dir:
# test_dir = package_test_dir
path = test_dir / f"test_{function_name}__{test_type}_test_{iteration}{extension}"
if path.exists():