mirror of
https://github.com/codeflash-ai/codeflash.git
synced 2026-05-04 18:25:17 +00:00
some fixes for test runner and instrumentation
This commit is contained in:
parent
6c23255bca
commit
dcd9e2a502
9 changed files with 393 additions and 102 deletions
|
|
@ -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];
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)}")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
Loading…
Reference in a new issue