mirror of
https://github.com/codeflash-ai/codeflash.git
synced 2026-05-04 18:25:17 +00:00
Merge branch 'main' into dependabot/uv/uv-0.11.6
This commit is contained in:
commit
970aeb4430
9 changed files with 546 additions and 24 deletions
38
.github/CODEOWNERS
vendored
Normal file
38
.github/CODEOWNERS
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Default fallback
|
||||
* @KRRT7
|
||||
|
||||
# Java
|
||||
/codeflash/languages/java/ @mashraf-222 @HeshamHM28 @misrasaurabh1
|
||||
|
||||
# JavaScript / TypeScript
|
||||
/codeflash/languages/javascript/ @Saga4 @mohammedahmed18 @KRRT7
|
||||
|
||||
# Python language support
|
||||
/codeflash/languages/python/ @KRRT7
|
||||
|
||||
# Core pipeline
|
||||
/codeflash/optimization/ @KRRT7 @aseembits93 @misrasaurabh1
|
||||
/codeflash/verification/ @KRRT7 @misrasaurabh1
|
||||
/codeflash/benchmarking/ @KRRT7
|
||||
/codeflash/discovery/ @KRRT7 @misrasaurabh1
|
||||
|
||||
# CLI & setup
|
||||
/codeflash/cli_cmds/ @KRRT7 @misrasaurabh1
|
||||
|
||||
# LSP
|
||||
/codeflash/lsp/ @mohammedahmed18
|
||||
|
||||
# API
|
||||
/codeflash/api/ @KRRT7 @aseembits93
|
||||
|
||||
# Tracing & entry points
|
||||
/codeflash/tracing/ @misrasaurabh1 @KRRT7
|
||||
/codeflash/main.py @misrasaurabh1 @KRRT7
|
||||
/codeflash/tracer.py @misrasaurabh1 @KRRT7
|
||||
|
||||
# Shared utilities
|
||||
/codeflash/code_utils/ @KRRT7 @aseembits93 @misrasaurabh1
|
||||
/codeflash/models/ @KRRT7
|
||||
|
||||
# CI / workflows
|
||||
/.github/ @KRRT7
|
||||
18
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
18
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
## Linked issue or discussion
|
||||
|
||||
<!-- Every PR must link to an issue or discussion — this ensures the approach has been discussed with maintainers before implementation begins, so your work fits the project's direction and doesn't need to be reworked. -->
|
||||
<!-- Replace the line below with one of: -->
|
||||
<!-- Closes #<number> -->
|
||||
<!-- Fixes #<number> -->
|
||||
<!-- Relates to #<number> -->
|
||||
<!-- Discussion: <url> -->
|
||||
|
||||
**Required:** <!-- CI will fail if no linked issue or discussion is found. -->
|
||||
|
||||
## What changed
|
||||
|
||||
<!-- Brief description of the changes. -->
|
||||
|
||||
## Test plan
|
||||
|
||||
<!-- How was this tested? Link to passing CI, new tests, or manual verification steps. -->
|
||||
52
.github/workflows/ci.yaml
vendored
52
.github/workflows/ci.yaml
vendored
|
|
@ -22,6 +22,57 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Linked issue check — every PR must reference an issue or discussion.
|
||||
# Skipped on push to main and workflow_dispatch.
|
||||
# ---------------------------------------------------------------------------
|
||||
check-linked-issue:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Check PR body for linked issue or discussion
|
||||
env:
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
run: |
|
||||
# Skip for bots (dependabot, renovate, github-actions)
|
||||
if [[ "$PR_AUTHOR" == *"[bot]"* || "$PR_AUTHOR" == "dependabot" ]]; then
|
||||
echo "Bot PR — skipping linked issue check."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -z "$PR_BODY" ]; then
|
||||
echo "::error::PR body is empty. Every PR must link an issue or discussion."
|
||||
echo "Use 'Closes #<number>', 'Fixes #<number>', 'Relates to #<number>', or include a discussion URL."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Match: #123, GH-123, org/repo#123, Closes/Fixes/Relates/Resolves #123,
|
||||
# or a github.com URL to an issue or discussion
|
||||
if echo "$PR_BODY" | grep -qiP '(close[sd]?|fix(e[sd])?|relate[sd]?\s+to|resolve[sd]?)\s+#\d+'; then
|
||||
echo "Found linked issue keyword."
|
||||
exit 0
|
||||
fi
|
||||
if echo "$PR_BODY" | grep -qP '#\d+'; then
|
||||
echo "Found issue reference."
|
||||
exit 0
|
||||
fi
|
||||
if echo "$PR_BODY" | grep -qiP 'github\.com/[^\s]+/(issues|discussions)/\d+'; then
|
||||
echo "Found GitHub issue/discussion URL."
|
||||
exit 0
|
||||
fi
|
||||
if echo "$PR_BODY" | grep -qiP 'CF-#?\d+'; then
|
||||
echo "Found Linear ticket reference."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "::error::No linked issue or discussion found in PR body."
|
||||
echo "Every PR must reference an issue or discussion. See CONTRIBUTING.md for details."
|
||||
echo "Use 'Closes #<number>', 'Fixes #<number>', 'Relates to #<number>', or include a discussion URL."
|
||||
exit 1
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Change detection — decides which downstream jobs actually run.
|
||||
# On push/workflow_dispatch every flag is true so all jobs execute.
|
||||
|
|
@ -506,6 +557,7 @@ jobs:
|
|||
name: required checks passed
|
||||
if: always()
|
||||
needs:
|
||||
- check-linked-issue
|
||||
- unit-tests
|
||||
- type-check
|
||||
- prek
|
||||
|
|
|
|||
157
CONTRIBUTING.md
Normal file
157
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
# Contributing to Codeflash
|
||||
|
||||
Thanks for your interest in contributing. This guide covers both contributing changes back to Codeflash itself and running Codeflash from this repository in editable mode to optimize your own projects.
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Quick links](#quick-links)
|
||||
- [Ways to contribute](#ways-to-contribute)
|
||||
- [Development environment](#development-environment)
|
||||
- [Running tests and checks](#running-tests-and-checks)
|
||||
- [Code style](#code-style)
|
||||
- [Branches, commits, and pull requests](#branches-commits-and-pull-requests)
|
||||
- [Using Codeflash in editable mode](#using-codeflash-in-editable-mode)
|
||||
- [Reporting bugs and requesting features](#reporting-bugs-and-requesting-features)
|
||||
- [Security issues](#security-issues)
|
||||
|
||||
## Quick links
|
||||
|
||||
- Issues: https://github.com/codeflash-ai/codeflash/issues
|
||||
- Discussions and support: [Discord](https://www.codeflash.ai/discord)
|
||||
- Documentation: https://docs.codeflash.ai
|
||||
- Security policy: [`SECURITY.md`](SECURITY.md)
|
||||
- Project conventions for AI agents and humans alike: [`CLAUDE.md`](CLAUDE.md)
|
||||
|
||||
## Ways to contribute
|
||||
|
||||
- **Bug reports**: open an issue with a minimal reproducer that fails on `main`.
|
||||
- **Bug fixes**: follow the bug-fix workflow in [`CLAUDE.md`](CLAUDE.md) - read the code, write a failing test, apply the fix, confirm the test now passes.
|
||||
- **Features**: open an issue first for anything non-trivial so the approach can be agreed before implementation.
|
||||
- **Documentation**: the full documentation lives at https://docs.codeflash.ai. Fixes to README, docstrings, and this guide can be submitted as PRs here.
|
||||
- **Language support**: Codeflash supports Python, JavaScript / TypeScript, and Java today. New language support is a significant effort - please start with an issue.
|
||||
|
||||
## Development environment
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.9 or newer
|
||||
- [`uv`](https://github.com/astral-sh/uv) for dependency management (required - do not use `pip` directly)
|
||||
- `git`
|
||||
- For JavaScript end-to-end tests: Node.js and `npm`
|
||||
- For Java end-to-end tests: a JDK (see `.github/workflows/java-e2e.yaml` for the tested version)
|
||||
|
||||
### Setup
|
||||
|
||||
Fork the repository, clone your fork, and install the dev dependencies with `uv`:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/<your-username>/codeflash.git
|
||||
cd codeflash
|
||||
uv sync
|
||||
```
|
||||
|
||||
`uv sync` installs Codeflash plus the `dev` dependency group (ruff, mypy, ipython, type stubs). The `codeflash` CLI is installed into the virtualenv and can be invoked via `uv run codeflash ...`.
|
||||
|
||||
### Optional: point at your fork's upstream
|
||||
|
||||
```bash
|
||||
git remote add upstream https://github.com/codeflash-ai/codeflash.git
|
||||
git fetch upstream
|
||||
```
|
||||
|
||||
## Running tests and checks
|
||||
|
||||
Use `uv run prek` as the single verification command. It runs ruff (lint + format), mypy (strict), and related checks in one pass, matching what CI runs.
|
||||
|
||||
```bash
|
||||
# Check every changed file against the pre-commit hooks locally
|
||||
uv run prek
|
||||
|
||||
# Match CI behavior: check everything changed against the PR base branch
|
||||
BASE=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || echo main)
|
||||
uv run prek run --from-ref origin/$BASE
|
||||
```
|
||||
|
||||
Run the test suite with pytest via `uv`:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/
|
||||
```
|
||||
|
||||
To run a subset:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/code_utils/ -k "test_something"
|
||||
```
|
||||
|
||||
End-to-end tests live under `code_to_optimize/` and are exercised by CI (`.github/workflows/ci.yaml`, `java-e2e.yaml`). They can be run locally by invoking the scripts referenced from those workflows if you have the relevant runtime installed.
|
||||
|
||||
## Code style
|
||||
|
||||
The full ruleset is in [`.claude/rules/code-style.md`](.claude/rules/code-style.md). Highlights:
|
||||
|
||||
- **Line length**: 120 characters.
|
||||
- **Python**: 3.9+ syntax. Use type annotations consistent with surrounding code.
|
||||
- **Package management**: `uv` only. Do not add dependencies with `pip install`.
|
||||
- **Docstrings**: do not add docstrings to new or changed code unless explicitly requested. The codebase intentionally keeps functions self-documenting through clear naming and type annotations.
|
||||
- **Naming**: no leading underscores on Python names (`_private` style). Python has no true private functions; use public names.
|
||||
- **File I/O**: always pass `encoding="utf-8"` to `open()`, `Path.read_text()`, `Path.write_text()`, and similar calls in new or changed code. Windows defaults to `cp1252`, which breaks on non-ASCII content.
|
||||
- **Paths**: prefer absolute paths internally.
|
||||
- **Verification**: `uv run prek` is the canonical check. Don't run `ruff`, `mypy`, or `python -c "import ..."` separately; `prek` handles them together.
|
||||
|
||||
## Branches, commits, and pull requests
|
||||
|
||||
- **Every PR must link an issue or discussion.** Use `Closes #<number>`, `Fixes #<number>`, or `Relates to #<number>` in the PR body. CI will fail if no linked issue or discussion is found. For trivial fixes (typos, formatting), open a lightweight issue first — it only takes a moment and keeps the history traceable. The goal is to have a conversation before the code — discussing the approach on an issue or discussion helps maintainers point you in the right direction early, so your implementation fits the project's needs and you don't spend time on work that gets reworked.
|
||||
- Create a feature branch off an up-to-date `main`. Never commit directly to `main`.
|
||||
- Use conventional-commit prefixes: `fix:`, `feat:`, `refactor:`, `docs:`, `test:`, `chore:`. Keep commit messages concise (1-2 sentence body max).
|
||||
- Keep commits atomic - one logical change per commit.
|
||||
- PR titles also use the conventional format. The PR body should be short and link the related issue.
|
||||
- If the change corresponds to a Linear ticket, include `CF-#<number>` in the PR body.
|
||||
- Run `uv run prek` (or `uv run prek run --from-ref origin/main`) before pushing. CI will block merge if hooks fail.
|
||||
|
||||
## Using Codeflash in editable mode
|
||||
|
||||
If you want to use Codeflash itself to optimize your own Python projects while developing or testing changes to Codeflash, you can install it in editable mode from this repository.
|
||||
|
||||
### Install as an editable dependency
|
||||
|
||||
From your target project's directory:
|
||||
|
||||
```bash
|
||||
# Using uv (recommended)
|
||||
uv add --editable /absolute/path/to/your/codeflash/checkout
|
||||
|
||||
# Or, if you use pip inside a virtualenv
|
||||
pip install -e /absolute/path/to/your/codeflash/checkout
|
||||
```
|
||||
|
||||
From the Codeflash repository root you can also run the CLI directly without installing into the target project:
|
||||
|
||||
```bash
|
||||
cd /absolute/path/to/your/codeflash/checkout
|
||||
uv run codeflash init # in the target project, cwd matters
|
||||
uv run codeflash --all # optimize the entire target codebase
|
||||
uv run codeflash optimize script.py
|
||||
```
|
||||
|
||||
You will still need a Codeflash API key - `uv run codeflash init` walks through key generation and GitHub app setup. See the [Quick Start in the README](README.md#quick-start) for the full flow.
|
||||
|
||||
### When to use editable mode
|
||||
|
||||
- You are iterating on a Codeflash change and want to dogfood it against a real codebase.
|
||||
- You need to reproduce a bug your target project hits, with your local patches applied.
|
||||
- You are developing a new optimization rule, heuristic, or language integration and want end-to-end coverage beyond `tests/`.
|
||||
|
||||
For day-to-day optimization of a project you are not hacking on Codeflash itself, install the released package from PyPI (`pip install codeflash` or `uv add codeflash`) instead.
|
||||
|
||||
## Reporting bugs and requesting features
|
||||
|
||||
Before filing a new issue, please:
|
||||
|
||||
1. Search existing [open and closed issues](https://github.com/codeflash-ai/codeflash/issues?q=is%3Aissue) to avoid duplicates.
|
||||
2. Include the Codeflash version (`codeflash --version`) and Python / uv versions.
|
||||
3. Include the smallest reproducer you can. For bugs, a failing test that exercises the behavior is ideal.
|
||||
|
||||
## Security issues
|
||||
|
||||
Do not report suspected security issues in public GitHub issues. See [`SECURITY.md`](SECURITY.md) for the reporting process.
|
||||
|
|
@ -533,7 +533,15 @@ class GradleStrategy(BuildToolStrategy):
|
|||
logger.error("Gradle not found — cannot pre-install multi-module dependencies")
|
||||
return False
|
||||
|
||||
cmd = [gradle, f":{test_module}:testClasses", "-x", "test", "--build-cache", "--no-daemon"]
|
||||
cmd = [
|
||||
gradle,
|
||||
f":{test_module}:testClasses",
|
||||
"-x",
|
||||
"test",
|
||||
"--build-cache",
|
||||
"--no-daemon",
|
||||
"--configure-on-demand",
|
||||
]
|
||||
cmd.extend(["--init-script", _get_skip_validation_init_script()])
|
||||
|
||||
logger.info("Pre-installing multi-module dependencies: %s (module: %s)", build_root, test_module)
|
||||
|
|
@ -568,9 +576,9 @@ class GradleStrategy(BuildToolStrategy):
|
|||
return subprocess.CompletedProcess(args=["gradle"], returncode=-1, stdout="", stderr="Gradle not found")
|
||||
|
||||
if test_module:
|
||||
cmd = [gradle, f":{test_module}:testClasses", "--no-daemon"]
|
||||
cmd = [gradle, f":{test_module}:testClasses", "--no-daemon", "--configure-on-demand"]
|
||||
else:
|
||||
cmd = [gradle, "testClasses", "--no-daemon"]
|
||||
cmd = [gradle, "testClasses", "--no-daemon", "--configure-on-demand"]
|
||||
cmd.extend(["--init-script", _get_skip_validation_init_script()])
|
||||
|
||||
logger.debug("Compiling tests: %s in %s", " ".join(cmd), build_root)
|
||||
|
|
@ -592,9 +600,9 @@ class GradleStrategy(BuildToolStrategy):
|
|||
return subprocess.CompletedProcess(args=["gradle"], returncode=-1, stdout="", stderr="Gradle not found")
|
||||
|
||||
if test_module:
|
||||
cmd = [gradle, f":{test_module}:classes", "--no-daemon"]
|
||||
cmd = [gradle, f":{test_module}:classes", "--no-daemon", "--configure-on-demand"]
|
||||
else:
|
||||
cmd = [gradle, "classes", "--no-daemon"]
|
||||
cmd = [gradle, "classes", "--no-daemon", "--configure-on-demand"]
|
||||
cmd.extend(["--init-script", _get_skip_validation_init_script()])
|
||||
|
||||
logger.debug("Compiling source only: %s in %s", " ".join(cmd), build_root)
|
||||
|
|
@ -638,7 +646,7 @@ class GradleStrategy(BuildToolStrategy):
|
|||
else:
|
||||
task = "codeflashPrintClasspath"
|
||||
|
||||
cmd = [gradle, "--init-script", init_script_path, task, "-q", "--no-daemon"]
|
||||
cmd = [gradle, "--init-script", init_script_path, task, "-q", "--no-daemon", "--configure-on-demand"]
|
||||
|
||||
logger.debug("Getting classpath: %s", " ".join(cmd))
|
||||
|
||||
|
|
@ -789,7 +797,7 @@ class GradleStrategy(BuildToolStrategy):
|
|||
with os.fdopen(init_fd, "w", encoding="utf-8") as f:
|
||||
f.write(init_script_content)
|
||||
|
||||
cmd = [gradle, task, "--no-daemon", "--rerun", "--init-script", init_path]
|
||||
cmd = [gradle, task, "--no-daemon", "--rerun", "--configure-on-demand", "--init-script", init_path]
|
||||
cmd.extend(["--init-script", _get_skip_validation_init_script()])
|
||||
|
||||
for class_filter in test_filter.split(","):
|
||||
|
|
@ -1044,7 +1052,7 @@ class GradleStrategy(BuildToolStrategy):
|
|||
raise ValueError(msg)
|
||||
|
||||
gradle = self.find_executable(project_root) or "gradle"
|
||||
cmd = [gradle, "test", "--no-daemon"]
|
||||
cmd = [gradle, "test", "--no-daemon", "--configure-on-demand"]
|
||||
if test_classes:
|
||||
for cls in test_classes:
|
||||
cmd.extend(["--tests", cls])
|
||||
|
|
|
|||
|
|
@ -18,17 +18,19 @@ logger = logging.getLogger(__name__)
|
|||
GRACEFUL_SHUTDOWN_WAIT = 5 # seconds to wait after SIGTERM before SIGKILL
|
||||
|
||||
|
||||
def _run_java_with_graceful_timeout(
|
||||
java_command: list[str], env: dict[str, str], timeout: int, stage_name: str
|
||||
) -> None:
|
||||
def _run_java_with_graceful_timeout(java_command: list[str], env: dict[str, str], timeout: int, stage_name: str) -> int:
|
||||
"""Run a Java command with graceful timeout handling.
|
||||
|
||||
Sends SIGTERM first (allowing JFR dump and shutdown hooks to run),
|
||||
then SIGKILL if the process doesn't exit within GRACEFUL_SHUTDOWN_WAIT seconds.
|
||||
|
||||
Returns the process exit code, or -1 if the process was killed due to timeout.
|
||||
"""
|
||||
if not timeout:
|
||||
subprocess.run(java_command, env=env, check=False)
|
||||
return
|
||||
result = subprocess.run(java_command, env=env, check=False)
|
||||
if result.returncode != 0:
|
||||
logger.warning("%s exited with code %d", stage_name, result.returncode)
|
||||
return result.returncode
|
||||
|
||||
import signal
|
||||
|
||||
|
|
@ -46,6 +48,11 @@ def _run_java_with_graceful_timeout(
|
|||
logger.warning("%s stage did not exit after SIGTERM, sending SIGKILL", stage_name)
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
return -1
|
||||
|
||||
if proc.returncode != 0:
|
||||
logger.warning("%s exited with code %d", stage_name, proc.returncode)
|
||||
return proc.returncode
|
||||
|
||||
|
||||
# --add-opens flags needed for Kryo serialization on Java 16+
|
||||
|
|
@ -85,12 +92,23 @@ class JavaTracer:
|
|||
combined_env = self.build_combined_env(jfr_file, config_path)
|
||||
|
||||
logger.info("Running combined JFR profiling + argument capture...")
|
||||
_run_java_with_graceful_timeout(java_command, combined_env, timeout, "Combined tracing")
|
||||
exit_code = _run_java_with_graceful_timeout(java_command, combined_env, timeout, "Combined tracing")
|
||||
|
||||
if not trace_db_path.exists():
|
||||
msg = (
|
||||
f"Combined tracing failed with exit code {exit_code} — trace database was not created at "
|
||||
f"{trace_db_path}. Cannot proceed without trace data."
|
||||
)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
if exit_code != 0:
|
||||
logger.warning(
|
||||
"Combined tracing exited with code %d but trace database was created — proceeding with partial data",
|
||||
exit_code,
|
||||
)
|
||||
|
||||
if not jfr_file.exists():
|
||||
logger.warning("JFR file was not created at %s", jfr_file)
|
||||
if not trace_db_path.exists():
|
||||
logger.error("Trace database was not created at %s", trace_db_path)
|
||||
|
||||
return trace_db_path, jfr_file
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,14 @@
|
|||
"getting-started/java-installation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Claude Code Plugin",
|
||||
"pages": [
|
||||
"claude-code-plugin/getting-started",
|
||||
"claude-code-plugin/usage-guide",
|
||||
"claude-code-plugin/troubleshooting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Using Codeflash",
|
||||
"pages": [
|
||||
|
|
@ -67,19 +75,15 @@
|
|||
"editor-plugins/vscode/configuration",
|
||||
"editor-plugins/vscode/troubleshooting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Claude Code Plugin",
|
||||
"pages": [
|
||||
"claude-code-plugin/getting-started",
|
||||
"claude-code-plugin/usage-guide",
|
||||
"claude-code-plugin/troubleshooting"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"banner": {
|
||||
"content": "New: Use Codeflash directly inside Claude Code with the [Codeflash Claude Code Plugin](/claude-code-plugin/getting-started).",
|
||||
"dismissible": true
|
||||
},
|
||||
"logo": {
|
||||
"light": "/images/codeflash_light.svg",
|
||||
"dark": "/images/codeflash_darkmode.svg"
|
||||
|
|
|
|||
76
tests/test_languages/test_java/test_gradle_strategy.py
Normal file
76
tests/test_languages/test_java/test_gradle_strategy.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from codeflash.languages.java.gradle_strategy import GradleStrategy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
COD_FLAG = "--configure-on-demand"
|
||||
MOCK_TARGET = "codeflash.languages.java.test_runner._run_cmd_kill_pg_on_timeout"
|
||||
|
||||
|
||||
class TestConfigureOnDemand:
|
||||
def test_compile_tests_includes_configure_on_demand(self, tmp_path: Path) -> None:
|
||||
strategy = GradleStrategy()
|
||||
with patch.object(strategy, "find_executable", return_value="gradlew"), patch(MOCK_TARGET) as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
||||
strategy.compile_tests(tmp_path, {}, test_module=None)
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert COD_FLAG in cmd
|
||||
|
||||
def test_compile_tests_multimodule_includes_configure_on_demand(self, tmp_path: Path) -> None:
|
||||
strategy = GradleStrategy()
|
||||
with patch.object(strategy, "find_executable", return_value="gradlew"), patch(MOCK_TARGET) as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
||||
strategy.compile_tests(tmp_path, {}, test_module="core")
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert COD_FLAG in cmd
|
||||
assert ":core:testClasses" in cmd
|
||||
|
||||
def test_compile_source_only_includes_configure_on_demand(self, tmp_path: Path) -> None:
|
||||
strategy = GradleStrategy()
|
||||
with patch.object(strategy, "find_executable", return_value="gradlew"), patch(MOCK_TARGET) as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
||||
strategy.compile_source_only(tmp_path, {}, test_module=None)
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert COD_FLAG in cmd
|
||||
|
||||
def test_get_test_run_command_includes_configure_on_demand(self, tmp_path: Path) -> None:
|
||||
strategy = GradleStrategy()
|
||||
with patch.object(strategy, "find_executable", return_value="gradlew"):
|
||||
cmd = strategy.get_test_run_command(tmp_path)
|
||||
assert COD_FLAG in cmd
|
||||
|
||||
def test_install_multi_module_deps_includes_configure_on_demand(self, tmp_path: Path) -> None:
|
||||
strategy = GradleStrategy()
|
||||
with (
|
||||
patch.object(strategy, "find_executable", return_value="gradlew"),
|
||||
patch(MOCK_TARGET) as mock_run,
|
||||
patch("codeflash.languages.java.gradle_strategy._multimodule_deps_installed", set()),
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
||||
strategy.install_multi_module_deps(tmp_path, test_module="core", env={})
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert COD_FLAG in cmd
|
||||
assert ":core:testClasses" in cmd
|
||||
|
||||
def test_run_tests_via_build_tool_includes_configure_on_demand(self, tmp_path: Path) -> None:
|
||||
strategy = GradleStrategy()
|
||||
reports_dir = tmp_path / "build" / "test-results" / "test"
|
||||
reports_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with patch.object(strategy, "find_executable", return_value="gradlew"), patch(MOCK_TARGET) as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
||||
strategy.run_tests_via_build_tool(
|
||||
build_root=tmp_path,
|
||||
test_paths=["com.example.TestFoo"],
|
||||
env={},
|
||||
timeout=60,
|
||||
mode="behavior",
|
||||
test_module=None,
|
||||
)
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert COD_FLAG in cmd
|
||||
151
tests/test_languages/test_java/test_tracer_exit_codes.py
Normal file
151
tests/test_languages/test_java/test_tracer_exit_codes.py
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from codeflash.languages.java.tracer import JavaTracer, _run_java_with_graceful_timeout
|
||||
|
||||
|
||||
class TestRunJavaWithGracefulTimeout:
|
||||
def test_returns_zero_on_success(self) -> None:
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
with patch("codeflash.languages.java.tracer.subprocess.run", return_value=mock_result):
|
||||
rc = _run_java_with_graceful_timeout(["java", "-version"], {}, 0, "test")
|
||||
assert rc == 0
|
||||
|
||||
def test_returns_nonzero_on_failure(self) -> None:
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 1
|
||||
with patch("codeflash.languages.java.tracer.subprocess.run", return_value=mock_result):
|
||||
rc = _run_java_with_graceful_timeout(["java", "-version"], {}, 0, "test")
|
||||
assert rc == 1
|
||||
|
||||
def test_returns_exit_code_137_oom_kill(self) -> None:
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 137
|
||||
with patch("codeflash.languages.java.tracer.subprocess.run", return_value=mock_result):
|
||||
rc = _run_java_with_graceful_timeout(["java", "-version"], {}, 0, "test")
|
||||
assert rc == 137
|
||||
|
||||
def test_timeout_path_returns_zero_on_success(self) -> None:
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.returncode = 0
|
||||
with patch("codeflash.languages.java.tracer.subprocess.Popen", return_value=mock_proc):
|
||||
rc = _run_java_with_graceful_timeout(["java", "-version"], {}, 60, "test")
|
||||
assert rc == 0
|
||||
|
||||
def test_timeout_path_returns_nonzero_on_failure(self) -> None:
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.returncode = 1
|
||||
with patch("codeflash.languages.java.tracer.subprocess.Popen", return_value=mock_proc):
|
||||
rc = _run_java_with_graceful_timeout(["java", "-version"], {}, 60, "test")
|
||||
assert rc == 1
|
||||
|
||||
def test_timeout_returns_negative_one(self) -> None:
|
||||
import subprocess
|
||||
|
||||
mock_proc = MagicMock()
|
||||
# First wait() times out, SIGTERM wait succeeds
|
||||
mock_proc.wait.side_effect = [
|
||||
subprocess.TimeoutExpired(cmd="java", timeout=60),
|
||||
None, # SIGTERM wait succeeds
|
||||
]
|
||||
with patch("codeflash.languages.java.tracer.subprocess.Popen", return_value=mock_proc):
|
||||
rc = _run_java_with_graceful_timeout(["java", "-version"], {}, 60, "test")
|
||||
assert rc == -1
|
||||
|
||||
def test_timeout_sends_sigterm_then_sigkill(self) -> None:
|
||||
import signal
|
||||
import subprocess
|
||||
|
||||
mock_proc = MagicMock()
|
||||
# First wait() times out, SIGTERM wait also times out
|
||||
mock_proc.wait.side_effect = [
|
||||
subprocess.TimeoutExpired(cmd="java", timeout=60),
|
||||
subprocess.TimeoutExpired(cmd="java", timeout=5),
|
||||
None,
|
||||
]
|
||||
with patch("codeflash.languages.java.tracer.subprocess.Popen", return_value=mock_proc):
|
||||
rc = _run_java_with_graceful_timeout(["java", "-version"], {}, 60, "test")
|
||||
|
||||
assert rc == -1
|
||||
mock_proc.send_signal.assert_called_once_with(signal.SIGTERM)
|
||||
mock_proc.kill.assert_called_once()
|
||||
|
||||
|
||||
class TestJavaTracerExitCodeHandling:
|
||||
def test_success_with_trace_db_created(self, tmp_path: Path) -> None:
|
||||
trace_db_path = (tmp_path / "trace.db").resolve()
|
||||
tracer = JavaTracer()
|
||||
|
||||
def mock_run_timeout(java_command: list[str], env: dict, timeout: int, stage_name: str) -> int:
|
||||
trace_db_path.write_bytes(b"fake-db")
|
||||
return 0
|
||||
|
||||
with (
|
||||
patch("codeflash.languages.java.tracer._run_java_with_graceful_timeout", side_effect=mock_run_timeout),
|
||||
patch.object(tracer, "build_combined_env", return_value={}),
|
||||
patch.object(tracer, "create_tracer_config", return_value=tmp_path / "config.json"),
|
||||
):
|
||||
trace_db, _jfr_file = tracer.trace(
|
||||
java_command=["java", "-cp", ".", "Main"], trace_db_path=trace_db_path, packages=["com.example"]
|
||||
)
|
||||
assert trace_db == trace_db_path
|
||||
|
||||
def test_failure_without_trace_db_raises(self, tmp_path: Path) -> None:
|
||||
trace_db_path = (tmp_path / "trace.db").resolve()
|
||||
tracer = JavaTracer()
|
||||
|
||||
def mock_run_timeout(java_command: list[str], env: dict, timeout: int, stage_name: str) -> int:
|
||||
return 1
|
||||
|
||||
with (
|
||||
patch("codeflash.languages.java.tracer._run_java_with_graceful_timeout", side_effect=mock_run_timeout),
|
||||
patch.object(tracer, "build_combined_env", return_value={}),
|
||||
patch.object(tracer, "create_tracer_config", return_value=tmp_path / "config.json"),
|
||||
pytest.raises(RuntimeError, match="Combined tracing failed with exit code 1"),
|
||||
):
|
||||
tracer.trace(
|
||||
java_command=["java", "-cp", ".", "Main"], trace_db_path=trace_db_path, packages=["com.example"]
|
||||
)
|
||||
|
||||
def test_nonzero_exit_with_trace_db_continues(self, tmp_path: Path) -> None:
|
||||
trace_db_path = (tmp_path / "trace.db").resolve()
|
||||
tracer = JavaTracer()
|
||||
|
||||
def mock_run_timeout(java_command: list[str], env: dict, timeout: int, stage_name: str) -> int:
|
||||
trace_db_path.write_bytes(b"fake-db")
|
||||
return 1
|
||||
|
||||
with (
|
||||
patch("codeflash.languages.java.tracer._run_java_with_graceful_timeout", side_effect=mock_run_timeout),
|
||||
patch.object(tracer, "build_combined_env", return_value={}),
|
||||
patch.object(tracer, "create_tracer_config", return_value=tmp_path / "config.json"),
|
||||
):
|
||||
trace_db, _jfr_file = tracer.trace(
|
||||
java_command=["java", "-cp", ".", "Main"], trace_db_path=trace_db_path, packages=["com.example"]
|
||||
)
|
||||
assert trace_db == trace_db_path
|
||||
|
||||
def test_timeout_without_trace_db_raises(self, tmp_path: Path) -> None:
|
||||
trace_db_path = (tmp_path / "trace.db").resolve()
|
||||
tracer = JavaTracer()
|
||||
|
||||
def mock_run_timeout(java_command: list[str], env: dict, timeout: int, stage_name: str) -> int:
|
||||
return -1
|
||||
|
||||
with (
|
||||
patch("codeflash.languages.java.tracer._run_java_with_graceful_timeout", side_effect=mock_run_timeout),
|
||||
patch.object(tracer, "build_combined_env", return_value={}),
|
||||
patch.object(tracer, "create_tracer_config", return_value=tmp_path / "config.json"),
|
||||
pytest.raises(RuntimeError, match="Combined tracing failed with exit code -1"),
|
||||
):
|
||||
tracer.trace(
|
||||
java_command=["java", "-cp", ".", "Main"], trace_db_path=trace_db_path, packages=["com.example"]
|
||||
)
|
||||
Loading…
Reference in a new issue