"""Unit tests for eval/environments/gortex_docker.py. Tests focus on pure logic (cache key, failure recording, properties) and mock Docker interactions to avoid requiring a running Docker daemon. """ from __future__ import annotations import io import tarfile from pathlib import Path from unittest.mock import MagicMock, patch import pytest import sys sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from environments.gortex_docker import ( DEFAULT_EVAL_SERVER_PORT, DEFAULT_GORTEX_TIMEOUT, GortexDockerEnvironment, _make_cache_key, ) # -- _make_cache_key --------------------------------------------------------- class TestMakeCacheKey: def test_basic(self): assert _make_cache_key("django", "abc123") == "django_abc123" def test_slash_in_repo_name(self): assert _make_cache_key("django/django", "abc123") == "django__django_abc123" def test_deterministic(self): k1 = _make_cache_key("repo", "commit") k2 = _make_cache_key("repo", "commit") assert k1 == k2 def test_different_inputs_different_keys(self): k1 = _make_cache_key("repo_a", "commit1") k2 = _make_cache_key("repo_b", "commit2") assert k1 != k2 # -- GortexDockerEnvironment init ------------------------------------------- class TestInit: def test_defaults(self): env = GortexDockerEnvironment(image="test:latest") assert env.image == "test:latest" assert env.enable_gortex is True assert env.gortex_timeout == DEFAULT_GORTEX_TIMEOUT assert env.eval_server_port == DEFAULT_EVAL_SERVER_PORT assert env._container is None assert env._gortex_ready is False def test_gortex_disabled(self): env = GortexDockerEnvironment(image="test:latest", enable_gortex=False) assert env.enable_gortex is False def test_custom_params(self): env = GortexDockerEnvironment( image="swe:v1", gortex_binary="/tmp/gortex", gortex_timeout=60, eval_server_port=9999, cache_dir="/tmp/cache", instance_id="django__django-1234", ) assert env.gortex_binary == Path("/tmp/gortex") assert env.gortex_timeout == 60 assert env.eval_server_port == 9999 assert env.cache_dir == Path("/tmp/cache") assert env.instance_id == "django__django-1234" # -- setup / failure recording ----------------------------------------------- class TestRecordFailure: def test_returns_failure_dict(self): env = GortexDockerEnvironment(image="test:latest", instance_id="test-123") result = env._record_failure("something broke") assert result["exit_status"] == "setup_failure" assert result["instance_id"] == "test-123" assert "something broke" in result["setup_error"] assert result["submission"] == "" def test_sets_setup_error(self): env = GortexDockerEnvironment(image="test:latest") env._record_failure("timeout") assert env.setup_error == "timeout" # -- is_ready property ------------------------------------------------------- class TestIsReady: def test_not_ready_no_container(self): env = GortexDockerEnvironment(image="test:latest") assert env.is_ready is False def test_ready_when_gortex_disabled(self): env = GortexDockerEnvironment(image="test:latest", enable_gortex=False) env._container = MagicMock() # simulate running container assert env.is_ready is True def test_not_ready_gortex_enabled_but_not_setup(self): env = GortexDockerEnvironment(image="test:latest", enable_gortex=True) env._container = MagicMock() assert env.is_ready is False def test_ready_gortex_enabled_and_setup(self): env = GortexDockerEnvironment(image="test:latest", enable_gortex=True) env._container = MagicMock() env._gortex_ready = True assert env.is_ready is True # -- extract_patch ----------------------------------------------------------- class TestExtractPatch: def test_no_container(self): env = GortexDockerEnvironment(image="test:latest") assert env.extract_patch() == "" def test_successful_diff(self): env = GortexDockerEnvironment(image="test:latest") mock_container = MagicMock() mock_container.exec_run.return_value = (0, b"diff --git a/foo.py b/foo.py\n+hello\n") env._container = mock_container patch = env.extract_patch() assert "diff --git" in patch assert "+hello" in patch def test_failed_diff(self): env = GortexDockerEnvironment(image="test:latest") mock_container = MagicMock() mock_container.exec_run.return_value = (1, b"error") env._container = mock_container assert env.extract_patch() == "" def test_exception_returns_empty(self): env = GortexDockerEnvironment(image="test:latest") mock_container = MagicMock() mock_container.exec_run.side_effect = RuntimeError("boom") env._container = mock_container assert env.extract_patch() == "" # -- teardown ---------------------------------------------------------------- class TestTeardown: def test_teardown_stops_and_removes(self): env = GortexDockerEnvironment(image="test:latest") mock_container = MagicMock() mock_client = MagicMock() env._container = mock_container env._client = mock_client env.teardown() mock_container.stop.assert_called_once() mock_container.remove.assert_called_once() mock_client.close.assert_called_once() assert env._container is None assert env._client is None def test_teardown_no_container(self): env = GortexDockerEnvironment(image="test:latest") env.teardown() # should not raise # -- exec_run ---------------------------------------------------------------- class TestExecRun: def test_no_container(self): env = GortexDockerEnvironment(image="test:latest") code, output = env.exec_run("echo hello") assert code == 1 assert "not running" in output.lower() def test_string_command_wrapped_in_bash(self): env = GortexDockerEnvironment(image="test:latest") mock_container = MagicMock() mock_container.exec_run.return_value = (0, b"hello\n") env._container = mock_container code, output = env.exec_run("echo hello") assert code == 0 assert output == "hello\n" # Verify it was wrapped in bash -c call_args = mock_container.exec_run.call_args assert call_args[0][0] == ["bash", "-c", "echo hello"] def test_list_command_passed_directly(self): env = GortexDockerEnvironment(image="test:latest") mock_container = MagicMock() mock_container.exec_run.return_value = (0, b"ok") env._container = mock_container code, output = env.exec_run(["ls", "-la"]) assert code == 0 call_args = mock_container.exec_run.call_args assert call_args[0][0] == ["ls", "-la"] # -- setup with gortex disabled --------------------------------------------- class TestSetupGortexDisabled: @patch("environments.gortex_docker.docker") def test_setup_skips_gortex(self, mock_docker): mock_client = MagicMock() mock_container = MagicMock() mock_container.short_id = "abc123" mock_client.containers.run.return_value = mock_container mock_docker.from_env.return_value = mock_client env = GortexDockerEnvironment(image="test:latest", enable_gortex=False) result = env.setup() assert result is None assert env._container is mock_container # -- setup container launch failure ------------------------------------------ class TestSetupContainerFailure: @patch("environments.gortex_docker.docker") def test_container_launch_failure(self, mock_docker): mock_client = MagicMock() mock_client.containers.run.side_effect = RuntimeError("Docker not running") mock_docker.from_env.return_value = mock_client env = GortexDockerEnvironment( image="test:latest", instance_id="fail-instance", ) result = env.setup() assert result is not None assert result["exit_status"] == "setup_failure" assert "fail-instance" in result["instance_id"]