Skip to content

Commit 33f15e6

Browse files
committed
Merge branch 'main' into chore/asyncio-optimization
2 parents e22d55e + c09a334 commit 33f15e6

25 files changed

+679
-374
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[tool.codeflash]
2+
# All paths are relative to this pyproject.toml's directory.
3+
module-root = "src/app"
4+
tests-root = "src/tests"
5+
test-framework = "pytest"
6+
ignore-paths = []
7+
disable-telemetry = true
8+
formatter-cmds = ["disabled"]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
def sorter(arr):
2+
print("codeflash stdout: Sorting list")
3+
for i in range(len(arr)):
4+
for j in range(len(arr) - 1):
5+
if arr[j] > arr[j + 1]:
6+
temp = arr[j]
7+
arr[j] = arr[j + 1]
8+
arr[j + 1] = temp
9+
print(f"result: {arr}")
10+
return arr

code_to_optimize/code_directories/nested_module_root/src/tests/.gitkeep

Whitespace-only changes.

codeflash/api/aiservice.py

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,30 @@
44
import os
55
import platform
66
import time
7-
from typing import TYPE_CHECKING, Any
7+
from pathlib import Path
8+
from typing import TYPE_CHECKING, Any, cast
89

910
import requests
1011
from pydantic.json import pydantic_encoder
1112

1213
from codeflash.cli_cmds.console import console, logger
14+
from codeflash.code_utils.code_replacer import is_zero_diff
15+
from codeflash.code_utils.code_utils import unified_diff_strings
1316
from codeflash.code_utils.config_consts import N_CANDIDATES_EFFECTIVE, N_CANDIDATES_LP_EFFECTIVE
1417
from codeflash.code_utils.env_utils import get_codeflash_api_key
1518
from codeflash.code_utils.git_utils import get_last_commit_author_if_pr_exists, get_repo_owner_and_name
19+
from codeflash.code_utils.time_utils import humanize_runtime
1620
from codeflash.lsp.helpers import is_LSP_enabled
1721
from codeflash.models.ExperimentMetadata import ExperimentMetadata
1822
from codeflash.models.models import AIServiceRefinerRequest, CodeStringsMarkdown, OptimizedCandidate
1923
from codeflash.telemetry.posthog_cf import ph
2024
from codeflash.version import __version__ as codeflash_version
2125

2226
if TYPE_CHECKING:
23-
from pathlib import Path
24-
2527
from codeflash.discovery.functions_to_optimize import FunctionToOptimize
2628
from codeflash.models.ExperimentMetadata import ExperimentMetadata
2729
from codeflash.models.models import AIServiceRefinerRequest
30+
from codeflash.result.explanation import Explanation
2831

2932

3033
class AiServiceClient:
@@ -529,6 +532,85 @@ def generate_regression_tests( # noqa: D417
529532
ph("cli-testgen-error-response", {"response_status_code": response.status_code, "error": response.text})
530533
return None
531534

535+
def get_optimization_impact(
536+
self,
537+
original_code: dict[Path, str],
538+
new_code: dict[Path, str],
539+
explanation: Explanation,
540+
existing_tests_source: str,
541+
generated_original_test_source: str,
542+
function_trace_id: str,
543+
coverage_message: str,
544+
replay_tests: str,
545+
root_dir: Path,
546+
concolic_tests: str, # noqa: ARG002
547+
) -> str:
548+
"""Compute the optimization impact of current Pull Request.
549+
550+
Args:
551+
original_code: dict -> data structure mapping file paths to function definition for original code
552+
new_code: dict -> data structure mapping file paths to function definition for optimized code
553+
explanation: Explanation -> data structure containing runtime information
554+
existing_tests_source: str -> existing tests table
555+
generated_original_test_source: str -> annotated generated tests
556+
function_trace_id: str -> traceid of function
557+
coverage_message: str -> coverage information
558+
replay_tests: str -> replay test table
559+
root_dir: Path -> path of git directory
560+
concolic_tests: str -> concolic_tests (not used)
561+
562+
Returns:
563+
-------
564+
- 'high' or 'low' optimization impact
565+
566+
"""
567+
diff_str = "\n".join(
568+
[
569+
unified_diff_strings(
570+
code1=original_code[p],
571+
code2=new_code[p],
572+
fromfile=Path(p).relative_to(root_dir).as_posix(),
573+
tofile=Path(p).relative_to(root_dir).as_posix(),
574+
)
575+
for p in original_code
576+
if not is_zero_diff(original_code[p], new_code[p])
577+
]
578+
)
579+
code_diff = f"```diff\n{diff_str}\n```"
580+
logger.info("!lsp|Computing Optimization Impact…")
581+
payload = {
582+
"code_diff": code_diff,
583+
"explanation": explanation.raw_explanation_message,
584+
"existing_tests": existing_tests_source,
585+
"generated_tests": generated_original_test_source,
586+
"trace_id": function_trace_id,
587+
"coverage_message": coverage_message,
588+
"replay_tests": replay_tests,
589+
"speedup": f"{(100 * float(explanation.speedup)):.2f}%",
590+
"loop_count": explanation.winning_benchmarking_test_results.number_of_loops(),
591+
"benchmark_details": explanation.benchmark_details if explanation.benchmark_details else None,
592+
"optimized_runtime": humanize_runtime(explanation.best_runtime_ns),
593+
"original_runtime": humanize_runtime(explanation.original_runtime_ns),
594+
}
595+
console.rule()
596+
try:
597+
response = self.make_ai_service_request("/optimization_impact", payload=payload, timeout=600)
598+
except requests.exceptions.RequestException as e:
599+
logger.exception(f"Error generating optimization refinements: {e}")
600+
ph("cli-optimize-error-caught", {"error": str(e)})
601+
return ""
602+
603+
if response.status_code == 200:
604+
return cast("str", response.json()["impact"])
605+
try:
606+
error = cast("str", response.json()["error"])
607+
except Exception:
608+
error = response.text
609+
logger.error(f"Error generating impact candidates: {response.status_code} - {error}")
610+
ph("cli-optimize-error-response", {"response_status_code": response.status_code, "error": error})
611+
console.rule()
612+
return ""
613+
532614

533615
class LocalAiServiceClient(AiServiceClient):
534616
"""Client for interacting with the local AI service."""

codeflash/code_utils/coverage_utils.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212

1313
def extract_dependent_function(main_function: str, code_context: CodeOptimizationContext) -> str | Literal[False]:
1414
"""Extract the single dependent function from the code context excluding the main function."""
15-
ast_tree = ast.parse(code_context.testgen_context_code)
16-
17-
dependent_functions = {
18-
node.name for node in ast_tree.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
19-
}
15+
dependent_functions = set()
16+
for code_string in code_context.testgen_context.code_strings:
17+
ast_tree = ast.parse(code_string.code)
18+
dependent_functions.update(
19+
{node.name for node in ast_tree.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))}
20+
)
2021

2122
if main_function in dependent_functions:
2223
dependent_functions.discard(main_function)

codeflash/code_utils/git_worktree_utils.py

Lines changed: 5 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import json
43
import subprocess
54
import tempfile
65
import time
@@ -9,15 +8,12 @@
98
from typing import TYPE_CHECKING, Optional
109

1110
import git
12-
from filelock import FileLock
1311

1412
from codeflash.cli_cmds.console import logger
1513
from codeflash.code_utils.compat import codeflash_cache_dir
1614
from codeflash.code_utils.git_utils import check_running_in_git_repo, git_root_dir
1715

1816
if TYPE_CHECKING:
19-
from typing import Any
20-
2117
from git import Repo
2218

2319

@@ -100,71 +96,24 @@ def get_patches_dir_for_project() -> Path:
10096
return Path(patches_dir / project_id)
10197

10298

103-
def get_patches_metadata() -> dict[str, Any]:
104-
project_patches_dir = get_patches_dir_for_project()
105-
meta_file = project_patches_dir / "metadata.json"
106-
if meta_file.exists():
107-
with meta_file.open("r", encoding="utf-8") as f:
108-
return json.load(f)
109-
return {"id": get_git_project_id() or "", "patches": []}
110-
111-
112-
def save_patches_metadata(patch_metadata: dict) -> dict:
113-
project_patches_dir = get_patches_dir_for_project()
114-
meta_file = project_patches_dir / "metadata.json"
115-
lock_file = project_patches_dir / "metadata.json.lock"
116-
117-
# we are not supporting multiple concurrent optimizations within the same process, but keep that in case we decide to do so in the future.
118-
with FileLock(lock_file, timeout=10):
119-
metadata = get_patches_metadata()
120-
121-
patch_metadata["id"] = time.strftime("%Y%m%d-%H%M%S")
122-
metadata["patches"].append(patch_metadata)
123-
124-
meta_file.write_text(json.dumps(metadata, indent=2))
125-
126-
return patch_metadata
127-
128-
129-
def overwrite_patch_metadata(patches: list[dict]) -> bool:
130-
project_patches_dir = get_patches_dir_for_project()
131-
meta_file = project_patches_dir / "metadata.json"
132-
lock_file = project_patches_dir / "metadata.json.lock"
133-
134-
with FileLock(lock_file, timeout=10):
135-
metadata = get_patches_metadata()
136-
metadata["patches"] = patches
137-
meta_file.write_text(json.dumps(metadata, indent=2))
138-
return True
139-
140-
14199
def create_diff_patch_from_worktree(
142-
worktree_dir: Path,
143-
files: list[str],
144-
fto_name: Optional[str] = None,
145-
metadata_input: Optional[dict[str, Any]] = None,
146-
) -> dict[str, Any]:
100+
worktree_dir: Path, files: list[str], fto_name: Optional[str] = None
101+
) -> Optional[Path]:
147102
repository = git.Repo(worktree_dir, search_parent_directories=True)
148103
uni_diff_text = repository.git.diff(None, "HEAD", *files, ignore_blank_lines=True, ignore_space_at_eol=True)
149104

150105
if not uni_diff_text:
151106
logger.warning("No changes found in worktree.")
152-
return {}
107+
return None
153108

154109
if not uni_diff_text.endswith("\n"):
155110
uni_diff_text += "\n"
156111

157112
project_patches_dir = get_patches_dir_for_project()
158113
project_patches_dir.mkdir(parents=True, exist_ok=True)
159114

160-
final_function_name = fto_name or metadata_input.get("fto_name", "unknown")
161-
patch_path = project_patches_dir / f"{worktree_dir.name}.{final_function_name}.patch"
115+
patch_path = project_patches_dir / f"{worktree_dir.name}.{fto_name}.patch"
162116
with patch_path.open("w", encoding="utf8") as f:
163117
f.write(uni_diff_text)
164118

165-
final_metadata = {"patch_path": str(patch_path)}
166-
if metadata_input:
167-
final_metadata.update(metadata_input)
168-
final_metadata = save_patches_metadata(final_metadata)
169-
170-
return final_metadata
119+
return patch_path

codeflash/context/code_context_extractor.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,32 +114,32 @@ def get_code_optimization_context(
114114
read_only_context_code = ""
115115

116116
# Extract code context for testgen
117-
testgen_code_markdown = extract_code_string_context_from_files(
117+
testgen_context = extract_code_markdown_context_from_files(
118118
helpers_of_fto_dict,
119119
helpers_of_helpers_dict,
120120
project_root_path,
121121
remove_docstrings=False,
122122
code_context_type=CodeContextType.TESTGEN,
123123
)
124-
testgen_context_code = testgen_code_markdown.code
125-
testgen_context_code_tokens = encoded_tokens_len(testgen_context_code)
126-
if testgen_context_code_tokens > testgen_token_limit:
127-
testgen_code_markdown = extract_code_string_context_from_files(
124+
testgen_markdown_code = testgen_context.markdown
125+
testgen_code_token_length = encoded_tokens_len(testgen_markdown_code)
126+
if testgen_code_token_length > testgen_token_limit:
127+
testgen_context = extract_code_markdown_context_from_files(
128128
helpers_of_fto_dict,
129129
helpers_of_helpers_dict,
130130
project_root_path,
131131
remove_docstrings=True,
132132
code_context_type=CodeContextType.TESTGEN,
133133
)
134-
testgen_context_code = testgen_code_markdown.code
135-
testgen_context_code_tokens = encoded_tokens_len(testgen_context_code)
136-
if testgen_context_code_tokens > testgen_token_limit:
134+
testgen_markdown_code = testgen_context.markdown
135+
testgen_code_token_length = encoded_tokens_len(testgen_markdown_code)
136+
if testgen_code_token_length > testgen_token_limit:
137137
raise ValueError("Testgen code context has exceeded token limit, cannot proceed")
138138
code_hash_context = hashing_code_context.markdown
139139
code_hash = hashlib.sha256(code_hash_context.encode("utf-8")).hexdigest()
140140

141141
return CodeOptimizationContext(
142-
testgen_context_code=testgen_context_code,
142+
testgen_context=testgen_context,
143143
read_writable_code=final_read_writable_code,
144144
read_only_context_code=read_only_context_code,
145145
hashing_code_context=code_hash_context,

codeflash/context/unused_definition_remover.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ def _expand_qualified_functions(self) -> set[str]:
279279
# Find class methods and add their containing classes and dunder methods
280280
for qualified_name in list(self.qualified_function_names):
281281
if "." in qualified_name:
282-
class_name, method_name = qualified_name.split(".", 1)
282+
class_name, _method_name = qualified_name.split(".", 1)
283283

284284
# Add the class itself
285285
expanded.add(class_name)
@@ -511,7 +511,7 @@ def revert_unused_helper_functions(
511511
if not unused_helpers:
512512
return
513513

514-
logger.info(f"Reverting {len(unused_helpers)} unused helper function(s) to original definitions")
514+
logger.debug(f"Reverting {len(unused_helpers)} unused helper function(s) to original definitions")
515515

516516
# Group unused helpers by file path
517517
unused_helpers_by_file = defaultdict(list)

codeflash/discovery/discover_unit_tests.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from __future__ import annotations
33

44
import ast
5+
import enum
56
import hashlib
67
import os
78
import pickle
@@ -11,12 +12,11 @@
1112
import unittest
1213
from collections import defaultdict
1314
from pathlib import Path
14-
from typing import TYPE_CHECKING, Callable, Optional
15+
from typing import TYPE_CHECKING, Callable, Optional, final
1516

1617
if TYPE_CHECKING:
1718
from codeflash.discovery.functions_to_optimize import FunctionToOptimize
1819

19-
import pytest
2020
from pydantic.dataclasses import dataclass
2121
from rich.panel import Panel
2222
from rich.text import Text
@@ -35,6 +35,22 @@
3535
from codeflash.verification.verification_utils import TestConfig
3636

3737

38+
@final
39+
class PytestExitCode(enum.IntEnum): # don't need to import entire pytest just for this
40+
#: Tests passed.
41+
OK = 0
42+
#: Tests failed.
43+
TESTS_FAILED = 1
44+
#: pytest was interrupted.
45+
INTERRUPTED = 2
46+
#: An internal error got in the way.
47+
INTERNAL_ERROR = 3
48+
#: pytest was misused.
49+
USAGE_ERROR = 4
50+
#: pytest couldn't find tests.
51+
NO_TESTS_COLLECTED = 5
52+
53+
3854
@dataclass(frozen=True)
3955
class TestFunction:
4056
function_name: str
@@ -401,6 +417,7 @@ def discover_tests_pytest(
401417
with tmp_pickle_path.open(mode="rb") as f:
402418
exitcode, tests, pytest_rootdir = pickle.load(f)
403419
except Exception as e:
420+
tests, pytest_rootdir = [], None
404421
logger.exception(f"Failed to discover tests: {e}")
405422
exitcode = -1
406423
finally:
@@ -412,15 +429,15 @@ def discover_tests_pytest(
412429
error_section = match.group(1) if match else result.stdout
413430

414431
logger.warning(
415-
f"Failed to collect tests. Pytest Exit code: {exitcode}={pytest.ExitCode(exitcode).name}\n {error_section}"
432+
f"Failed to collect tests. Pytest Exit code: {exitcode}={PytestExitCode(exitcode).name}\n {error_section}"
416433
)
417434
if "ModuleNotFoundError" in result.stdout:
418435
match = ImportErrorPattern.search(result.stdout).group()
419436
panel = Panel(Text.from_markup(f"⚠️ {match} ", style="bold red"), expand=False)
420437
console.print(panel)
421438

422439
elif 0 <= exitcode <= 5:
423-
logger.warning(f"Failed to collect tests. Pytest Exit code: {exitcode}={pytest.ExitCode(exitcode).name}")
440+
logger.warning(f"Failed to collect tests. Pytest Exit code: {exitcode}={PytestExitCode(exitcode).name}")
424441
else:
425442
logger.warning(f"Failed to collect tests. Pytest Exit code: {exitcode}")
426443
console.rule()

0 commit comments

Comments
 (0)