-
Notifications
You must be signed in to change notification settings - Fork 16
ep local-test #327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ep local-test #327
Changes from 8 commits
cd9cc91
e7615d7
72b9178
2907cf8
4f1ff85
99169ab
75d4cb6
41b79da
5eb5fac
4a17784
9b476dc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| import argparse | ||
| import os | ||
| import subprocess | ||
| import sys | ||
| from typing import List | ||
|
|
||
| from .upload import _discover_tests, _prompt_select | ||
|
|
||
|
|
||
| def _find_dockerfiles(root: str) -> List[str]: | ||
| skip_dirs = {".venv", "venv", "node_modules", "dist", "build", "__pycache__", ".git", "vendor"} | ||
| dockerfiles: List[str] = [] | ||
| for dirpath, dirnames, filenames in os.walk(root): | ||
| dirnames[:] = [d for d in dirnames if d not in skip_dirs and not d.startswith(".")] | ||
| for name in filenames: | ||
| if name == "Dockerfile": | ||
| dockerfiles.append(os.path.join(dirpath, name)) | ||
| return dockerfiles | ||
|
|
||
|
|
||
| def _run_pytest_host(pytest_target: str) -> int: | ||
| print(f"Running locally: pytest {pytest_target} -vs") | ||
| proc = subprocess.run([sys.executable, "-m", "pytest", pytest_target, "-vs"]) | ||
| return proc.returncode | ||
|
|
||
|
|
||
| def _build_docker_image(dockerfile_path: str, image_tag: str) -> bool: | ||
| context_dir = os.path.dirname(dockerfile_path) | ||
| print(f"Building Docker image '{image_tag}' from {dockerfile_path} ...") | ||
| try: | ||
| proc = subprocess.run( | ||
| ["docker", "build", "-t", image_tag, "-f", dockerfile_path, context_dir], | ||
| stdout=subprocess.PIPE, | ||
| stderr=subprocess.STDOUT, | ||
| text=True, | ||
| ) | ||
| print(proc.stdout) | ||
| return proc.returncode == 0 | ||
| except FileNotFoundError: | ||
| print("Error: docker not found in PATH. Install Docker or use --ignore-docker.") | ||
| return False | ||
|
|
||
|
|
||
| def _run_pytest_in_docker(project_root: str, image_tag: str, pytest_target: str) -> int: | ||
| workdir = "/workspace" | ||
| # Host HOME logs directory to map into container | ||
| host_home = os.path.expanduser("~") | ||
| host_logs_dir = os.path.join(host_home, ".eval_protocol") | ||
| try: | ||
| os.makedirs(host_logs_dir, exist_ok=True) | ||
| except Exception: | ||
| pass | ||
| # Mount read-only is safer; but tests may write artifacts. Use read-write. | ||
| cmd = [ | ||
| "docker", | ||
| "run", | ||
| "--rm", | ||
| "-v", | ||
| f"{project_root}:{workdir}", | ||
| "-v", | ||
| f"{host_logs_dir}:/container_home/.eval_protocol", | ||
| "-e", | ||
| "HOME=/container_home", | ||
| "-e", | ||
| "EVAL_PROTOCOL_DIR=/container_home/.eval_protocol", | ||
| "-w", | ||
| workdir, | ||
| ] | ||
| # Try to match host user to avoid permission problems on mounted volume | ||
| try: | ||
| uid = os.getuid() # type: ignore[attr-defined] | ||
| gid = os.getgid() # type: ignore[attr-defined] | ||
| cmd += ["--user", f"{uid}:{gid}"] | ||
| except Exception: | ||
| pass | ||
| cmd += [image_tag, "pytest", pytest_target, "-vs"] | ||
| print("Running in Docker:", " ".join(cmd)) | ||
| try: | ||
| proc = subprocess.run(cmd) | ||
| return proc.returncode | ||
| except FileNotFoundError: | ||
| print("Error: docker not found in PATH. Install Docker or use --ignore-docker.") | ||
| return 1 | ||
|
|
||
|
|
||
| def local_test_command(args: argparse.Namespace) -> int: | ||
| project_root = os.getcwd() | ||
|
|
||
| # Selection and pytest target resolution | ||
| pytest_target: str = "" | ||
| entry = getattr(args, "entry", None) | ||
| if entry: | ||
| if "::" in entry: | ||
| file_part = entry.split("::", 1)[0] | ||
| file_path = ( | ||
| file_part if os.path.isabs(file_part) else os.path.abspath(os.path.join(project_root, file_part)) | ||
| ) | ||
| pytest_target = entry | ||
| else: | ||
| file_path = entry if os.path.isabs(entry) else os.path.abspath(os.path.join(project_root, entry)) | ||
| # Use path relative to project_root when possible | ||
| try: | ||
| rel = os.path.relpath(file_path, project_root) | ||
| except Exception: | ||
| rel = file_path | ||
| pytest_target = rel | ||
| else: | ||
| tests = _discover_tests(project_root) | ||
| if not tests: | ||
| print("No evaluation tests found.\nHint: Ensure @evaluation_test is applied.") | ||
| return 1 | ||
| non_interactive = bool(getattr(args, "yes", False)) | ||
| selected = _prompt_select(tests, non_interactive=non_interactive) | ||
| if not selected: | ||
| print("No tests selected.") | ||
| return 1 | ||
| if len(selected) != 1: | ||
| print("Error: Please select exactly one evaluation test for 'local-test'.") | ||
| return 1 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Non-interactive
|
||
| chosen = selected[0] | ||
| abs_path = os.path.abspath(chosen.file_path) | ||
| try: | ||
| rel = os.path.relpath(abs_path, project_root) | ||
| except Exception: | ||
| rel = abs_path | ||
| pytest_target = rel | ||
|
|
||
| ignore_docker = bool(getattr(args, "ignore_docker", False)) | ||
| if ignore_docker: | ||
| if not pytest_target: | ||
| print("Error: Failed to resolve a pytest target to run.") | ||
| return 1 | ||
| return _run_pytest_host(pytest_target) | ||
|
|
||
| dockerfiles = _find_dockerfiles(project_root) | ||
| if len(dockerfiles) > 1: | ||
| print("Error: Multiple Dockerfiles found. Only one Dockerfile is allowed for local-test.") | ||
| for df in dockerfiles: | ||
| print(f" - {df}") | ||
| print("Hint: use --ignore-docker to bypass Docker.") | ||
| return 1 | ||
| if len(dockerfiles) == 1: | ||
| # Ensure host home logs directory exists so container writes are visible to host ep logs | ||
| try: | ||
| os.makedirs(os.path.join(os.path.expanduser("~"), ".eval_protocol"), exist_ok=True) | ||
| except Exception: | ||
| pass | ||
| image_tag = "ep-evaluator:local" | ||
| ok = _build_docker_image(dockerfiles[0], image_tag) | ||
| if not ok: | ||
| print("Docker build failed. See logs above.") | ||
| return 1 | ||
| if not pytest_target: | ||
| print("Error: Failed to resolve a pytest target to run.") | ||
| return 1 | ||
| return _run_pytest_in_docker(project_root, image_tag, pytest_target) | ||
|
|
||
| # No Dockerfile: run on host | ||
| if not pytest_target: | ||
| print("Error: Failed to resolve a pytest target to run.") | ||
| return 1 | ||
| return _run_pytest_host(pytest_target) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| import os | ||
| from types import SimpleNamespace | ||
|
|
||
| import pytest | ||
|
|
||
|
|
||
| def test_local_test_runs_host_pytest_with_entry(tmp_path, monkeypatch): | ||
| project = tmp_path / "proj" | ||
| project.mkdir() | ||
| monkeypatch.chdir(project) | ||
|
|
||
| # Create a dummy test file | ||
| test_file = project / "metric" / "test_one.py" | ||
| test_file.parent.mkdir(parents=True, exist_ok=True) | ||
| test_file.write_text("def test_dummy():\n assert True\n", encoding="utf-8") | ||
|
|
||
| # Import module under test | ||
| from eval_protocol.cli_commands import local_test as lt | ||
|
|
||
| # Avoid Docker path | ||
| monkeypatch.setattr(lt, "_find_dockerfiles", lambda root: []) | ||
|
|
||
| captured = {"target": ""} | ||
|
|
||
| def _fake_host(target: str) -> int: | ||
| captured["target"] = target | ||
| return 0 | ||
|
|
||
| monkeypatch.setattr(lt, "_run_pytest_host", _fake_host) | ||
|
|
||
| args = SimpleNamespace(entry=str(test_file), ignore_docker=False, yes=True) | ||
| rc = lt.local_test_command(args) # pyright: ignore[reportArgumentType] | ||
| assert rc == 0 | ||
| # Expect relative path target | ||
| assert captured["target"] == os.path.relpath(str(test_file), str(project)) | ||
|
|
||
|
|
||
| def test_local_test_ignores_docker_when_flag_set(tmp_path, monkeypatch): | ||
| project = tmp_path / "proj" | ||
| project.mkdir() | ||
| monkeypatch.chdir(project) | ||
|
|
||
| test_file = project / "metric" / "test_two.py" | ||
| test_file.parent.mkdir(parents=True, exist_ok=True) | ||
| test_file.write_text("def test_dummy():\n assert True\n", encoding="utf-8") | ||
|
|
||
| from eval_protocol.cli_commands import local_test as lt | ||
|
|
||
| # Pretend we have Dockerfile(s), but ignore_docker=True should skip | ||
| monkeypatch.setattr(lt, "_find_dockerfiles", lambda root: [str(project / "Dockerfile")]) | ||
|
|
||
| called = {"host": False} | ||
|
|
||
| def _fake_host(target: str) -> int: | ||
| called["host"] = True | ||
| return 0 | ||
|
|
||
| monkeypatch.setattr(lt, "_run_pytest_host", _fake_host) | ||
|
|
||
| args = SimpleNamespace(entry=str(test_file), ignore_docker=True, yes=True) | ||
| rc = lt.local_test_command(args) # pyright: ignore[reportArgumentType] | ||
| assert rc == 0 | ||
| assert called["host"] is True | ||
|
|
||
|
|
||
| def test_local_test_errors_on_multiple_dockerfiles(tmp_path, monkeypatch): | ||
| project = tmp_path / "proj" | ||
| project.mkdir() | ||
| monkeypatch.chdir(project) | ||
|
|
||
| test_file = project / "metric" / "test_three.py" | ||
| test_file.parent.mkdir(parents=True, exist_ok=True) | ||
| test_file.write_text("def test_dummy():\n assert True\n", encoding="utf-8") | ||
|
|
||
| from eval_protocol.cli_commands import local_test as lt | ||
|
|
||
| monkeypatch.setattr( | ||
| lt, "_find_dockerfiles", lambda root: [str(project / "Dockerfile"), str(project / "another" / "Dockerfile")] | ||
| ) | ||
|
|
||
| args = SimpleNamespace(entry=str(test_file), ignore_docker=False, yes=True) | ||
| rc = lt.local_test_command(args) # pyright: ignore[reportArgumentType] | ||
| assert rc == 1 | ||
|
|
||
|
|
||
| def test_local_test_builds_and_runs_in_docker(tmp_path, monkeypatch): | ||
| project = tmp_path / "proj" | ||
| project.mkdir() | ||
| monkeypatch.chdir(project) | ||
|
|
||
| test_file = project / "metric" / "test_four.py" | ||
| test_file.parent.mkdir(parents=True, exist_ok=True) | ||
| test_file.write_text("def test_dummy():\n assert True\n", encoding="utf-8") | ||
|
|
||
| from eval_protocol.cli_commands import local_test as lt | ||
|
|
||
| monkeypatch.setattr(lt, "_find_dockerfiles", lambda root: [str(project / "Dockerfile")]) | ||
| monkeypatch.setattr(lt, "_build_docker_image", lambda dockerfile, tag, platform=None: True) | ||
|
cursor[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| captured = {"target": "", "image": ""} | ||
|
|
||
| def _fake_run_docker(root: str, image_tag: str, pytest_target: str, platform=None) -> int: | ||
|
cursor[bot] marked this conversation as resolved.
Outdated
|
||
| captured["target"] = pytest_target | ||
| captured["image"] = image_tag | ||
| return 0 | ||
|
|
||
| monkeypatch.setattr(lt, "_run_pytest_in_docker", _fake_run_docker) | ||
|
|
||
| args = SimpleNamespace(entry=str(test_file), ignore_docker=False, yes=True) | ||
| rc = lt.local_test_command(args) # pyright: ignore[reportArgumentType] | ||
| assert rc == 0 | ||
| assert captured["image"] == "ep-evaluator:local" | ||
| assert captured["target"] == os.path.relpath(str(test_file), str(project)) | ||
|
|
||
|
|
||
| def test_local_test_selector_single_test(tmp_path, monkeypatch): | ||
| project = tmp_path / "proj" | ||
| project.mkdir() | ||
| monkeypatch.chdir(project) | ||
|
|
||
| test_file = project / "metric" / "test_sel.py" | ||
| test_file.parent.mkdir(parents=True, exist_ok=True) | ||
| test_file.write_text("def test_dummy():\n assert True\n", encoding="utf-8") | ||
|
|
||
| from eval_protocol.cli_commands import local_test as lt | ||
| from eval_protocol.cli_commands import upload as up | ||
|
|
||
| # No entry; force discover + selector | ||
| disc = SimpleNamespace(qualname="metric.test_sel", file_path=str(test_file)) | ||
| monkeypatch.setattr(lt, "_discover_tests", lambda root: [disc]) | ||
| monkeypatch.setattr(up, "_prompt_select", lambda tests, non_interactive=False: tests[:1]) | ||
|
cursor[bot] marked this conversation as resolved.
Outdated
|
||
| monkeypatch.setattr(lt, "_find_dockerfiles", lambda root: []) | ||
|
|
||
| called = {"host": False} | ||
|
|
||
| def _fake_host(target: str) -> int: | ||
| called["host"] = True | ||
| return 0 | ||
|
|
||
| monkeypatch.setattr(lt, "_run_pytest_host", _fake_host) | ||
|
|
||
| args = SimpleNamespace(entry=None, ignore_docker=False, yes=True) | ||
| rc = lt.local_test_command(args) # pyright: ignore[reportArgumentType] | ||
| assert rc == 0 | ||
| assert called["host"] is True | ||
Uh oh!
There was an error while loading. Please reload this page.