Skip to content

Commit b6e59cf

Browse files
JasonW404Jinglong Wang
andauthored
fix: Parallel unit test runner with file-level subprocess isolation (#3285)
* fix: parallel unit test runner with file-level subprocess isolation - Rewrite test/run_all_test.py as file-level parallel runner using ThreadPoolExecutor with configurable workers (NEXENT_PYTEST_WORKERS) and per-file timeout (NEXENT_PYTEST_FILE_TIMEOUT) - Add pytest-xdist to backend test extras - Fix test_mcp_service.py: clear proxy env vars (socks://) in fixture to prevent httpx.AsyncClient ValueError - Fix test_remote_mcp_service.py: mock check_runtime_host_port_available to prevent port conflict in container enable test - Fix test_openai_llm.py: reduce memory leak from repeated module imports - Update CI workflow: default to parallel mode, add dispatch inputs for worker count and per-file timeout Serial: 229/229 pass (7m7s). Parallel: 229/229 pass (1m1s, ~7x speedup). * chore: remove unused pytest-xdist dependency The parallel runner uses ThreadPoolExecutor with per-file subprocess isolation, not pytest-xdist. The xdist package was added but never used due to sys.modules mock conflicts during pytest collection. --------- Co-authored-by: Jinglong Wang <wangjinglong8@huawei.com>
1 parent 068b418 commit b6e59cf

5 files changed

Lines changed: 286 additions & 492 deletions

File tree

.github/workflows/auto-unit-test.yml

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ on:
1111
description: 'runner array in json format (e.g. ["ubuntu-latest"] or ["self-hosted"])'
1212
required: false
1313
default: '["ubuntu-24.04-arm"]'
14+
pytest_workers:
15+
description: 'parallel test workers (auto=CPU count, 0=serial, N=fixed count)'
16+
required: false
17+
default: 'auto'
18+
pytest_file_timeout:
19+
description: 'per-file timeout in seconds (0=disabled)'
20+
required: false
21+
default: '300'
1422
pull_request:
1523
branches: [develop, 'release/**', 'hotfix/**']
1624
paths:
@@ -49,23 +57,25 @@ jobs:
4957
cd ..
5058
5159
- name: Run all tests and collect coverage
60+
env:
61+
NEXENT_PYTEST_WORKERS: ${{ github.event.inputs.pytest_workers || 'auto' }}
62+
NEXENT_PYTEST_FILE_TIMEOUT: ${{ github.event.inputs.pytest_file_timeout || '300' }}
5263
run: |
5364
source backend/.venv/bin/activate && python test/run_all_test.py
5465
TEST_EXIT_CODE=$?
5566
5667
if [ -f "test/coverage.xml" ]; then
57-
echo "Coverage XML file generated successfully."
68+
echo "Coverage XML file generated successfully."
5869
else
59-
echo "Coverage XML file not found."
70+
echo "Coverage XML file not found."
6071
exit 1
6172
fi
6273
63-
# Check if tests actually passed
6474
if [ $TEST_EXIT_CODE -ne 0 ]; then
65-
echo "Tests failed with exit code $TEST_EXIT_CODE"
75+
echo "Tests failed with exit code $TEST_EXIT_CODE"
6676
exit $TEST_EXIT_CODE
6777
else
68-
echo "All tests passed successfully."
78+
echo "All tests passed successfully."
6979
fi
7080
7181
- name: Upload coverage to Codecov

test/backend/services/test_mcp_service.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,23 @@ def __init__(self, name, description, inputSchema, outputSchema=None):
158158
mcp_service.ToolResult = RealToolResult
159159

160160

161+
# Proxy env vars that can leak into httpx.AsyncClient and cause failures
162+
# (e.g. socks:// proxies that httpx does not support natively)
163+
_PROXY_ENV_KEYS = [
164+
"HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy",
165+
"ALL_PROXY", "all_proxy", "NO_PROXY", "no_proxy",
166+
]
167+
168+
161169
# Reset global state before each test
162170
@pytest.fixture(autouse=True)
163171
def reset_global_state():
164172
"""Reset global state before each test"""
173+
saved_proxy = {}
174+
for key in _PROXY_ENV_KEYS:
175+
if key in os.environ:
176+
saved_proxy[key] = os.environ.pop(key)
177+
165178
# Reset before test
166179
mcp_service._openapi_mcp_services = {}
167180
mcp_service._mcp_management_app = None
@@ -182,6 +195,8 @@ def reset_global_state():
182195

183196
yield
184197

198+
os.environ.update(saved_proxy)
199+
185200
# Reset after test
186201
mcp_service._openapi_mcp_services = {}
187202
mcp_service._mcp_management_app = None

test/backend/services/test_remote_mcp_service.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,14 +526,15 @@ async def test_non_container_enable_without_custom_headers(
526526
custom_headers=None,
527527
)
528528

529+
@patch('backend.services.remote_mcp_service.check_runtime_host_port_available', return_value=True)
529530
@patch('backend.services.remote_mcp_service.update_mcp_record_enabled_by_id')
530531
@patch('backend.services.remote_mcp_service.update_mcp_record_container_fields_by_id')
531532
@patch('backend.services.remote_mcp_service.mcp_server_health')
532533
@patch('backend.services.remote_mcp_service.MCPContainerManager')
533534
@patch('backend.services.remote_mcp_service.get_mcp_record_by_id_and_tenant')
534535
@patch('backend.services.remote_mcp_service.get_mcp_records_by_tenant')
535536
async def test_container_enable_with_custom_headers(
536-
self, mock_records, mock_get, mock_mgr_cls, mock_health, mock_cont_fields, mock_enabled
537+
self, mock_records, mock_get, mock_mgr_cls, mock_health, mock_cont_fields, mock_enabled, mock_port_check
537538
):
538539
"""Test container enable with custom_headers passed to health check."""
539540
mock_get.return_value = self._make_record(

0 commit comments

Comments
 (0)