Spaces:
Sleeping
Sleeping
| # Copyright (c) Meta Platforms, Inc. and affiliates. | |
| # All rights reserved. | |
| # | |
| # This source code is licensed under the BSD-style license found in the | |
| # LICENSE file in the root directory of this source tree. | |
| """Docker sandbox for running tests inside containers. | |
| Falls back to running tests via venv when Docker is not available. | |
| """ | |
| from __future__ import annotations | |
| import shutil | |
| import subprocess | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| class TestResult: | |
| """Result of a test execution.""" | |
| truncated_log: str = "" | |
| full_log: str | None = None | |
| exit_code: int | None = None | |
| def _is_docker_available() -> bool: | |
| """Check if Docker CLI exists and daemon is running.""" | |
| if not shutil.which("docker"): | |
| return False | |
| try: | |
| subprocess.run(["docker", "info"], capture_output=True, timeout=5, check=True) | |
| return True | |
| except Exception: | |
| return False | |
| def _truncate_log(full_log: str, max_lines: int = 100) -> str: | |
| """Truncate log to last N lines with line numbers.""" | |
| lines = full_log.splitlines(keepends=True) | |
| num_lines = len(lines) | |
| lines_to_show = lines[-max_lines:] | |
| start_line = num_lines - len(lines_to_show) + 1 | |
| truncated = "".join( | |
| f"{i}: {line}" | |
| for i, line in enumerate(lines_to_show, start=start_line) | |
| ) | |
| lines_before = max(0, num_lines - max_lines) | |
| if lines_before > 0: | |
| truncated = f"({lines_before} lines above)\n" + truncated | |
| return truncated.rstrip() | |
| class DockerSandbox: | |
| """Build Docker images and run containers with timeout/memory limits. | |
| Falls back to running tests via venv when Docker is not available. | |
| """ | |
| def __init__(self, timeout: int = 600, memory_limit: str = "16g") -> None: | |
| self.timeout = timeout | |
| self.memory_limit = memory_limit | |
| def run_tests(self, image_name: str, workspace_dir: str) -> TestResult: | |
| """Run tests. Uses Docker if available, otherwise runs via venv.""" | |
| if _is_docker_available(): | |
| return self._run_tests_docker(image_name, workspace_dir) | |
| else: | |
| return self._run_tests_venv(image_name, workspace_dir) | |
| def _run_tests_docker(self, image_name: str, workspace_dir: str) -> TestResult: | |
| """Run the container, capture output, truncate to last 100 lines.""" | |
| container_name = image_name | |
| try: | |
| command = [ | |
| "timeout", "--foreground", "-s", "KILL", str(self.timeout), | |
| "docker", "run", | |
| "-v", f"{workspace_dir}:/work", | |
| f"--memory={self.memory_limit}", | |
| "--name", container_name, | |
| image_name, | |
| ] | |
| result = subprocess.run( | |
| command, text=True, stdout=subprocess.PIPE, | |
| stderr=subprocess.STDOUT, check=False, | |
| ) | |
| return TestResult( | |
| truncated_log=_truncate_log(result.stdout), | |
| full_log=result.stdout, | |
| exit_code=result.returncode, | |
| ) | |
| except Exception as e: | |
| return TestResult(truncated_log=f"Test execution failed: {e}", full_log=None, exit_code=None) | |
| finally: | |
| try: | |
| self.remove_container(container_name) | |
| except Exception: | |
| pass | |
| def _run_tests_venv(self, image_name: str, workspace_dir: str) -> TestResult: | |
| """Run tests using the venv created during setup (no Docker).""" | |
| try: | |
| venv_python = str(Path(workspace_dir) / ".venv" / "bin" / "python") | |
| if not Path(venv_python).exists(): | |
| return TestResult( | |
| truncated_log="No venv found. Setup may have failed.", | |
| full_log=None, exit_code=1, | |
| ) | |
| # Find the test script: image_name is like "brmc__shortbus_new" | |
| base_name = image_name.replace("_new", "") | |
| test_script = Path(workspace_dir) / f"test_{base_name}.sh" | |
| if not test_script.exists(): | |
| candidates = list(Path(workspace_dir).glob("test_*.sh")) | |
| test_script = candidates[0] if candidates else None | |
| if not test_script or not test_script.exists(): | |
| return TestResult( | |
| truncated_log="No test script found in workspace.", | |
| full_log=None, exit_code=1, | |
| ) | |
| # Parse the test command from the script | |
| test_cmd = None | |
| with open(test_script) as f: | |
| for line in f: | |
| line = line.strip() | |
| if not line or line.startswith("#") or line.startswith("set "): | |
| continue | |
| test_cmd = line | |
| break | |
| if not test_cmd: | |
| return TestResult( | |
| truncated_log="Test script is empty.", | |
| full_log=None, exit_code=1, | |
| ) | |
| # Replace python with venv python | |
| test_cmd = test_cmd.replace("python3 ", f"{venv_python} ").replace("python ", f"{venv_python} ") | |
| print(f"[VENV-TEST] Running: {test_cmd}", flush=True) | |
| result = subprocess.run( | |
| test_cmd, shell=True, cwd=workspace_dir, | |
| text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, | |
| timeout=self.timeout, | |
| ) | |
| return TestResult( | |
| truncated_log=_truncate_log(result.stdout), | |
| full_log=result.stdout, | |
| exit_code=result.returncode, | |
| ) | |
| except subprocess.TimeoutExpired: | |
| return TestResult( | |
| truncated_log=f"Test execution timed out after {self.timeout}s.", | |
| full_log=None, exit_code=1, | |
| ) | |
| except Exception as e: | |
| return TestResult( | |
| truncated_log=f"Test execution failed: {e}", | |
| full_log=None, exit_code=None, | |
| ) | |
| def remove_container(container_name: str) -> None: | |
| """Force-remove a Docker container.""" | |
| subprocess.run(["docker", "rm", "-f", container_name], capture_output=True) | |