Skip to content

Commit af953be

Browse files
committed
CMLDEV-966 Improve unit test coverage (#200)
* CMLDEV-966 Improve unit test coverage Add coverage to github actions and makefile * More splits ; copyright ; cleanup
1 parent 9a308f4 commit af953be

46 files changed

Lines changed: 10981 additions & 2062 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/main.yml

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
name: CI
44

55
on:
6-
# Triggers the workflow on push or pull request events but only for the main branch
6+
# Trigger on pushes and pull requests to key branches.
77
push:
88
branches:
99
- main
1010
- dev
1111
pull_request:
12-
branches: [main]
12+
branches:
13+
- main
14+
- dev
1315
# Allows you to run this workflow manually from the Actions tab
1416
workflow_dispatch:
1517

@@ -25,9 +27,9 @@ jobs:
2527
- "3.13"
2628
- "3.14"
2729
steps:
28-
- uses: actions/checkout@v2
30+
- uses: actions/checkout@v4
2931
- name: Set up Python ${{ matrix.python-version }}
30-
uses: actions/setup-python@v2
32+
uses: actions/setup-python@v5
3133
with:
3234
python-version: ${{ matrix.python-version }}
3335
- name: Install dependencies
@@ -40,6 +42,13 @@ jobs:
4042
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
4143
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
4244
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
43-
- name: Test with pytest
45+
- name: Test with pytest and coverage
4446
run: |
45-
pytest
47+
pytest -n auto --cov=virl2_client --cov-report=term-missing --cov-report=xml --cov-report=json
48+
- name: Upload coverage reports
49+
uses: actions/upload-artifact@v4
50+
with:
51+
name: coverage-py${{ matrix.python-version }}
52+
path: |
53+
coverage.xml
54+
coverage.json

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ dist
88
.built
99
*/__pycache__
1010
.pytest_cache
11+
.coverage
12+
coverage.json
13+
coverage.xml
14+
htmlcov/
1115
docs/build
1216
docs/source/api
1317
*.pyc

Makefile

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11

2-
.phony: export diff
2+
.PHONY: export diff test coverage coverage-html
33

44
tests/requirements.txt: poetry.lock
55
poetry export --format=requirements.txt --with dev --output=$@
66

77
clean:
8-
rm -rf dist virl2_client.egg-info .built .pytest_cache .coverage coverage.xml
8+
rm -rf dist virl2_client.egg-info .built .pytest_cache .coverage coverage.xml coverage.json htmlcov
99
find . -depth -type f -name '*.pyc' -exec rm {} \; || true
1010
find . -depth -type d -name '__pycache__' -exec rmdir {} \; || true
1111
cd docs && make clean
@@ -18,3 +18,12 @@ export: tests/requirements.txt
1818

1919
diff:
2020
diff -ruN -X.gitignore -x.github -x.git -xdist -x.pytest_cache ./ ../simple/virl2_client/ | pygmentize | less -r
21+
22+
test:
23+
pytest -n auto
24+
25+
coverage:
26+
pytest -n auto --cov=virl2_client --cov-report=term-missing --cov-report=xml --cov-report=json
27+
28+
coverage-html:
29+
pytest -n auto --cov=virl2_client --cov-report=html --cov-report=term-missing

poetry.lock

Lines changed: 428 additions & 291 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ aiohttp = {version = "^3", optional = true}
3737
# optional pyATS package
3838
# (this pulls in a lot more dependencies)
3939
# it does not pull in genie, need to specify extras=["full"] for pyATS.
40-
pyats = {version = "^25", optional = true}
40+
pyats = {version = "^26", optional = true}
4141

4242
# optional packages for documentation build
4343
sphinx_rtd_theme = {version="^3", optional = true}
@@ -48,6 +48,7 @@ sphinx = {version="<8.2", optional = true}
4848
flake8 = "^7"
4949
pre-commit = "^4"
5050
pytest = "*"
51+
pytest-cov = "*"
5152
pytest-xdist = "*"
5253
respx = "^0.22.0"
5354

tests/AGENTS.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# LLM Test Writing Notes
2+
3+
When generating/editing tests in this directory:
4+
5+
## File Structure
6+
7+
- Start every `.py` file with the full Apache 2.0 license header, then a one-line
8+
module docstring.
9+
- Use `from __future__ import annotations` as the first application-level import.
10+
- Keep module names as `test_<domain>.py`; avoid mixed catch-all files.
11+
- Place behavior in the closest module-specific file.
12+
13+
## Docstrings and Types
14+
15+
- Use reST docstrings and explicit type annotations in tests/helpers.
16+
Omit `:returns: None` when the return type is `-> None`.
17+
Omit `:raises AssertionError:` on test functions (every test raises on failure).
18+
- **Every LLM-generated or LLM-modified test must include** the following note in
19+
its docstring, on its own line after the summary:
20+
`NOTE: LLM-generated test -- verify for correctness.`
21+
Remove the note only after a human has verified the test logic.
22+
23+
## Test Design
24+
25+
- Write unit tests only; no external services or mutable environment assumptions.
26+
- Keep test names behavior-focused; prefer short names (`<30`, hard cap `<50`).
27+
- Each test should exercise one logical behavior; split tests with more than ~3
28+
arrange/act/assert cycles.
29+
- Prefer parametrization for repeated patterns; use `pytest.param(..., id="...")`
30+
for readable test IDs. Avoid copy-paste duplication.
31+
- Do not duplicate tests that already exist in a domain-specific module; search for
32+
existing coverage before adding a new test.
33+
34+
## Shared Infrastructure
35+
36+
- Use `make_lab()` / `make_lab_with_topology()` from `helpers.py` for lab setup.
37+
- Use `lab._create_node_local()`, `_create_interface_local()`, `_create_link_local()`,
38+
`_create_annotation_local()`, `_create_smart_annotation_local()` for custom
39+
topologies. Prefer `make_lab_with_topology()` when the standard shape suffices.
40+
- Use conftest fixtures: `FAKE_HOST`, `FAKE_HOST_API`, `CURRENT_VERSION`,
41+
`reset_env`, `client_library_server_*`, `mocked_session`, `test_data_dir`,
42+
`respx_mock_with_labs`, `client_library`.
43+
- Place JSON fixtures in `test_data/`; access via the `test_data_dir` fixture.
44+
- Assign side-effect-only fixtures to `_`: `_ = client_library_server_current`.
45+
46+
## Assertions and Exceptions
47+
48+
- Assert concrete behavior (exact calls, exact exception types/messages).
49+
- Use `pytest.raises(ExcType, match="regex")` for exception assertions.
50+
- Capture expected warnings with `pytest.warns` / `pytest.deprecated_call`.
51+
- Capture log output with `caplog.at_level(...)` and assert on `caplog.text`.
52+
53+
## Mocking
54+
55+
- Do not use `@respx.mock` unless the test body configures at least one route.
56+
- Three valid `respx` patterns: fixture (`respx_mock: MockRouter`), decorator
57+
(`@respx.mock`), or context manager (`with respx.mock(...)`).
58+
- Patch at the import site, not the definition site
59+
(e.g. `patch("virl2_client.models.node.time.sleep")`, not `patch("time.sleep")`).
60+
- Use `patch.object(Cls, "attr")` as a context manager; never assign directly
61+
(e.g. `Lab.sync = Mock()`) without cleanup.
62+
- `assert_called_once_with(...)` already verifies call count — do not follow it with a
63+
redundant `assert_called_once()`.
64+
- Extract module-local helpers (`_make_node()`, `_new_event()`) for repeated setup;
65+
promote to `helpers.py`/`conftest.py` only when shared across modules.
66+
67+
## Environment and Imports
68+
69+
- Never place `import` statements inside test function or fixture bodies; put them at
70+
module level.
71+
- Fixtures must not call `os.chdir` or directly mutate `os.environ`; use
72+
`monkeypatch.chdir` / `monkeypatch.setenv` instead so teardown is guaranteed.
73+
- For flaky paths (time/async/threading), patch clocks/sleeps and use controlled mocks.
74+
75+
## Optional Dependencies
76+
77+
- Gate modules requiring optional packages with `pytest.importorskip("pkg")` at
78+
module level.
79+
80+
## Coverage
81+
82+
- Keep coverage complete for touched branches and verify with:
83+
- `pytest -n auto --cov=virl2_client --cov-report=term-missing`
84+
85+
## File Placement
86+
87+
Place new tests in the closest domain-specific `test_<domain>.py` file.
88+
The `_runtime` suffix marks runtime/integration-level tests for the same domain.
89+
See [Test Module Reference](README.md#test-module-reference) for the full
90+
module-to-scope mapping.
91+
92+
> For full explanations and examples, see [README.md](README.md).

0 commit comments

Comments
 (0)