migratron / code_migration /docker_sandbox.py
amrithanandini's picture
fixing dockerfile
364b897
# 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
@dataclass
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,
)
@staticmethod
def remove_container(container_name: str) -> None:
"""Force-remove a Docker container."""
subprocess.run(["docker", "rm", "-f", container_name], capture_output=True)