diff --git a/codeflash/cli_cmds/init_javascript.py b/codeflash/cli_cmds/init_javascript.py index d88c69904..e43bdc167 100644 --- a/codeflash/cli_cmds/init_javascript.py +++ b/codeflash/cli_cmds/init_javascript.py @@ -128,7 +128,7 @@ def determine_js_package_manager(project_root: Path) -> JsPackageManager: """ # Search from project_root up to filesystem root for lock files # This supports monorepo setups where lock file is at workspace root - current_dir = project_root.resolve() + current_dir = project_root while current_dir != current_dir.parent: if (current_dir / "bun.lockb").exists() or (current_dir / "bun.lock").exists(): return JsPackageManager.BUN @@ -161,7 +161,7 @@ def find_node_modules_with_package(project_root: Path, package_name: str) -> Pat Path to the node_modules directory containing the package, or None if not found. """ - current_dir = project_root.resolve() + current_dir = project_root while current_dir != current_dir.parent: node_modules = current_dir / "node_modules" if node_modules.exists(): diff --git a/codeflash/code_utils/instrument_existing_tests.py b/codeflash/code_utils/instrument_existing_tests.py index 9486fc677..f15b2d56a 100644 --- a/codeflash/code_utils/instrument_existing_tests.py +++ b/codeflash/code_utils/instrument_existing_tests.py @@ -709,6 +709,7 @@ def inject_profiling_into_existing_test( tests_project_root: Path, mode: TestingMode = TestingMode.BEHAVIOR, ) -> tuple[bool, str | None]: + tests_project_root = tests_project_root.resolve() if function_to_optimize.is_async: return inject_async_profiling_into_existing_test( test_path, call_positions, function_to_optimize, tests_project_root, mode diff --git a/codeflash/discovery/discover_unit_tests.py b/codeflash/discovery/discover_unit_tests.py index d1ef28a8d..1a032ec36 100644 --- a/codeflash/discovery/discover_unit_tests.py +++ b/codeflash/discovery/discover_unit_tests.py @@ -69,8 +69,8 @@ class TestFunction: class TestsCache: SCHEMA_VERSION = 1 # Increment this when schema changes - def __init__(self, project_root_path: str | Path) -> None: - self.project_root_path = Path(project_root_path).resolve().as_posix() + def __init__(self, project_root_path: Path) -> None: + self.project_root_path = project_root_path.resolve().as_posix() self.connection = sqlite3.connect(codeflash_cache_db) self.cur = self.connection.cursor() diff --git a/codeflash/discovery/functions_to_optimize.py b/codeflash/discovery/functions_to_optimize.py index ed18ed53b..14455b890 100644 --- a/codeflash/discovery/functions_to_optimize.py +++ b/codeflash/discovery/functions_to_optimize.py @@ -836,6 +836,7 @@ def filter_functions( *, disable_logs: bool = False, ) -> tuple[dict[Path, list[FunctionToOptimize]], int]: + resolved_project_root = project_root.resolve() filtered_modified_functions: dict[str, list[FunctionToOptimize]] = {} blocklist_funcs = get_blocklisted_functions() logger.debug(f"Blocklisted functions: {blocklist_funcs}") @@ -912,7 +913,7 @@ def is_test_file(file_path_normalized: str) -> bool: lang_support = get_language_support(Path(file_path)) if lang_support.language == Language.PYTHON: try: - ast.parse(f"import {module_name_from_file_path(Path(file_path), project_root)}") + ast.parse(f"import {module_name_from_file_path(Path(file_path), resolved_project_root)}") except SyntaxError: malformed_paths_count += 1 continue @@ -934,7 +935,10 @@ def is_test_file(file_path_normalized: str) -> bool: if previous_checkpoint_functions: functions_tmp = [] for function in _functions: - if function.qualified_name_with_modules_from_root(project_root) in previous_checkpoint_functions: + if ( + function.qualified_name_with_modules_from_root(resolved_project_root) + in previous_checkpoint_functions + ): previous_checkpoint_functions_removed_count += 1 continue functions_tmp.append(function) diff --git a/codeflash/languages/javascript/import_resolver.py b/codeflash/languages/javascript/import_resolver.py index 8f5dbe8ca..b5ec67115 100644 --- a/codeflash/languages/javascript/import_resolver.py +++ b/codeflash/languages/javascript/import_resolver.py @@ -44,8 +44,7 @@ def __init__(self, project_root: Path) -> None: project_root: Root directory of the project. """ - # Resolve to real path to handle macOS symlinks like /var -> /private/var - self.project_root = project_root.resolve() + self.project_root = project_root self._resolution_cache: dict[tuple[Path, str], Path | None] = {} def resolve_import(self, import_info: ImportInfo, source_file: Path) -> ResolvedImport | None: diff --git a/codeflash/languages/python/context/code_context_extractor.py b/codeflash/languages/python/context/code_context_extractor.py index 09b045dcc..9a4daf726 100644 --- a/codeflash/languages/python/context/code_context_extractor.py +++ b/codeflash/languages/python/context/code_context_extractor.py @@ -2,7 +2,6 @@ import ast import hashlib -import os from collections import defaultdict from itertools import chain from pathlib import Path @@ -512,6 +511,10 @@ def get_function_sources_from_jedi( # TODO: there can be multiple definitions, see how to handle such cases definition = definitions[0] definition_path = definition.module_path + if definition_path is not None: + rel = safe_relative_to(definition_path, project_root_path) + if not rel.is_absolute(): + definition_path = project_root_path / rel # The definition is part of this project and not defined within the original function is_valid_definition = ( @@ -936,7 +939,11 @@ def is_project_path(module_path: Path | None, project_root_path: Path) -> bool: # site-packages must be checked first because .venv/site-packages is under project root if path_belongs_to_site_packages(module_path): return False - return str(module_path).startswith(str(project_root_path) + os.sep) + try: + module_path.resolve().relative_to(project_root_path.resolve()) + return True + except ValueError: + return False def _is_project_module(module_name: str, project_root_path: Path) -> bool: diff --git a/codeflash/languages/python/context/unused_definition_remover.py b/codeflash/languages/python/context/unused_definition_remover.py index 38b58f63e..ba6e4d549 100644 --- a/codeflash/languages/python/context/unused_definition_remover.py +++ b/codeflash/languages/python/context/unused_definition_remover.py @@ -587,17 +587,20 @@ def revert_unused_helper_functions( logger.debug(f"Reverting {len(unused_helpers)} unused helper function(s) to original definitions") + # Resolve all path keys for consistent comparison (Windows 8.3 short names may differ from Jedi-resolved paths) + resolved_original_helper_code = {p.resolve(): code for p, code in original_helper_code.items()} + # Group unused helpers by file path unused_helpers_by_file = defaultdict(list) for helper in unused_helpers: - unused_helpers_by_file[helper.file_path].append(helper) + unused_helpers_by_file[helper.file_path.resolve()].append(helper) # For each file, revert the unused helper functions to their original definitions for file_path, helpers_in_file in unused_helpers_by_file.items(): - if file_path in original_helper_code: + if file_path in resolved_original_helper_code: try: # Get original code for this file - original_code = original_helper_code[file_path] + original_code = resolved_original_helper_code[file_path] # Use the code replacer to selectively revert only the unused helper functions helper_names = [helper.qualified_name for helper in helpers_in_file] diff --git a/codeflash/optimization/function_optimizer.py b/codeflash/optimization/function_optimizer.py index 7cbcda976..c24d84ae5 100644 --- a/codeflash/optimization/function_optimizer.py +++ b/codeflash/optimization/function_optimizer.py @@ -2,6 +2,7 @@ import ast import concurrent.futures +import dataclasses import logging import os import queue @@ -443,9 +444,12 @@ def __init__( args: Namespace | None = None, replay_tests_dir: Path | None = None, ) -> None: - self.project_root = test_cfg.project_root_path + self.project_root = test_cfg.project_root_path.resolve() self.test_cfg = test_cfg self.aiservice_client = aiservice_client if aiservice_client else AiServiceClient() + resolved_file_path = function_to_optimize.file_path.resolve() + if resolved_file_path != function_to_optimize.file_path: + function_to_optimize = dataclasses.replace(function_to_optimize, file_path=resolved_file_path) self.function_to_optimize = function_to_optimize self.function_to_optimize_source_code = ( function_to_optimize_source_code @@ -1451,7 +1455,7 @@ def reformat_code_and_helpers( optimized_code = "" if optimized_context is not None: file_to_code_context = optimized_context.file_to_path() - optimized_code = file_to_code_context.get(str(path.relative_to(self.project_root)), "") + optimized_code = file_to_code_context.get(str(path.resolve().relative_to(self.project_root)), "") new_code = format_code( self.args.formatter_cmds, path, optimized_code=optimized_code, check_diff=True, exit_on_failure=False diff --git a/codeflash/result/create_pr.py b/codeflash/result/create_pr.py index 0be4e1cf8..b276725f2 100644 --- a/codeflash/result/create_pr.py +++ b/codeflash/result/create_pr.py @@ -126,10 +126,10 @@ def existing_tests_source_for( tests_dir_name = test_cfg.tests_project_rootdir.name if file_path.startswith((tests_dir_name + os.sep, tests_dir_name + "/")): # Module path includes "tests." - use project root parent - instrumented_abs_path = (test_cfg.tests_project_rootdir.parent / file_path).resolve() + instrumented_abs_path = test_cfg.tests_project_rootdir.parent / file_path else: # Module path doesn't include tests dir - use tests root directly - instrumented_abs_path = (test_cfg.tests_project_rootdir / file_path).resolve() + instrumented_abs_path = test_cfg.tests_project_rootdir / file_path logger.debug(f"[PR-DEBUG] Looking up: {instrumented_abs_path}") logger.debug(f"[PR-DEBUG] Available keys: {list(instrumented_to_original.keys())[:3]}") # Try to map instrumented path to original path diff --git a/codeflash/telemetry/sentry.py b/codeflash/telemetry/sentry.py index 2357439dc..3ee266326 100644 --- a/codeflash/telemetry/sentry.py +++ b/codeflash/telemetry/sentry.py @@ -2,6 +2,7 @@ import sentry_sdk from sentry_sdk.integrations.logging import LoggingIntegration +from sentry_sdk.integrations.stdlib import StdlibIntegration def init_sentry(*, enabled: bool = False, exclude_errors: bool = False) -> None: @@ -16,12 +17,8 @@ def init_sentry(*, enabled: bool = False, exclude_errors: bool = False) -> None: sentry_sdk.init( dsn="https://4b9a1902f9361b48c04376df6483bc96@o4506833230561280.ingest.sentry.io/4506833262477312", integrations=[sentry_logging], - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - traces_sample_rate=1.0, - # Set profiles_sample_rate to 1.0 to profile 100% - # of sampled transactions. - # We recommend adjusting this value in production. - profiles_sample_rate=1.0, + disabled_integrations=[StdlibIntegration], + traces_sample_rate=0, + profiles_sample_rate=0, ignore_errors=[KeyboardInterrupt], ) diff --git a/codeflash/verification/parse_test_output.py b/codeflash/verification/parse_test_output.py index 4c2c809eb..53012feb1 100644 --- a/codeflash/verification/parse_test_output.py +++ b/codeflash/verification/parse_test_output.py @@ -47,8 +47,24 @@ def parse_func(file_path: Path) -> XMLParser: return parse(file_path, xml_parser) -matches_re_start = re.compile(r"!\$######(.*?):(.*?)([^\.:]*?):(.*?):(.*?):(.*?)######\$!\n") -matches_re_end = re.compile(r"!######(.*?):(.*?)([^\.:]*?):(.*?):(.*?):(.*?)######!") +matches_re_start = re.compile( + r"!\$######([^:]*)" # group 1: module path + r":((?:[^:.]*\.)*)" # group 2: class prefix with trailing dot, or empty + r"([^.:]*)" # group 3: test function name + r":([^:]*)" # group 4: function being tested + r":([^:]*)" # group 5: loop index + r":([^#]*)" # group 6: iteration id + r"######\$!\n" +) +matches_re_end = re.compile( + r"!######([^:]*)" # group 1: module path + r":((?:[^:.]*\.)*)" # group 2: class prefix with trailing dot, or empty + r"([^.:]*)" # group 3: test function name + r":([^:]*)" # group 4: function being tested + r":([^:]*)" # group 5: loop index + r":([^#]*)" # group 6: iteration_id or iteration_id:runtime + r"######!" +) start_pattern = re.compile(r"!\$######([^:]*):([^:]*):([^:]*):([^:]*):([^:]+)######\$!") @@ -893,7 +909,6 @@ def merge_test_results( return merged_test_results -FAILURES_HEADER_RE = re.compile(r"=+ FAILURES =+") TEST_HEADER_RE = re.compile(r"_{3,}\s*(.*?)\s*_{3,}$") @@ -903,7 +918,7 @@ def parse_test_failures_from_stdout(stdout: str) -> dict[str, str]: start = end = None for i, line in enumerate(lines): - if FAILURES_HEADER_RE.search(line.strip()): + if "= FAILURES =" in line: start = i break diff --git a/codeflash/verification/verification_utils.py b/codeflash/verification/verification_utils.py index c567e6a9a..d586d9962 100644 --- a/codeflash/verification/verification_utils.py +++ b/codeflash/verification/verification_utils.py @@ -158,6 +158,10 @@ class TestConfig: _language: Optional[str] = None # Language identifier for multi-language support js_project_root: Optional[Path] = None # JavaScript project root (directory containing package.json) + def __post_init__(self) -> None: + self.project_root_path = self.project_root_path.resolve() + self.tests_project_rootdir = self.tests_project_rootdir.resolve() + @property def test_framework(self) -> str: """Returns the appropriate test framework based on language. diff --git a/pyproject.toml b/pyproject.toml index fe96c54cb..56736fc6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -207,6 +207,8 @@ warn_unreachable = true install_types = true plugins = ["pydantic.mypy"] +exclude = ["tests/", "code_to_optimize/", "pie_test_set/", "experiments/"] + [[tool.mypy.overrides]] module = ["jedi", "jedi.api.classes", "inquirer", "inquirer.themes", "numba"] ignore_missing_imports = true diff --git a/tests/test_parse_test_output_regex.py b/tests/test_parse_test_output_regex.py new file mode 100644 index 000000000..e313885ab --- /dev/null +++ b/tests/test_parse_test_output_regex.py @@ -0,0 +1,189 @@ +"""Tests for the regex patterns and string matching in parse_test_output.py.""" + +from codeflash.verification.parse_test_output import ( + matches_re_end, + matches_re_start, + parse_test_failures_from_stdout, +) + + +# --- matches_re_start tests --- + + +class TestMatchesReStart: + def test_simple_no_class(self) -> None: + s = "!$######tests.test_foo:test_bar:target_func:1:abc######$!\n" + m = matches_re_start.search(s) + assert m is not None + assert m.groups() == ("tests.test_foo", "", "test_bar", "target_func", "1", "abc") + + def test_with_class(self) -> None: + s = "!$######tests.test_foo:MyClass.test_bar:target_func:1:abc######$!\n" + m = matches_re_start.search(s) + assert m is not None + assert m.groups() == ("tests.test_foo", "MyClass.", "test_bar", "target_func", "1", "abc") + + def test_nested_class(self) -> None: + s = "!$######a.b.c:A.B.test_x:func:3:id123######$!\n" + m = matches_re_start.search(s) + assert m is not None + assert m.groups() == ("a.b.c", "A.B.", "test_x", "func", "3", "id123") + + def test_empty_class_and_function(self) -> None: + s = "!$######mod::func:0:iter######$!\n" + m = matches_re_start.search(s) + assert m is not None + assert m.groups() == ("mod", "", "", "func", "0", "iter") + + def test_embedded_in_stdout(self) -> None: + s = "some output\n!$######mod:test_fn:f:1:x######$!\nmore output\n" + m = matches_re_start.search(s) + assert m is not None + assert m.groups() == ("mod", "", "test_fn", "f", "1", "x") + + def test_multiple_matches(self) -> None: + s = ( + "!$######m1:C1.fn1:t1:1:a######$!\n" + "!$######m2:fn2:t2:2:b######$!\n" + ) + matches = list(matches_re_start.finditer(s)) + assert len(matches) == 2 + assert matches[0].groups() == ("m1", "C1.", "fn1", "t1", "1", "a") + assert matches[1].groups() == ("m2", "", "fn2", "t2", "2", "b") + + def test_no_match_without_newline(self) -> None: + s = "!$######mod:test_fn:f:1:x######$!" + m = matches_re_start.search(s) + assert m is None + + def test_dots_in_module_path(self) -> None: + s = "!$######a.b.c.d.e:test_fn:f:1:x######$!\n" + m = matches_re_start.search(s) + assert m is not None + assert m.group(1) == "a.b.c.d.e" + + +# --- matches_re_end tests --- + + +class TestMatchesReEnd: + def test_simple_no_class_with_runtime(self) -> None: + s = "!######tests.test_foo:test_bar:target_func:1:abc:12345######!" + m = matches_re_end.search(s) + assert m is not None + assert m.groups() == ("tests.test_foo", "", "test_bar", "target_func", "1", "abc:12345") + + def test_with_class_no_runtime(self) -> None: + s = "!######tests.test_foo:MyClass.test_bar:target_func:1:abc######!" + m = matches_re_end.search(s) + assert m is not None + assert m.groups() == ("tests.test_foo", "MyClass.", "test_bar", "target_func", "1", "abc") + + def test_nested_class_with_runtime(self) -> None: + s = "!######mod:A.B.test_x:func:3:id123:99999######!" + m = matches_re_end.search(s) + assert m is not None + assert m.groups() == ("mod", "A.B.", "test_x", "func", "3", "id123:99999") + + def test_runtime_colon_preserved_in_group6(self) -> None: + """Group 6 must capture 'iteration_id:runtime' as a single string (colon included).""" + s = "!######m:fn:f:1:iter42:98765######!" + m = matches_re_end.search(s) + assert m is not None + assert m.group(6) == "iter42:98765" + + def test_embedded_in_stdout(self) -> None: + s = "captured output\n!######mod:test_fn:f:1:x:500######!\nmore" + m = matches_re_end.search(s) + assert m is not None + assert m.groups() == ("mod", "", "test_fn", "f", "1", "x:500") + + +# --- Start/End pairing (simulates parse_test_xml matching logic) --- + + +class TestStartEndPairing: + def test_paired_markers(self) -> None: + stdout = ( + "!$######mod:Class.test_fn:func:1:iter1######$!\n" + "test output here\n" + "!######mod:Class.test_fn:func:1:iter1:54321######!" + ) + starts = list(matches_re_start.finditer(stdout)) + ends = {} + for match in matches_re_end.finditer(stdout): + groups = match.groups() + g5 = groups[5] + colon_pos = g5.find(":") + if colon_pos != -1: + key = groups[:5] + (g5[:colon_pos],) + else: + key = groups + ends[key] = match + + assert len(starts) == 1 + assert len(ends) == 1 + # Start and end should pair on the first 5 groups + iteration_id + start_groups = starts[0].groups() + assert start_groups in ends + + +# --- parse_test_failures_from_stdout tests --- + + +class TestParseTestFailuresHeader: + def test_standard_pytest_header(self) -> None: + stdout = ( + "..F.\n" + "=================================== FAILURES ===================================\n" + "_______ test_foo _______\n" + "\n" + " def test_foo():\n" + "> assert False\n" + "E AssertionError\n" + "\n" + "test.py:3: AssertionError\n" + "=========================== short test summary info ============================\n" + "FAILED test.py::test_foo\n" + ) + result = parse_test_failures_from_stdout(stdout) + assert "test_foo" in result + + def test_minimal_equals(self) -> None: + """Even a short '= FAILURES =' header should be detected.""" + stdout = ( + "= FAILURES =\n" + "_______ test_bar _______\n" + "\n" + " assert False\n" + "\n" + "test.py:1: AssertionError\n" + "= short test summary info =\n" + ) + result = parse_test_failures_from_stdout(stdout) + assert "test_bar" in result + + def test_no_failures_section(self) -> None: + stdout = "....\n4 passed in 0.1s\n" + result = parse_test_failures_from_stdout(stdout) + assert result == {} + + def test_word_failures_without_equals_is_not_matched(self) -> None: + """'FAILURES' without surrounding '=' signs should not trigger the header detection.""" + stdout = ( + "FAILURES detected in module\n" + "_______ test_baz _______\n" + "\n" + " assert False\n" + ) + result = parse_test_failures_from_stdout(stdout) + assert result == {} + + def test_failures_in_test_output_not_matched(self) -> None: + """A test printing 'FAILURES' (no = signs) should not trigger header detection.""" + stdout = ( + "Testing FAILURES handling\n" + "All good\n" + ) + result = parse_test_failures_from_stdout(stdout) + assert result == {}