From 8cf2bd65075497d404a54907d24b6e49c23d07ad Mon Sep 17 00:00:00 2001 From: Angus Whitehead Date: Wed, 30 Apr 2025 14:50:04 +0100 Subject: [PATCH] =?UTF-8?q?Add=20CI=20environment=20optimizations=20to=20i?= =?UTF-8?q?mprove=20test=20stability=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=C3=94=C3=B6=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Set environment variable for CI detection in containers - Add configurable process cleanup timeouts for CI environments - Improve test stability with better timeout handling in helpers - Prevent indefinitely hanging tests with max time limits --- .github/workflows/test.yml | 3 ++- further_link/runner/process_handler.py | 5 +++-- tests/conftest.py | 5 +++++ tests/e2e/helpers.py | 24 ++++++++++++++++++++++-- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 41ff8ea6..1015956c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,6 +19,7 @@ jobs: run: | docker run --rm \ --volume ${{ github.workspace }}:/${{ github.event.repository.name }} \ + -e CI=true \ debian:${{ matrix.debian }} \ /bin/bash -c " \ apt-get update && \ @@ -32,7 +33,7 @@ jobs: # Install from our source until they release their latest version to PyPI pip3 install bluez-peripheral==0.1.8 --extra-index-url=https://packagecloud.io/pi-top/pypi/pypi/simple && \ pip3 install .[test] && \ - pytest -v --cov=further_link && \ + CI=true pytest -v --cov=further_link && \ coverage xml \ " diff --git a/further_link/runner/process_handler.py b/further_link/runner/process_handler.py index 7e54ba8a..fa128008 100644 --- a/further_link/runner/process_handler.py +++ b/further_link/runner/process_handler.py @@ -249,9 +249,10 @@ async def _process_communicate(self): # wait a little for the io tasks to complete to let them send # output produced right before the process stopped # but cancel them after a timeout if they don't stop themselves - await timeout(output_tasks, 1) + cleanup_timeout = float(os.environ.get("FURTHER_LINK_CLEANUP_TIMEOUT", "1")) + await timeout(output_tasks, cleanup_timeout) if hasattr(self, "ipc_tasks"): - await timeout(self.ipc_tasks, 0.1) + await timeout(self.ipc_tasks, cleanup_timeout * 0.1) await self._handle_process_end() diff --git a/tests/conftest.py b/tests/conftest.py index bb71685c..fb0e3962 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,11 @@ os.environ["FURTHER_LINK_WORK_DIR"] = WORKING_DIRECTORY os.environ["FURTHER_LINK_MINISCREEN_PROJECTS_DIR"] = PROJECTS_DIR +# Set shorter timeouts for CI environments to prevent hanging tests +if os.environ.get("CI") == "true": + os.environ["FURTHER_LINK_CLEANUP_TIMEOUT"] = "0.2" # 200ms instead of default 1s + os.environ["FURTHER_LINK_TEST_TIMEOUT"] = "2" # 2s timeout for test operations + @pytest.fixture(autouse=True) def create_working_directory(): diff --git a/tests/e2e/helpers.py b/tests/e2e/helpers.py index e2860bd0..ea8be0ec 100644 --- a/tests/e2e/helpers.py +++ b/tests/e2e/helpers.py @@ -1,5 +1,6 @@ import asyncio import json +import os from concurrent.futures import TimeoutError from time import time @@ -39,10 +40,29 @@ async def receive_data(ws, channel, data_key=None, data_value=None, process=""): async def wait_for_data( ws, channel, data_key=None, data_value=None, timeout=0, process="" ): + # Get timeout from environment if in CI mode + if os.environ.get("CI") == "true" and timeout == 0: + try: + timeout = int(os.environ.get("FURTHER_LINK_TEST_TIMEOUT", "2")) + except (ValueError, TypeError): + timeout = 2 + start_time = round(time()) + max_time = timeout if timeout > 0 else 120 # Set a maximum timeout for safety + while timeout <= 0 or (round(time()) - start_time) <= timeout: + # Prevent tests from hanging indefinitely + if round(time()) - start_time > max_time: + raise TimeoutError(f"Test operation timed out after {max_time}s") + try: - message = await ws.receive() + # Set a reasonable receive timeout to prevent indefinite hangs + receive_timeout = min(0.5, max_time - (round(time()) - start_time)) + if receive_timeout <= 0: + raise TimeoutError("Receive timeout expired") + + # Use wait_for to prevent indefinite blocking + message = await asyncio.wait_for(ws.receive(), timeout=receive_timeout) m_type, m_data, m_process, m_client = parse_message(message.data) assert m_process == process, f"{m_process} != {process}" @@ -71,7 +91,7 @@ async def wait_for_data( return await receive_data(ws, channel, data_key, remaining_data, process) except (TimeoutError, asyncio.TimeoutError): continue - raise TimeoutError + raise TimeoutError(f"Test operation timed out after {timeout}s") async def send_formatted_bluetooth_message(