Skip to content

Commit c4089f6

Browse files
Merge branch 'multi-language' of github.com:codeflash-ai/codeflash into multi-language
2 parents 3475f22 + 4e39408 commit c4089f6

7 files changed

Lines changed: 92 additions & 20 deletions

File tree

code_to_optimize_js/codeflash-jest-helper.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ function initDatabase() {
193193
)
194194
`);
195195
} catch (e) {
196-
console.log('[codeflash] Failed to initialize SQLite:', e.message);
196+
console.error('[codeflash] Failed to initialize SQLite:', e.message);
197197
useSqlite = false;
198198
}
199199
}

codeflash/discovery/discover_unit_tests.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -554,9 +554,7 @@ def filter_test_files_by_imports(
554554
return filtered_map
555555

556556

557-
def _detect_language_from_functions(
558-
file_to_funcs: dict[Path, list[FunctionToOptimize]] | None,
559-
) -> str | None:
557+
def _detect_language_from_functions(file_to_funcs: dict[Path, list[FunctionToOptimize]] | None) -> str | None:
560558
"""Detect language from the functions to optimize.
561559
562560
Args:
@@ -576,9 +574,7 @@ def _detect_language_from_functions(
576574

577575

578576
def discover_tests_for_language(
579-
cfg: TestConfig,
580-
language: str,
581-
file_to_funcs_to_optimize: dict[Path, list[FunctionToOptimize]] | None,
577+
cfg: TestConfig, language: str, file_to_funcs_to_optimize: dict[Path, list[FunctionToOptimize]] | None
582578
) -> tuple[dict[str, set[FunctionCalledInTest]], int, int]:
583579
"""Discover tests using language-specific support.
584580
@@ -656,6 +652,10 @@ def discover_unit_tests(
656652

657653
# Route to language-specific test discovery for non-Python languages
658654
if language and language != "python":
655+
# For JavaScript/TypeScript, tests_project_rootdir should be tests_root itself
656+
# The Jest helper will be configured to NOT include "tests." prefix to match
657+
if language in ("javascript", "typescript"):
658+
cfg.tests_project_rootdir = cfg.tests_root
659659
return discover_tests_for_language(cfg, language, file_to_funcs_to_optimize)
660660

661661
# Existing Python logic

codeflash/languages/javascript/runtime/codeflash-jest-helper.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -301,10 +301,12 @@ function capture(funcName, lineId, fn, ...args) {
301301
// Get relative path from cwd and convert to module-style path
302302
const path = require('path');
303303
const relativePath = path.relative(process.cwd(), currentTestPath);
304-
// Convert to Python module-style path (e.g., "tests/test_foo.test.js" -> "tests.test_foo.test")
305-
// This matches what Jest's junit XML produces
304+
// Convert to module-style path (e.g., "tests/test_foo.test.js" -> "test_foo.test")
305+
// Strip the leading "tests/" directory to match what Jest's junit XML produces
306+
// and what module_name_from_file_path(test_file, tests_root) generates
306307
testModulePath = relativePath
307308
.replace(/\\/g, '/') // Handle Windows paths
309+
.replace(/^tests\//, '') // Strip leading "tests/" directory
308310
.replace(/\.js$/, '') // Remove .js extension
309311
.replace(/\.test$/, '.test') // Keep .test suffix
310312
.replace(/\//g, '.'); // Convert path separators to dots
@@ -404,9 +406,11 @@ function capturePerf(funcName, lineId, fn, ...args) {
404406
// Get relative path from cwd and convert to module-style path
405407
const path = require('path');
406408
const relativePath = path.relative(process.cwd(), currentTestPath);
407-
// Convert to Python module-style path (e.g., "tests/test_foo.test.js" -> "tests.test_foo.test")
409+
// Convert to module-style path (e.g., "tests/test_foo.test.js" -> "test_foo.test")
410+
// Strip leading "tests/" to match XML module path format
408411
testModulePath = relativePath
409412
.replace(/\\/g, '/')
413+
.replace(/^tests\//, '') // Strip leading "tests/" directory
410414
.replace(/\.js$/, '')
411415
.replace(/\.test$/, '.test')
412416
.replace(/\//g, '.');
@@ -544,9 +548,11 @@ function capturePerfLooped(funcName, lineId, fn, ...args) {
544548
// Get relative path from cwd and convert to module-style path
545549
const path = require('path');
546550
const relativePath = path.relative(process.cwd(), currentTestPath);
547-
// Convert to Python module-style path (e.g., "tests/test_foo.test.js" -> "tests.test_foo.test")
551+
// Convert to module-style path (e.g., "tests/test_foo.test.js" -> "test_foo.test")
552+
// Strip leading "tests/" to match XML module path format
548553
testModulePath = relativePath
549554
.replace(/\\/g, '/')
555+
.replace(/^tests\//, '') // Strip leading "tests/" directory
550556
.replace(/\.js$/, '')
551557
.replace(/\.test$/, '.test')
552558
.replace(/\//g, '.');
@@ -632,8 +638,8 @@ function capturePerfLooped(funcName, lineId, fn, ...args) {
632638
break;
633639
}
634640

635-
// Stop if we've reached min loops AND exceeded time limit
636-
if (loopCount >= MIN_LOOPS && elapsedMs >= TARGET_DURATION_MS) {
641+
// Stop if we've reached min loops AND exceeded a reasonable portion of time
642+
if (loopCount >= MIN_LOOPS && elapsedMs >= TARGET_DURATION_MS * 0.8) {
637643
break;
638644
}
639645

codeflash/optimization/function_optimizer.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,24 @@ def generate_and_instrument_tests(
601601
for test_index in range(n_tests)
602602
]
603603

604+
# For JavaScript/TypeScript, copy all runtime files to tests directory
605+
if language in ("javascript", "typescript"):
606+
import shutil
607+
608+
from codeflash.languages.javascript.runtime import get_all_runtime_files
609+
610+
# Copy all runtime files (helper, serializer, comparator, etc.)
611+
for runtime_file_source in get_all_runtime_files():
612+
runtime_file_dest = self.test_cfg.tests_root / runtime_file_source.name
613+
614+
# Copy file if it doesn't exist or is outdated
615+
if (
616+
not runtime_file_dest.exists()
617+
or runtime_file_source.stat().st_mtime > runtime_file_dest.stat().st_mtime
618+
):
619+
shutil.copy2(runtime_file_source, runtime_file_dest)
620+
logger.debug(f"Copied {runtime_file_source.name} to {runtime_file_dest}")
621+
604622
test_results = self.generate_tests(
605623
testgen_context=code_context.testgen_context,
606624
helper_functions=code_context.helper_functions,
@@ -2238,6 +2256,7 @@ def establish_original_code_baseline(
22382256
for result in behavioral_results
22392257
if (result.test_type == TestType.GENERATED_REGRESSION and not result.did_pass)
22402258
]
2259+
22412260
if total_timing == 0:
22422261
logger.warning("The overall summed benchmark runtime of the original function is 0, couldn't run tests.")
22432262
console.rule()
@@ -2587,6 +2606,7 @@ def run_and_parse_tests(
25872606

25882607
if testing_type in {TestingMode.BEHAVIOR, TestingMode.PERFORMANCE}:
25892608
# For JavaScript behavior tests, skip SQLite cleanup - files needed for JS-native comparison
2609+
#TODO (ali): make sure it works fine
25902610
is_js_for_original_code = self.is_js and optimization_iteration == 0
25912611
is_js_behavior = (self.is_js and testing_type == TestingMode.BEHAVIOR) or is_js_for_original_code
25922612

codeflash/verification/parse_test_output.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -519,9 +519,11 @@ def parse_jest_test_xml(
519519
timed_out = True
520520

521521
# Find matching timing markers for this test
522-
# Jest test names in markers match the full test name
523-
# the test_name is the string in describe and test arguments, the group 2 is the same but with _ instead of spaces
524-
matching_starts = [m for m in start_matches if m.group(2).replace("_", " ") in test_name]
522+
# Jest test names in markers are sanitized (spaces → underscores)
523+
# The marker format is: testModulePath:testFunctionName:funcName:loopIndex:lineId
524+
# We need to check group(2) (testFunctionName) and handle sanitization
525+
sanitized_test_name = re.sub(r"[!#: ()\[\]{}|\\/*?^$.+\-]", "_", test_name)
526+
matching_starts = [m for m in start_matches if sanitized_test_name in m.group(2)]
525527

526528
if not matching_starts:
527529
# No timing markers found - add basic result
@@ -1024,6 +1026,7 @@ def parse_test_results(
10241026
# (comparison happens in JavaScript land via compare_javascript_test_results)
10251027
if not skip_sqlite_cleanup:
10261028
get_run_tmp_file(Path(f"test_return_values_{optimization_iteration}.sqlite")).unlink(missing_ok=True)
1029+
10271030
results = merge_test_results(test_results_xml, test_results_data, test_config.test_framework)
10281031

10291032
all_args = False

codeflash/verification/test_runner.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import contextlib
44
import shlex
5+
import shutil
56
import subprocess
67
import sys
78
from pathlib import Path
@@ -13,6 +14,7 @@
1314
from codeflash.code_utils.config_consts import TOTAL_LOOPING_TIME_EFFECTIVE
1415
from codeflash.code_utils.coverage_utils import prepare_coverage_files
1516
from codeflash.code_utils.shell_utils import get_cross_platform_subprocess_run_args
17+
from codeflash.languages.javascript.runtime import get_all_runtime_files
1618
from codeflash.models.models import TestFiles, TestType
1719

1820
if TYPE_CHECKING:
@@ -22,6 +24,23 @@
2224
BENCHMARKING_BLOCKLISTED_PLUGINS = ["codspeed", "cov", "benchmark", "profiling", "xdist", "sugar"]
2325

2426

27+
def _ensure_js_runtime_files(js_project_root: Path) -> None:
28+
"""Ensure JavaScript runtime files are present in the project root.
29+
30+
Copies codeflash-jest-helper.js and related files to the JS project root
31+
if they don't already exist or are outdated.
32+
33+
Args:
34+
js_project_root: The JavaScript project root directory.
35+
"""
36+
for runtime_file in get_all_runtime_files():
37+
dest_path = js_project_root / runtime_file.name
38+
# Always copy to ensure we have the latest version
39+
if not dest_path.exists() or dest_path.stat().st_mtime < runtime_file.stat().st_mtime:
40+
shutil.copy2(runtime_file, dest_path)
41+
logger.debug(f"Copied {runtime_file.name} to {js_project_root}")
42+
43+
2544
def _find_js_project_root(file_path: Path) -> Path | None:
2645
"""Find the JavaScript/TypeScript project root by looking for package.json.
2746
@@ -82,6 +101,9 @@ def run_jest_behavioral_tests(
82101
effective_cwd = js_project_root if js_project_root else cwd
83102
logger.debug(f"Jest working directory: {effective_cwd}")
84103

104+
# Ensure runtime files (codeflash-jest-helper.js, etc.) are present
105+
_ensure_js_runtime_files(effective_cwd)
106+
85107
# Coverage output directory
86108
coverage_dir = get_run_tmp_file(Path("jest_coverage"))
87109
coverage_json_path = coverage_dir / "coverage-final.json" if enable_coverage else None
@@ -372,11 +394,13 @@ def run_jest_benchmarking_tests(
372394
test_paths: TestFiles object containing test file information.
373395
test_env: Environment variables for the test run.
374396
cwd: Working directory for running tests.
375-
timeout: Optional timeout in seconds.
397+
timeout: Optional timeout in seconds for the subprocess.
376398
js_project_root: JavaScript project root (directory containing package.json).
377399
min_loops: Minimum number of loops to run for each test case.
378400
max_loops: Maximum number of loops to run for each test case.
379-
target_duration_ms: Target duration in milliseconds for looping.
401+
target_duration_ms: Target TOTAL duration in milliseconds for looping.
402+
This is divided among test cases since JavaScript uses capturePerfLooped
403+
which loops internally per test case, unlike Python's external looping.
380404
stability_check: Whether to enable stability-based early stopping.
381405
382406
Returns:
@@ -388,6 +412,13 @@ def run_jest_benchmarking_tests(
388412
# Get performance test files
389413
test_files = [str(file.benchmarking_file_path) for file in test_paths.test_files if file.benchmarking_file_path]
390414

415+
# Count approximate number of test cases to divide time budget
416+
# JavaScript's capturePerfLooped loops internally per test case, so we need to divide
417+
# the total time budget among test cases to avoid timeout
418+
num_test_cases = len(test_files) * 10 # Estimate ~10 test cases per file (conservative)
419+
# Use at least 500ms per test case for fast functions, cap at 2 seconds
420+
per_test_duration_ms = max(500, min(2000, target_duration_ms // max(1, num_test_cases)))
421+
391422
# Use provided js_project_root, or detect it as fallback
392423
if js_project_root is None and test_files:
393424
first_test_file = Path(test_files[0])
@@ -396,6 +427,9 @@ def run_jest_benchmarking_tests(
396427
effective_cwd = js_project_root if js_project_root else cwd
397428
logger.debug(f"Jest benchmarking working directory: {effective_cwd}")
398429

430+
# Ensure runtime files (codeflash-jest-helper.js, etc.) are present
431+
_ensure_js_runtime_files(effective_cwd)
432+
399433
# Build Jest command for performance tests
400434
jest_cmd = ["npx", "jest", "--reporters=default", "--reporters=jest-junit", "--runInBand", "--forceExit"]
401435

@@ -425,12 +459,21 @@ def run_jest_benchmarking_tests(
425459
# Looping configuration for stable performance measurements
426460
jest_env["CODEFLASH_MIN_LOOPS"] = str(min_loops)
427461
jest_env["CODEFLASH_MAX_LOOPS"] = str(max_loops)
428-
jest_env["CODEFLASH_TARGET_DURATION_MS"] = str(target_duration_ms)
462+
# Use per-test duration instead of total duration
463+
jest_env["CODEFLASH_TARGET_DURATION_MS"] = str(per_test_duration_ms)
429464
jest_env["CODEFLASH_STABILITY_CHECK"] = "true" if stability_check else "false"
430465
# Seed random number generator for reproducible test runs across original and optimized code
431466
jest_env["CODEFLASH_RANDOM_SEED"] = "42"
432467

468+
# Calculate subprocess timeout based on expected benchmarking time
469+
# For slow O(n²) functions, a single call might take seconds, so add generous buffer
470+
# Allow for Jest startup overhead (10s) + per-test-case benchmarking + safety margin
471+
expected_benchmarking_time_s = (num_test_cases * per_test_duration_ms) / 1000 + 10
472+
# Use at least 60 seconds to handle slow functions, or calculated time with 2x margin
473+
subprocess_timeout = max(60, int(expected_benchmarking_time_s * 2))
474+
433475
logger.debug(f"Running Jest benchmarking tests: {' '.join(jest_cmd)}")
476+
logger.debug(f"Jest benchmarking config: {num_test_cases} estimated test cases, {per_test_duration_ms}ms per test, min_loops={min_loops}, {subprocess_timeout}s subprocess timeout")
434477

435478
# Calculate subprocess timeout: for Jest benchmarking, we need enough time for all tests to complete
436479
# Each test can run up to target_duration_ms (default 10s) for stable measurements

codeflash/verification/verifier.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def generate_tests(
2727
test_index: int,
2828
test_path: Path,
2929
test_perf_path: Path,
30-
) -> tuple[str, str, Path] | None:
30+
) -> tuple[str, str, str, Path, Path] | None:
3131
# TODO: Sometimes this recreates the original Class definition. This overrides and messes up the original
3232
# class import. Remove the recreation of the class definition
3333
start_time = time.perf_counter()

0 commit comments

Comments
 (0)