diff --git a/pipeline-runner/build.sh b/pipeline-runner/build.sh index a100ab2..51d48c7 100755 --- a/pipeline-runner/build.sh +++ b/pipeline-runner/build.sh @@ -49,10 +49,16 @@ fi echo "==> Build" cmake --build "${BUILD_DIR}" -j$(nproc) -echo "==> Run tests" +echo "==> Run C++ tests" rm -rf /tmp/pipeline-runner-test/ "${BUILD_DIR}/test-pipeline-runner" \ --gtest_brief=1 \ --gtest_output=xml:"${BUILD_DIR}/test-results.xml" +if [ "${FAKE}" = true ]; then + echo "==> Run Python tests" + timeout 60 python3 -m pytest "${SCRIPT_DIR}/tests" --tb=line -q \ + --junitxml="${BUILD_DIR}/python-test-results.xml" +fi + echo "==> Done" diff --git a/pipeline-runner/tests/conftest.py b/pipeline-runner/tests/conftest.py index f17c926..377ac45 100644 --- a/pipeline-runner/tests/conftest.py +++ b/pipeline-runner/tests/conftest.py @@ -82,16 +82,16 @@ class PipelineServer: except subprocess.TimeoutExpired: self.process.kill() self.process.wait(timeout=5) + # Close pipes to prevent resource leaks and zombie processes + if self.process.stdout: + self.process.stdout.close() + if self.process.stderr: + self.process.stderr.close() @pytest.fixture(scope="session") def pipeline_server(): - """Session-scoped fixture that builds, starts, and stops the pipeline-runner server.""" - project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - build_script = os.path.join(project_dir, "build.sh") - - subprocess.check_call([build_script, "--fake"], cwd=project_dir) - + """Session-scoped fixture that starts and stops the pipeline-runner server.""" work_dir = tempfile.mkdtemp(prefix="pipeline-test-") port = get_free_port() diff --git a/pipeline-runner/tests/test_integration.py b/pipeline-runner/tests/test_integration.py index 01a30c0..5705f16 100644 --- a/pipeline-runner/tests/test_integration.py +++ b/pipeline-runner/tests/test_integration.py @@ -3,8 +3,11 @@ import time import uuid +import pytest import requests +REQUEST_TIMEOUT = 5 + def _unique_id(): return str(uuid.uuid4()) @@ -26,7 +29,9 @@ def test_full_pipeline_lifecycle(pipeline_server): {"id": _unique_id(), "text": "echo step3", "title": "Step 3", "order": 2}, ], } - resp = requests.post(f"{base}/api/pipelines", json=pipeline) + resp = requests.post( + f"{base}/api/pipelines", json=pipeline, timeout=REQUEST_TIMEOUT + ) assert resp.status_code == 200 created = resp.json() assert created["id"] == pipeline_id @@ -34,13 +39,15 @@ def test_full_pipeline_lifecycle(pipeline_server): assert len(created["prompts"]) == 3 # --- List pipelines --- - resp = requests.get(f"{base}/api/pipelines") + resp = requests.get(f"{base}/api/pipelines", timeout=REQUEST_TIMEOUT) assert resp.status_code == 200 pipelines = resp.json() assert any(p["id"] == pipeline_id for p in pipelines) # --- Get single pipeline --- - resp = requests.get(f"{base}/api/pipelines/{pipeline_id}") + resp = requests.get( + f"{base}/api/pipelines/{pipeline_id}", timeout=REQUEST_TIMEOUT + ) assert resp.status_code == 200 fetched = resp.json() assert fetched["id"] == pipeline_id @@ -49,7 +56,9 @@ def test_full_pipeline_lifecycle(pipeline_server): assert fetched["prompts"][0]["order"] == 0 # --- Start a run --- - resp = requests.post(f"{base}/api/runs", json={"pipeline_id": pipeline_id}) + resp = requests.post( + f"{base}/api/runs", json={"pipeline_id": pipeline_id}, timeout=REQUEST_TIMEOUT + ) assert resp.status_code == 200 run_data = resp.json() run_id = run_data["run_id"] @@ -58,7 +67,9 @@ def test_full_pipeline_lifecycle(pipeline_server): # --- Poll run status until completed --- for _ in range(60): - resp = requests.get(f"{base}/api/runs/{run_id}/status") + resp = requests.get( + f"{base}/api/runs/{run_id}/status", timeout=REQUEST_TIMEOUT + ) assert resp.status_code == 200 status_data = resp.json() if status_data["status"] in ("completed", "error"): @@ -71,15 +82,19 @@ def test_full_pipeline_lifecycle(pipeline_server): assert len(status_data["steps"]) == 3 # --- Get log --- - resp = requests.get(f"{base}/api/runs/{run_id}/log") + resp = requests.get(f"{base}/api/runs/{run_id}/log", timeout=REQUEST_TIMEOUT) assert resp.status_code == 200 assert len(resp.text) > 0 # --- Delete pipeline --- - resp = requests.delete(f"{base}/api/pipelines/{pipeline_id}") + resp = requests.delete( + f"{base}/api/pipelines/{pipeline_id}", timeout=REQUEST_TIMEOUT + ) assert resp.status_code == 200 assert resp.json()["deleted"] == pipeline_id # --- Verify deletion --- - resp = requests.get(f"{base}/api/pipelines/{pipeline_id}") + resp = requests.get( + f"{base}/api/pipelines/{pipeline_id}", timeout=REQUEST_TIMEOUT + ) assert resp.status_code == 404 diff --git a/pipeline-runner/tests/test_smoke.py b/pipeline-runner/tests/test_smoke.py index cbe1db0..4d54038 100644 --- a/pipeline-runner/tests/test_smoke.py +++ b/pipeline-runner/tests/test_smoke.py @@ -1,16 +1,15 @@ """Smoke tests for pipeline-runner REST API endpoints.""" -import json import os import shutil import socket -import subprocess import tempfile -import time import pytest import requests +REQUEST_TIMEOUT = 5 + test_get_endpoints = [ ("api/pipelines", 200, list), @@ -21,7 +20,9 @@ test_get_endpoints = [ @pytest.mark.parametrize("endpoint,expected_status,expected_type", test_get_endpoints) def test_get_endpoints(pipeline_server, endpoint, expected_status, expected_type): """Check HTTP status code and response content-type for GET endpoints.""" - response = requests.get(f"{pipeline_server.base_url}/{endpoint}") + response = requests.get( + f"{pipeline_server.base_url}/{endpoint}", timeout=REQUEST_TIMEOUT + ) assert response.status_code == expected_status assert "application/json" in response.headers.get("Content-Type", "") data = response.json() @@ -30,7 +31,9 @@ def test_get_endpoints(pipeline_server, endpoint, expected_status, expected_type def test_cors_headers(pipeline_server): """Check that CORS headers are present on responses.""" - response = requests.get(f"{pipeline_server.base_url}/api/pipelines") + response = requests.get( + f"{pipeline_server.base_url}/api/pipelines", timeout=REQUEST_TIMEOUT + ) assert response.headers.get("Access-Control-Allow-Origin") == "*" @@ -45,10 +48,6 @@ def _get_free_port(): @pytest.fixture(scope="module") def static_file_server(): """Fixture that starts a server with web_root configured for static file tests.""" - project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - build_script = os.path.join(project_dir, "build.sh") - subprocess.check_call([build_script, "--fake"], cwd=project_dir) - work_dir = tempfile.mkdtemp(prefix="pipeline-static-test-") web_root = os.path.join(work_dir, "web") @@ -88,7 +87,7 @@ def static_file_server(): class TestStaticFiles: def test_serve_html(self, static_file_server): server, _ = static_file_server - resp = requests.get(f"{server.base_url}/index.html") + resp = requests.get(f"{server.base_url}/index.html", timeout=REQUEST_TIMEOUT) assert resp.status_code == 200 assert "text/html" in resp.headers.get("Content-Type", "") assert "