Skip to content

Commit 2f19cdd

Browse files
Merge pull request #1315 from codeflash-ai/fix/jest-xml-path-resolution-monorepo
fix: Jest XML path resolution and runner support for monorepos
2 parents a747513 + 0eb29bb commit 2f19cdd

3 files changed

Lines changed: 128 additions & 13 deletions

File tree

codeflash/languages/javascript/test_runner.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,33 @@ def _find_node_project_root(file_path: Path) -> Path | None:
296296
return None
297297

298298

299+
def _find_monorepo_root(start_path: Path) -> Path | None:
300+
"""Find the monorepo workspace root by looking for workspace markers.
301+
302+
Traverses up from the given path to find a directory containing
303+
monorepo workspace markers like yarn.lock, pnpm-workspace.yaml, etc.
304+
305+
Args:
306+
start_path: A path within the monorepo.
307+
308+
Returns:
309+
The monorepo root directory, or None if not found.
310+
311+
"""
312+
monorepo_markers = ["yarn.lock", "pnpm-workspace.yaml", "lerna.json", "package-lock.json"]
313+
current = start_path if start_path.is_dir() else start_path.parent
314+
315+
while current != current.parent:
316+
# Check for monorepo markers
317+
if any((current / marker).exists() for marker in monorepo_markers):
318+
# Verify it has node_modules (it's the workspace root)
319+
if (current / "node_modules").exists():
320+
return current
321+
current = current.parent
322+
323+
return None
324+
325+
299326
def _find_jest_config(project_root: Path) -> Path | None:
300327
"""Find Jest configuration file in the project.
301328
@@ -797,6 +824,12 @@ def run_jest_benchmarking_tests(
797824
jest_env["JEST_JUNIT_SUITE_NAME"] = "{filepath}"
798825
jest_env["JEST_JUNIT_ADD_FILE_ATTRIBUTE"] = "true"
799826
jest_env["JEST_JUNIT_INCLUDE_CONSOLE_OUTPUT"] = "true"
827+
828+
# Pass monorepo root to loop-runner for jest-runner resolution
829+
monorepo_root = _find_monorepo_root(effective_cwd)
830+
if monorepo_root:
831+
jest_env["CODEFLASH_MONOREPO_ROOT"] = str(monorepo_root)
832+
logger.debug(f"Detected monorepo root: {monorepo_root}")
800833
codeflash_sqlite_file = get_run_tmp_file(Path("test_return_values_0.sqlite"))
801834
jest_env["CODEFLASH_OUTPUT_FILE"] = str(codeflash_sqlite_file)
802835
jest_env["CODEFLASH_TEST_ITERATION"] = "0"

codeflash/verification/parse_test_output.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -150,19 +150,31 @@ def resolve_test_file_from_class_path(test_class_path: str, base_dir: Path) -> P
150150
# Handle file paths (contain slashes and extensions like .js/.ts)
151151
if "/" in test_class_path or "\\" in test_class_path:
152152
# This is a file path, not a Python module path
153+
# Try the path as-is if it's absolute
154+
potential_path = Path(test_class_path)
155+
if potential_path.is_absolute() and potential_path.exists():
156+
return potential_path
157+
153158
# Try to resolve relative to base_dir's parent (project root)
154159
project_root = base_dir.parent
155160
potential_path = project_root / test_class_path
156-
if potential_path.exists():
157-
return potential_path
161+
# Normalize to resolve .. and . components
162+
try:
163+
potential_path = potential_path.resolve()
164+
if potential_path.exists():
165+
return potential_path
166+
except (OSError, RuntimeError):
167+
pass
168+
158169
# Also try relative to base_dir itself
159170
potential_path = base_dir / test_class_path
160-
if potential_path.exists():
161-
return potential_path
162-
# Try the path as-is if it's absolute
163-
potential_path = Path(test_class_path)
164-
if potential_path.exists():
165-
return potential_path
171+
try:
172+
potential_path = potential_path.resolve()
173+
if potential_path.exists():
174+
return potential_path
175+
except (OSError, RuntimeError):
176+
pass
177+
166178
return None
167179

168180
# First try the full path (Python module path)
@@ -731,16 +743,25 @@ def parse_jest_test_xml(
731743
if not test_file_path.exists():
732744
test_file_path = base_dir / test_file_name
733745

734-
if test_file_path is None or not test_file_path.exists():
746+
# For Jest tests in monorepos, test files may not exist after cleanup
747+
# but we can still parse results and infer test type from the path
748+
if test_file_path is None:
735749
logger.warning(f"Could not resolve test file for Jest test: {test_class_path}")
736750
continue
737751

738752
# Get test type if not already set from lookup
739-
if test_type is None:
753+
if test_type is None and test_file_path.exists():
740754
test_type = test_files.get_test_type_by_instrumented_file_path(test_file_path)
741755
if test_type is None:
742-
# Default to GENERATED_REGRESSION for Jest tests
743-
test_type = TestType.GENERATED_REGRESSION
756+
# Infer test type from filename pattern
757+
filename = test_file_path.name
758+
if "__perf_test_" in filename or "_perf_test_" in filename:
759+
test_type = TestType.GENERATED_PERFORMANCE
760+
elif "__unit_test_" in filename or "_unit_test_" in filename:
761+
test_type = TestType.GENERATED_REGRESSION
762+
else:
763+
# Default to GENERATED_REGRESSION for Jest tests
764+
test_type = TestType.GENERATED_REGRESSION
744765

745766
# For Jest tests, keep the relative file path with extension intact
746767
# (Python uses module_name_from_file_path which strips extensions)

packages/codeflash/runtime/loop-runner.js

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,74 @@
3030

3131
const { createRequire } = require('module');
3232
const path = require('path');
33+
const fs = require('fs');
34+
35+
/**
36+
* Resolve jest-runner with monorepo support.
37+
* Uses CODEFLASH_MONOREPO_ROOT environment variable if available,
38+
* otherwise walks up the directory tree looking for node_modules/jest-runner.
39+
*/
40+
function resolveJestRunner() {
41+
// Try standard resolution first (works in simple projects)
42+
try {
43+
return require.resolve('jest-runner');
44+
} catch (e) {
45+
// Standard resolution failed - try monorepo-aware resolution
46+
}
47+
48+
// If Python detected a monorepo root, check there first
49+
const monorepoRoot = process.env.CODEFLASH_MONOREPO_ROOT;
50+
if (monorepoRoot) {
51+
const jestRunnerPath = path.join(monorepoRoot, 'node_modules', 'jest-runner');
52+
if (fs.existsSync(jestRunnerPath)) {
53+
const packageJsonPath = path.join(jestRunnerPath, 'package.json');
54+
if (fs.existsSync(packageJsonPath)) {
55+
return jestRunnerPath;
56+
}
57+
}
58+
}
59+
60+
// Fallback: Walk up from cwd looking for node_modules/jest-runner
61+
const monorepoMarkers = ['yarn.lock', 'pnpm-workspace.yaml', 'lerna.json', 'package-lock.json'];
62+
let currentDir = process.cwd();
63+
const visitedDirs = new Set();
64+
65+
while (currentDir !== path.dirname(currentDir)) {
66+
// Avoid infinite loops
67+
if (visitedDirs.has(currentDir)) break;
68+
visitedDirs.add(currentDir);
69+
70+
// Try node_modules/jest-runner at this level
71+
const jestRunnerPath = path.join(currentDir, 'node_modules', 'jest-runner');
72+
if (fs.existsSync(jestRunnerPath)) {
73+
const packageJsonPath = path.join(jestRunnerPath, 'package.json');
74+
if (fs.existsSync(packageJsonPath)) {
75+
return jestRunnerPath;
76+
}
77+
}
78+
79+
// Check if this is a workspace root (has monorepo markers)
80+
const isWorkspaceRoot = monorepoMarkers.some(marker =>
81+
fs.existsSync(path.join(currentDir, marker))
82+
);
83+
84+
if (isWorkspaceRoot) {
85+
// Found workspace root but no jest-runner - stop searching
86+
break;
87+
}
88+
89+
currentDir = path.dirname(currentDir);
90+
}
91+
92+
throw new Error('jest-runner not found');
93+
}
3394

3495
// Try to load jest-runner - it's a peer dependency that must be installed by the user
3596
let runTest;
3697
let jestRunnerAvailable = false;
3798

3899
try {
39-
const jestRunnerPath = require.resolve('jest-runner');
100+
const jestRunnerPath = resolveJestRunner();
40101
const internalRequire = createRequire(jestRunnerPath);
41102
runTest = internalRequire('./runTest').default;
42103
jestRunnerAvailable = true;

0 commit comments

Comments
 (0)