Skip to content

Commit a8901f2

Browse files
authored
Merge branch 'main' into fix/marker-matching-function-name
2 parents 160e138 + 19cf40d commit a8901f2

File tree

10 files changed

+457
-57
lines changed

10 files changed

+457
-57
lines changed

codeflash/cli_cmds/cli.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,11 +359,13 @@ def _handle_show_config() -> None:
359359
detected = detect_project(project_root)
360360

361361
# Check if config exists or is auto-detected
362-
config_exists, _ = has_existing_config(project_root)
362+
config_exists, config_file = has_existing_config(project_root)
363363
status = "Saved config" if config_exists else "Auto-detected (not saved)"
364364

365365
console.print()
366366
console.print(f"[bold]Codeflash Configuration[/bold] ({status})")
367+
if config_exists and config_file:
368+
console.print(f"[dim]Config file: {project_root / config_file}[/dim]")
367369
console.print()
368370

369371
table = Table(show_header=True, header_style="bold cyan")

codeflash/code_utils/config_js.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from pathlib import Path
77
from typing import Any
88

9+
from codeflash.setup.detector import is_build_output_dir
10+
911
PACKAGE_JSON_CACHE: dict[Path, Path] = {}
1012
PACKAGE_JSON_DATA_CACHE: dict[Path, dict[str, Any]] = {}
1113

@@ -50,12 +52,15 @@ def detect_module_root(project_root: Path, package_data: dict[str, Any]) -> str:
5052
"""Detect module root from package.json fields or directory conventions.
5153
5254
Detection order:
53-
1. package.json "exports" field (extract directory from main export)
54-
2. package.json "module" field (ESM entry point)
55-
3. package.json "main" field (CJS entry point)
56-
4. "src/" directory if it exists
55+
1. src/, lib/, source/ directories (common source directories)
56+
2. package.json "exports" field (if not in build output directory)
57+
3. package.json "module" field (ESM, if not in build output directory)
58+
4. package.json "main" field (CJS, if not in build output directory)
5759
5. Fall back to "." (project root)
5860
61+
Build output directories (build/, dist/, out/) are skipped since they contain
62+
compiled code, not source files.
63+
5964
Args:
6065
project_root: Root directory of the project.
6166
package_data: Parsed package.json data.
@@ -64,6 +69,11 @@ def detect_module_root(project_root: Path, package_data: dict[str, Any]) -> str:
6469
Detected module root path (relative to project root).
6570
6671
"""
72+
# Check for common source directories first - these are always preferred
73+
for src_dir in ["src", "lib", "source"]:
74+
if (project_root / src_dir).is_dir():
75+
return src_dir
76+
6777
# Check exports field (modern Node.js)
6878
exports = package_data.get("exports")
6979
if exports:
@@ -80,27 +90,38 @@ def detect_module_root(project_root: Path, package_data: dict[str, Any]) -> str:
8090

8191
if entry_path and isinstance(entry_path, str):
8292
parent = Path(entry_path).parent
83-
if parent != Path() and (project_root / parent).is_dir():
93+
if (
94+
parent != Path()
95+
and parent.as_posix() != "."
96+
and (project_root / parent).is_dir()
97+
and not is_build_output_dir(parent)
98+
):
8499
return parent.as_posix()
85100

86101
# Check module field (ESM)
87102
module_field = package_data.get("module")
88103
if module_field and isinstance(module_field, str):
89104
parent = Path(module_field).parent
90-
if parent != Path() and (project_root / parent).is_dir():
105+
if (
106+
parent != Path()
107+
and parent.as_posix() != "."
108+
and (project_root / parent).is_dir()
109+
and not is_build_output_dir(parent)
110+
):
91111
return parent.as_posix()
92112

93113
# Check main field (CJS)
94114
main_field = package_data.get("main")
95115
if main_field and isinstance(main_field, str):
96116
parent = Path(main_field).parent
97-
if parent != Path() and (project_root / parent).is_dir():
117+
if (
118+
parent != Path()
119+
and parent.as_posix() != "."
120+
and (project_root / parent).is_dir()
121+
and not is_build_output_dir(parent)
122+
):
98123
return parent.as_posix()
99124

100-
# Check for src/ directory convention
101-
if (project_root / "src").is_dir():
102-
return "src"
103-
104125
# Default to project root
105126
return "."
106127

codeflash/languages/javascript/module_system.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ def _convert_destructuring_to_imports(names_str: str) -> str:
222222
223223
Returns:
224224
Import names string with aliases using 'as' syntax
225+
225226
"""
226227
# Split by commas and process each name
227228
parts = []

codeflash/setup/detector.py

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
import tomlkit
2323

24+
_BUILD_DIRS = frozenset({"build", "dist", "out", ".next", ".nuxt"})
25+
2426

2527
@dataclass
2628
class DetectedProject:
@@ -310,14 +312,21 @@ def _detect_js_module_root(project_root: Path) -> tuple[Path, str]:
310312
"""Detect JavaScript/TypeScript module root.
311313
312314
Priority:
313-
1. package.json "exports" field
314-
2. package.json "module" field (ESM)
315-
3. package.json "main" field (CJS)
316-
4. src/ directory
317-
5. lib/ directory
318-
6. Project root
315+
1. src/, lib/, source/ directories (common source directories)
316+
2. package.json "exports" field (if not in build output directory)
317+
3. package.json "module" field (ESM, if not in build output directory)
318+
4. package.json "main" field (CJS, if not in build output directory)
319+
5. Project root
320+
321+
Build output directories (build/, dist/, out/) are skipped since they contain
322+
compiled code, not source files.
319323
320324
"""
325+
# Check for common source directories first - these are always preferred
326+
for src_dir in ["src", "lib", "source"]:
327+
if (project_root / src_dir).is_dir():
328+
return project_root / src_dir, f"{src_dir}/ directory"
329+
321330
package_json_path = project_root / "package.json"
322331
package_data: dict[str, Any] = {}
323332

@@ -334,32 +343,52 @@ def _detect_js_module_root(project_root: Path) -> tuple[Path, str]:
334343
entry_path = _extract_entry_path(exports)
335344
if entry_path:
336345
parent = Path(entry_path).parent
337-
if parent != Path() and parent.as_posix() != "." and (project_root / parent).is_dir():
346+
if (
347+
parent != Path()
348+
and parent.as_posix() != "."
349+
and (project_root / parent).is_dir()
350+
and not is_build_output_dir(parent)
351+
):
338352
return project_root / parent, f'{parent.as_posix()}/ (from package.json "exports")'
339353

340354
# Check module field (ESM)
341355
module_field = package_data.get("module")
342356
if module_field and isinstance(module_field, str):
343357
parent = Path(module_field).parent
344-
if parent != Path() and parent.as_posix() != "." and (project_root / parent).is_dir():
358+
if (
359+
parent != Path()
360+
and parent.as_posix() != "."
361+
and (project_root / parent).is_dir()
362+
and not is_build_output_dir(parent)
363+
):
345364
return project_root / parent, f'{parent.as_posix()}/ (from package.json "module")'
346365

347366
# Check main field (CJS)
348367
main_field = package_data.get("main")
349368
if main_field and isinstance(main_field, str):
350369
parent = Path(main_field).parent
351-
if parent != Path() and parent.as_posix() != "." and (project_root / parent).is_dir():
370+
if (
371+
parent != Path()
372+
and parent.as_posix() != "."
373+
and (project_root / parent).is_dir()
374+
and not is_build_output_dir(parent)
375+
):
352376
return project_root / parent, f'{parent.as_posix()}/ (from package.json "main")'
353377

354-
# Check for common source directories
355-
for src_dir in ["src", "lib", "source"]:
356-
if (project_root / src_dir).is_dir():
357-
return project_root / src_dir, f"{src_dir}/ directory"
358-
359378
# Default to project root
360379
return project_root, "project root"
361380

362381

382+
def is_build_output_dir(path: Path) -> bool:
383+
"""Check if a path is within a common build output directory.
384+
385+
Build output directories contain compiled code and should be skipped
386+
in favor of source directories.
387+
388+
"""
389+
return not _BUILD_DIRS.isdisjoint(path.parts)
390+
391+
363392
def _extract_entry_path(exports: Any) -> str | None:
364393
"""Extract entry path from package.json exports field."""
365394
if isinstance(exports, str):
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* Test: Dynamic environment variable reading
3+
*
4+
* This test verifies that the performance configuration functions read
5+
* environment variables at runtime rather than at module load time.
6+
*
7+
* This is critical for Vitest compatibility, where modules may be cached
8+
* and loaded before environment variables are set.
9+
*
10+
* Run with: node __tests__/dynamic-env-vars.test.js
11+
*/
12+
13+
const assert = require('assert');
14+
15+
// Clear any existing env vars before loading the module
16+
delete process.env.CODEFLASH_PERF_LOOP_COUNT;
17+
delete process.env.CODEFLASH_PERF_MIN_LOOPS;
18+
delete process.env.CODEFLASH_PERF_TARGET_DURATION_MS;
19+
delete process.env.CODEFLASH_PERF_BATCH_SIZE;
20+
delete process.env.CODEFLASH_PERF_STABILITY_CHECK;
21+
delete process.env.CODEFLASH_PERF_CURRENT_BATCH;
22+
23+
// Now load the module - at this point env vars are not set
24+
const capture = require('../capture');
25+
26+
console.log('Testing dynamic environment variable reading...\n');
27+
28+
// Test 1: Default values when env vars are not set
29+
console.log('Test 1: Default values');
30+
assert.strictEqual(capture.getPerfLoopCount(), 1, 'getPerfLoopCount default should be 1');
31+
assert.strictEqual(capture.getPerfMinLoops(), 5, 'getPerfMinLoops default should be 5');
32+
assert.strictEqual(capture.getPerfTargetDurationMs(), 10000, 'getPerfTargetDurationMs default should be 10000');
33+
assert.strictEqual(capture.getPerfBatchSize(), 10, 'getPerfBatchSize default should be 10');
34+
assert.strictEqual(capture.getPerfStabilityCheck(), false, 'getPerfStabilityCheck default should be false');
35+
assert.strictEqual(capture.getPerfCurrentBatch(), 0, 'getPerfCurrentBatch default should be 0');
36+
console.log(' PASS: All defaults correct\n');
37+
38+
// Test 2: Values change when env vars are set AFTER module load
39+
// This is the critical test - if these were constants, they would still return defaults
40+
console.log('Test 2: Dynamic reading after module load');
41+
process.env.CODEFLASH_PERF_LOOP_COUNT = '100';
42+
process.env.CODEFLASH_PERF_MIN_LOOPS = '10';
43+
process.env.CODEFLASH_PERF_TARGET_DURATION_MS = '5000';
44+
process.env.CODEFLASH_PERF_BATCH_SIZE = '20';
45+
process.env.CODEFLASH_PERF_STABILITY_CHECK = 'true';
46+
process.env.CODEFLASH_PERF_CURRENT_BATCH = '5';
47+
48+
assert.strictEqual(capture.getPerfLoopCount(), 100, 'getPerfLoopCount should read 100 from env');
49+
assert.strictEqual(capture.getPerfMinLoops(), 10, 'getPerfMinLoops should read 10 from env');
50+
assert.strictEqual(capture.getPerfTargetDurationMs(), 5000, 'getPerfTargetDurationMs should read 5000 from env');
51+
assert.strictEqual(capture.getPerfBatchSize(), 20, 'getPerfBatchSize should read 20 from env');
52+
assert.strictEqual(capture.getPerfStabilityCheck(), true, 'getPerfStabilityCheck should read true from env');
53+
assert.strictEqual(capture.getPerfCurrentBatch(), 5, 'getPerfCurrentBatch should read 5 from env');
54+
console.log(' PASS: Dynamic reading works correctly\n');
55+
56+
// Test 3: Values change again when env vars are modified
57+
console.log('Test 3: Values update when env vars change');
58+
process.env.CODEFLASH_PERF_LOOP_COUNT = '500';
59+
process.env.CODEFLASH_PERF_BATCH_SIZE = '50';
60+
61+
assert.strictEqual(capture.getPerfLoopCount(), 500, 'getPerfLoopCount should update to 500');
62+
assert.strictEqual(capture.getPerfBatchSize(), 50, 'getPerfBatchSize should update to 50');
63+
console.log(' PASS: Values update correctly\n');
64+
65+
// Cleanup
66+
delete process.env.CODEFLASH_PERF_LOOP_COUNT;
67+
delete process.env.CODEFLASH_PERF_MIN_LOOPS;
68+
delete process.env.CODEFLASH_PERF_TARGET_DURATION_MS;
69+
delete process.env.CODEFLASH_PERF_BATCH_SIZE;
70+
delete process.env.CODEFLASH_PERF_STABILITY_CHECK;
71+
delete process.env.CODEFLASH_PERF_CURRENT_BATCH;
72+
73+
console.log('All tests passed!');

packages/codeflash/runtime/capture.js

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,30 @@ const TEST_MODULE = process.env.CODEFLASH_TEST_MODULE;
4747
// Batch 1: Test1(5 loops) → Test2(5 loops) → Test3(5 loops)
4848
// Batch 2: Test1(5 loops) → Test2(5 loops) → Test3(5 loops)
4949
// ...until time budget exhausted
50-
const PERF_LOOP_COUNT = parseInt(process.env.CODEFLASH_PERF_LOOP_COUNT || '1', 10);
51-
const PERF_MIN_LOOPS = parseInt(process.env.CODEFLASH_PERF_MIN_LOOPS || '5', 10);
52-
const PERF_TARGET_DURATION_MS = parseInt(process.env.CODEFLASH_PERF_TARGET_DURATION_MS || '10000', 10);
53-
const PERF_BATCH_SIZE = parseInt(process.env.CODEFLASH_PERF_BATCH_SIZE || '10', 10);
54-
const PERF_STABILITY_CHECK = (process.env.CODEFLASH_PERF_STABILITY_CHECK || 'false').toLowerCase() === 'true';
50+
//
51+
// IMPORTANT: These are getter functions, NOT constants!
52+
// Vitest caches modules and may load this file before env vars are set.
53+
// Using getter functions ensures we read the env vars at runtime when they're actually needed.
54+
function getPerfLoopCount() {
55+
return parseInt(process.env.CODEFLASH_PERF_LOOP_COUNT || '1', 10);
56+
}
57+
function getPerfMinLoops() {
58+
return parseInt(process.env.CODEFLASH_PERF_MIN_LOOPS || '5', 10);
59+
}
60+
function getPerfTargetDurationMs() {
61+
return parseInt(process.env.CODEFLASH_PERF_TARGET_DURATION_MS || '10000', 10);
62+
}
63+
function getPerfBatchSize() {
64+
return parseInt(process.env.CODEFLASH_PERF_BATCH_SIZE || '10', 10);
65+
}
66+
function getPerfStabilityCheck() {
67+
return (process.env.CODEFLASH_PERF_STABILITY_CHECK || 'false').toLowerCase() === 'true';
68+
}
5569
// Current batch number - set by loop-runner before each batch
5670
// This allows continuous loop indices even when Jest resets module state
57-
const PERF_CURRENT_BATCH = parseInt(process.env.CODEFLASH_PERF_CURRENT_BATCH || '0', 10);
71+
function getPerfCurrentBatch() {
72+
return parseInt(process.env.CODEFLASH_PERF_CURRENT_BATCH || '0', 10);
73+
}
5874

5975
// Stability constants (matching Python's config_consts.py)
6076
const STABILITY_WINDOW_SIZE = 0.35;
@@ -86,7 +102,7 @@ function checkSharedTimeLimit() {
86102
return false;
87103
}
88104
const elapsed = Date.now() - sharedPerfState.startTime;
89-
if (elapsed >= PERF_TARGET_DURATION_MS && sharedPerfState.totalLoopsCompleted >= PERF_MIN_LOOPS) {
105+
if (elapsed >= getPerfTargetDurationMs() && sharedPerfState.totalLoopsCompleted >= getPerfMinLoops()) {
90106
sharedPerfState.shouldStop = true;
91107
return true;
92108
}
@@ -111,7 +127,7 @@ function getInvocationLoopIndex(invocationKey) {
111127
// Calculate global loop index using batch number from environment
112128
// PERF_CURRENT_BATCH is 1-based (set by loop-runner before each batch)
113129
const currentBatch = parseInt(process.env.CODEFLASH_PERF_CURRENT_BATCH || '1', 10);
114-
const globalIndex = (currentBatch - 1) * PERF_BATCH_SIZE + localIndex;
130+
const globalIndex = (currentBatch - 1) * getPerfBatchSize() + localIndex;
115131

116132
return globalIndex;
117133
}
@@ -606,7 +622,7 @@ function capture(funcName, lineId, fn, ...args) {
606622
*/
607623
function capturePerf(funcName, lineId, fn, ...args) {
608624
// Check if we should skip looping entirely (shared time budget exceeded)
609-
const shouldLoop = PERF_LOOP_COUNT > 1 && !checkSharedTimeLimit();
625+
const shouldLoop = getPerfLoopCount() > 1 && !checkSharedTimeLimit();
610626

611627
// Get test context (computed once, reused across batch)
612628
let testModulePath;
@@ -636,9 +652,9 @@ function capturePerf(funcName, lineId, fn, ...args) {
636652
// If so, just execute the function once without timing (for test assertions)
637653
const peekLoopIndex = (sharedPerfState.invocationLoopCounts[invocationKey] || 0);
638654
const currentBatch = parseInt(process.env.CODEFLASH_PERF_CURRENT_BATCH || '1', 10);
639-
const nextGlobalIndex = (currentBatch - 1) * PERF_BATCH_SIZE + peekLoopIndex + 1;
655+
const nextGlobalIndex = (currentBatch - 1) * getPerfBatchSize() + peekLoopIndex + 1;
640656

641-
if (shouldLoop && nextGlobalIndex > PERF_LOOP_COUNT) {
657+
if (shouldLoop && nextGlobalIndex > getPerfLoopCount()) {
642658
// All loops completed, just execute once for test assertion
643659
return fn(...args);
644660
}
@@ -654,7 +670,7 @@ function capturePerf(funcName, lineId, fn, ...args) {
654670
// Batched looping: run BATCH_SIZE loops per capturePerf call when using loop-runner
655671
// For Vitest (no loop-runner), do all loops internally in a single call
656672
const batchSize = shouldLoop
657-
? (hasExternalLoopRunner ? PERF_BATCH_SIZE : PERF_LOOP_COUNT)
673+
? (hasExternalLoopRunner ? getPerfBatchSize() : getPerfLoopCount())
658674
: 1;
659675

660676
for (let batchIndex = 0; batchIndex < batchSize; batchIndex++) {
@@ -667,7 +683,7 @@ function capturePerf(funcName, lineId, fn, ...args) {
667683
const loopIndex = getInvocationLoopIndex(invocationKey);
668684

669685
// Check if we've exceeded max loops for this invocation
670-
if (loopIndex > PERF_LOOP_COUNT) {
686+
if (loopIndex > getPerfLoopCount()) {
671687
break;
672688
}
673689

@@ -872,7 +888,11 @@ module.exports = {
872888
LOOP_INDEX,
873889
OUTPUT_FILE,
874890
TEST_ITERATION,
875-
// Batch configuration
876-
PERF_BATCH_SIZE,
877-
PERF_LOOP_COUNT,
891+
// Batch configuration (getter functions for dynamic env var reading)
892+
getPerfBatchSize,
893+
getPerfLoopCount,
894+
getPerfMinLoops,
895+
getPerfTargetDurationMs,
896+
getPerfStabilityCheck,
897+
getPerfCurrentBatch,
878898
};

0 commit comments

Comments
 (0)