diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3c32f93 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.pytest.ini_options] +pythonpath = ["."] diff --git a/tests/compatibility/conftest.py b/tests/compatibility/conftest.py index 38d00a7..38ac65b 100644 --- a/tests/compatibility/conftest.py +++ b/tests/compatibility/conftest.py @@ -13,6 +13,44 @@ DEVCLOUD_URL = os.environ.get("DEVCLOUD_URL", f"http://localhost:{DEVCLOUD_PORT}") +def _build_devcloud_cmd(project_root, bin_path): + """Build the devcloud command, conditionally adding -config if devcloud.yaml exists.""" + config_path = os.path.join(project_root, "devcloud.yaml") + + if bin_path: + cmd = [bin_path] + else: + cmd = ["go", "run", "./cmd/devcloud"] + + if os.path.isfile(config_path): + cmd.extend(["-config", "devcloud.yaml"]) + + return cmd + + +def _start_server_error(cmd, project_root, env): + """Re-run server with PIPE to capture stderr, then raise with diagnostic info.""" + debug_proc = subprocess.Popen( + cmd, + cwd=project_root, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + try: + _wait_for_server(DEVCLOUD_URL, timeout=5) + except RuntimeError: + pass + debug_proc.kill() + debug_proc.wait() + stderr = debug_proc.stderr.read().decode(errors="replace") + raise RuntimeError( + f"devcloud server did not start within 30s.\n" + f"command: {' '.join(cmd)}\n" + f"stderr:\n{stderr}" + ) from None + + def _wait_for_server(url, timeout=30, interval=0.5): """Poll server until it responds or timeout.""" deadline = time.time() + timeout @@ -46,10 +84,7 @@ def devcloud_server(): # collide with stale data. The directory is cleaned up after the session. data_dir = tempfile.mkdtemp(prefix="devcloud-test-") - if bin_path: - cmd = [bin_path, "-config", "devcloud.yaml"] - else: - cmd = ["go", "run", "./cmd/devcloud", "-config", "devcloud.yaml"] + cmd = _build_devcloud_cmd(project_root, bin_path) env = os.environ.copy() env["CGO_ENABLED"] = "1" @@ -62,7 +97,12 @@ def devcloud_server(): stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) - _wait_for_server(DEVCLOUD_URL) + try: + _wait_for_server(DEVCLOUD_URL) + except RuntimeError: + proc.kill() + proc.wait() + _start_server_error(cmd, project_root, env) yield proc proc.send_signal(signal.SIGINT) try: diff --git a/tests/compatibility/test_conftest_helpers.py b/tests/compatibility/test_conftest_helpers.py new file mode 100644 index 0000000..49840e6 --- /dev/null +++ b/tests/compatibility/test_conftest_helpers.py @@ -0,0 +1,80 @@ +import io + +from tests.compatibility.conftest import _build_devcloud_cmd, _start_server_error + + +class TestBuildDevcloudCmd: + def test_includes_config_when_file_exists(self, monkeypatch, tmp_path): + def fake_isfile(path): + return path.endswith("devcloud.yaml") + + monkeypatch.setattr("tests.compatibility.conftest.os.path.isfile", fake_isfile) + + cmd = _build_devcloud_cmd(str(tmp_path), None) + + assert cmd[:3] == ["go", "run", "./cmd/devcloud"] + assert "-config" in cmd + assert "devcloud.yaml" in cmd + + def test_omits_config_when_file_missing(self, monkeypatch, tmp_path): + monkeypatch.setattr( + "tests.compatibility.conftest.os.path.isfile", lambda p: False + ) + + cmd = _build_devcloud_cmd(str(tmp_path), None) + + assert cmd == ["go", "run", "./cmd/devcloud"] + assert "-config" not in cmd + assert "devcloud.yaml" not in cmd + + def test_uses_bin_path_when_provided(self, monkeypatch, tmp_path): + monkeypatch.setattr( + "tests.compatibility.conftest.os.path.isfile", lambda p: False + ) + + cmd = _build_devcloud_cmd(str(tmp_path), "/usr/local/bin/devcloud") + + assert cmd == ["/usr/local/bin/devcloud"] + + def test_bin_path_with_config(self, monkeypatch, tmp_path): + def fake_isfile(path): + return path.endswith("devcloud.yaml") + + monkeypatch.setattr("tests.compatibility.conftest.os.path.isfile", fake_isfile) + + cmd = _build_devcloud_cmd(str(tmp_path), "/usr/local/bin/devcloud") + + assert cmd[0] == "/usr/local/bin/devcloud" + assert "-config" in cmd + + +class TestDevcloudServerErrorHandling: + def test_raises_runtime_error_with_stderr_on_startup_failure(self, monkeypatch): + fake_stderr = io.BytesIO(b"fatal: config file not found\n") + + class FakeProc: + def kill(self): + pass + + def wait(self): + return 1 + + stderr = fake_stderr + + monkeypatch.setattr( + "tests.compatibility.conftest.subprocess.Popen", lambda *a, **kw: FakeProc() + ) + monkeypatch.setattr( + "tests.compatibility.conftest._wait_for_server", + lambda *a, **kw: (_ for _ in ()).throw(RuntimeError("timeout")), + ) + + import pytest + + with pytest.raises(RuntimeError) as exc_info: + _start_server_error(["go", "run", "./cmd/devcloud"], "/tmp/project", {}) + + msg = str(exc_info.value) + assert "fatal: config file not found" in msg + assert "command: go run ./cmd/devcloud" in msg + assert "stderr:" in msg