Skip to content

Commit 0338cfc

Browse files
Improve unit test times with isolated parallel workers (#225)
* Parallelize unit tests with isolated workspaces * [Automated] Updated coverage badge * Add language hint to test command * Shard E2E tests with configurable count * [Automated] Updated coverage badge * Expand E2E sharding to ten workers Add a shared ten-shard assignment while preserving the existing three-shard fallback. Run ten E2E shards per platform and fail explicitly when the public E2E collection drifts from the checked-in assignments. * Run representative E2E coverage on Windows and macOS --------- Co-authored-by: GitHub Actions <actions@github.com>
1 parent 70b534a commit 0338cfc

9 files changed

Lines changed: 807 additions & 94 deletions

File tree

.github/workflows/end2end_tests.yaml

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,61 @@ concurrency:
1515
cancel-in-progress: true
1616

1717
jobs:
18-
run_tests:
19-
name: E2E Tests on ${{ matrix.os }} / Python ${{ matrix.version }}
18+
ubuntu_full:
19+
name: E2E full on ubuntu-latest / Python ${{ matrix.version }} / shard ${{ matrix.shard }}
2020
strategy:
2121
fail-fast: false
2222
matrix:
23-
os: [ubuntu-latest, windows-latest, macOS-latest]
2423
version: ['3.10']
24+
shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
25+
26+
runs-on: ubuntu-latest
27+
28+
steps:
29+
- name: Checkout
30+
uses: actions/checkout@v4
31+
with:
32+
repository: ${{ github.event.pull_request.head.repo.full_name }}
33+
ref: ${{ github.event.pull_request.head.sha }}
34+
submodules: recursive
35+
36+
- name: Set up Python
37+
uses: actions/setup-python@v5
38+
with:
39+
python-version: ${{ matrix.version }}
40+
cache: pip
41+
42+
- name: Install dependencies
43+
shell: bash
44+
run: |
45+
CONSTRAINTS_FILE="$GITHUB_WORKSPACE/constraints.txt"
46+
echo "setuptools<82" > "$CONSTRAINTS_FILE"
47+
export PIP_CONSTRAINT="$CONSTRAINTS_FILE"
48+
pip install -e .[dev]
49+
50+
- name: Run full public tests
51+
run: pytest tests/test_end2end.py --e2e-suite full --e2e-shard-index ${{ matrix.shard }} --e2e-shard-count 10 --download-weights -s -v
52+
53+
- name: Authenticate to Google Cloud
54+
if: matrix.shard == 0 && github.event.pull_request.head.repo.full_name == github.repository
55+
uses: google-github-actions/auth@v2
56+
with:
57+
credentials_json: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
58+
create_credentials_file: true
59+
export_environment_variables: true
60+
61+
- name: Run private model tests
62+
if: matrix.shard == 0 && github.event.pull_request.head.repo.full_name == github.repository
63+
run: pytest tests/test_end2end.py::test_private_model_conversion --test-private --delete-weights-now -s -v
64+
65+
cross_platform_representative:
66+
name: E2E representative on ${{ matrix.os }} / Python ${{ matrix.version }} / shard ${{ matrix.shard }}
67+
strategy:
68+
fail-fast: false
69+
matrix:
70+
os: [windows-latest, macOS-latest]
71+
version: ['3.10']
72+
shard: [0, 1]
2573

2674
runs-on: ${{ matrix.os }}
2775

@@ -31,7 +79,7 @@ jobs:
3179
with:
3280
repository: ${{ github.event.pull_request.head.repo.full_name }}
3381
ref: ${{ github.event.pull_request.head.sha }}
34-
submodules: recursive # Ensures submodules are cloned
82+
submodules: recursive
3583

3684
- name: Set up Python
3785
uses: actions/setup-python@v5
@@ -47,16 +95,17 @@ jobs:
4795
export PIP_CONSTRAINT="$CONSTRAINTS_FILE"
4896
pip install -e .[dev]
4997
50-
- name: Run tests
51-
run: pytest tests/test_end2end.py --download-weights --delete-weights-now -s -v
98+
- name: Run representative public tests
99+
run: pytest tests/test_end2end.py --e2e-suite representative --e2e-shard-index ${{ matrix.shard }} --e2e-shard-count 2 --download-weights -s -v
52100

53101
- name: Authenticate to Google Cloud
102+
if: matrix.shard == 0 && github.event.pull_request.head.repo.full_name == github.repository
54103
uses: google-github-actions/auth@v2
55104
with:
56105
credentials_json: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
57106
create_credentials_file: true
58107
export_environment_variables: true
59108

60109
- name: Run private model tests
61-
if: github.event.pull_request.head.repo.full_name == github.repository
110+
if: matrix.shard == 0 && github.event.pull_request.head.repo.full_name == github.repository
62111
run: pytest tests/test_end2end.py::test_private_model_conversion --test-private --delete-weights-now -s -v

.github/workflows/unittests.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,19 @@ jobs:
8080
- name: Run tests with coverage [Ubuntu]
8181
if: matrix.os == 'ubuntu-latest' && matrix.version == '3.10'
8282
working-directory: ${{ inputs.tools_ref != '' && 'tools' || '' }}
83-
env:
84-
COVERAGE_PROCESS_START: .coveragerc
8583
run: |
84+
export COVERAGE_PROCESS_START="$PWD/.coveragerc"
85+
export COVERAGE_FILE="$PWD/.coverage"
8686
coverage erase
87-
coverage run -m pytest tests/test_unittests.py --download-weights --delete-weights-now -s -v --junit-xml pytest.xml
87+
coverage run -m pytest tests/test_unittests.py -n 2 --dist load --download-weights -s -v --junit-xml pytest.xml
8888
coverage combine
8989
coverage xml -o coverage.xml -i # Recent changes to coverage 7.13 make it stricter where it was previously passing https://coverage.readthedocs.io/en/7.13.0/messages.html#error-no-source
9090
9191
9292
- name: Run tests [Windows, macOS]
9393
if: matrix.os != 'ubuntu-latest' || matrix.version != '3.10'
9494
working-directory: ${{ inputs.tools_ref != '' && 'tools' || '' }}
95-
run: pytest tests/test_unittests.py --download-weights --delete-weights-now -s -v --junit-xml pytest.xml
95+
run: pytest tests/test_unittests.py -n 2 --dist load --download-weights -s -v --junit-xml pytest.xml
9696

9797
- name: Generate coverage badge [Ubuntu]
9898
if: matrix.os == 'ubuntu-latest' && matrix.version == '3.10' && inputs.tools_ref == '' && inputs.ml_ref == ''

media/coverage_badge.svg

Lines changed: 3 additions & 3 deletions
Loading

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pytest
2+
pytest-xdist>=3.6.1
23
pytest-subtests
34
pre-commit>=4.1.0
45
pytest-cov>=4.1.0

tests/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ Unit tests download the preset weights from original repositories and check if t
1313
If set then test only that specific yolo version
1414
--test-case=TEST_CASE
1515
If set then test only that specific test case
16-
--delete-weights-now Clean weights after every test to save space - but longer test time.
16+
--delete-weights-now Clean weights after every test to save disk space, at the cost of longer runtime.
1717
```
1818

1919
Here is an example of the call to run:
2020

21-
```
22-
pytest --download-weights --log-cli-level=INFO --log-file=out.log --log-file-level=DEBUG .
21+
```bash
22+
pytest tests/test_unittests.py -n 2 --dist load --download-weights --log-cli-level=INFO --log-file=out.log --log-file-level=DEBUG
2323
```
2424

25-
This will run the full test suite on all the supported models and store the DEBUG logs into the out.log file.
25+
This will run the full unit test suite on all supported models and store the DEBUG logs in the out.log file.

tests/conftest.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import logging
22
import os
33
import shutil
4+
from pathlib import Path
45

56
import pytest
67

8+
pytest_plugins = ["e2e_shards"]
9+
710
logger = logging.getLogger()
811

912
os.environ.setdefault("LUXONIS_TELEMETRY_ENABLED", "false")
@@ -68,21 +71,42 @@ def test_config(pytestconfig):
6871
}
6972

7073

74+
@pytest.fixture(scope="session")
75+
def test_workspace(tmp_path_factory, request) -> Path:
76+
"""Create one isolated conversion workspace per pytest worker."""
77+
worker_id = getattr(request.config, "workerinput", {}).get("workerid", "master")
78+
workspace = tmp_path_factory.mktemp(f"tools-tests-{worker_id}")
79+
(workspace / "weights").mkdir(exist_ok=True)
80+
return workspace
81+
82+
83+
def _artifact_path(request: pytest.FixtureRequest, name: str) -> Path:
84+
"""Resolve cleanup paths without changing E2E behavior."""
85+
if "test_workspace" in request.fixturenames:
86+
workspace = request.getfixturevalue("test_workspace")
87+
return Path(workspace) / name
88+
89+
return Path(name)
90+
91+
7192
@pytest.fixture(scope="function", autouse=True)
72-
def cleanup_output_after_tests(test_config):
93+
def cleanup_output_after_tests(test_config, request):
7394
yield # Tests run here
7495
if test_config["delete_output"]:
75-
folder_to_delete = "shared_with_container"
76-
if os.path.exists(folder_to_delete):
96+
folder_to_delete = _artifact_path(
97+
request,
98+
"shared_with_container",
99+
)
100+
if folder_to_delete.exists():
77101
shutil.rmtree(folder_to_delete)
78102
logger.info(f"Removed test artifacts from {folder_to_delete}")
79103

80104

81105
@pytest.fixture(scope="function", autouse=True)
82-
def cleanup_weights_after_tests(test_config):
106+
def cleanup_weights_after_tests(test_config, request):
83107
yield # Tests run here
84108
if test_config["delete_weights_now"]:
85-
folder_to_delete = "weights"
86-
if os.path.exists(folder_to_delete):
109+
folder_to_delete = _artifact_path(request, "weights")
110+
if folder_to_delete.exists():
87111
shutil.rmtree(folder_to_delete)
88112
logger.info(f"Removed test artifacts from {folder_to_delete}")

0 commit comments

Comments
 (0)