Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
name: CI

on:
# Triggers the workflow on push or pull request events but only for the main branch
# Trigger on pushes and pull requests to key branches.
push:
branches:
- main
- dev
pull_request:
branches: [main]
branches:
- main
- dev
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

Expand All @@ -25,9 +27,9 @@ jobs:
- "3.13"
- "3.14"
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand All @@ -40,6 +42,13 @@ jobs:
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
- name: Test with pytest and coverage
run: |
pytest
pytest -n auto --cov=virl2_client --cov-report=term-missing --cov-report=xml --cov-report=json
- name: Upload coverage reports
uses: actions/upload-artifact@v4
with:
name: coverage-py${{ matrix.python-version }}
path: |
coverage.xml
coverage.json
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ dist
.built
*/__pycache__
.pytest_cache
.coverage
coverage.json
coverage.xml
htmlcov/
docs/build
docs/source/api
*.pyc
Expand Down
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@

.phony: export diff
.PHONY: export diff test coverage coverage-html

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

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

diff:
diff -ruN -X.gitignore -x.github -x.git -xdist -x.pytest_cache ./ ../simple/virl2_client/ | pygmentize | less -r

test:
pytest -n auto

coverage:
pytest -n auto --cov=virl2_client --cov-report=term-missing --cov-report=xml --cov-report=json

coverage-html:
pytest -n auto --cov=virl2_client --cov-report=html --cov-report=term-missing
719 changes: 428 additions & 291 deletions poetry.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ aiohttp = {version = "^3", optional = true}
# optional pyATS package
# (this pulls in a lot more dependencies)
# it does not pull in genie, need to specify extras=["full"] for pyATS.
pyats = {version = "^25", optional = true}
pyats = {version = "^26", optional = true}

# optional packages for documentation build
sphinx_rtd_theme = {version="^3", optional = true}
Expand All @@ -48,6 +48,7 @@ sphinx = {version="<8.2", optional = true}
flake8 = "^7"
pre-commit = "^4"
pytest = "*"
pytest-cov = "*"
pytest-xdist = "*"
respx = "^0.22.0"

Expand Down
92 changes: 92 additions & 0 deletions tests/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# LLM Test Writing Notes

When generating/editing tests in this directory:

## File Structure

- Start every `.py` file with the full Apache 2.0 license header, then a one-line
module docstring.
- Use `from __future__ import annotations` as the first application-level import.
- Keep module names as `test_<domain>.py`; avoid mixed catch-all files.
- Place behavior in the closest module-specific file.

## Docstrings and Types

- Use reST docstrings and explicit type annotations in tests/helpers.
Omit `:returns: None` when the return type is `-> None`.
Omit `:raises AssertionError:` on test functions (every test raises on failure).
- **Every LLM-generated or LLM-modified test must include** the following note in
its docstring, on its own line after the summary:
`NOTE: LLM-generated test -- verify for correctness.`
Remove the note only after a human has verified the test logic.

## Test Design

- Write unit tests only; no external services or mutable environment assumptions.
- Keep test names behavior-focused; prefer short names (`<30`, hard cap `<50`).
- Each test should exercise one logical behavior; split tests with more than ~3
arrange/act/assert cycles.
- Prefer parametrization for repeated patterns; use `pytest.param(..., id="...")`
for readable test IDs. Avoid copy-paste duplication.
- Do not duplicate tests that already exist in a domain-specific module; search for
existing coverage before adding a new test.

## Shared Infrastructure

- Use `make_lab()` / `make_lab_with_topology()` from `helpers.py` for lab setup.
- Use `lab._create_node_local()`, `_create_interface_local()`, `_create_link_local()`,
`_create_annotation_local()`, `_create_smart_annotation_local()` for custom
topologies. Prefer `make_lab_with_topology()` when the standard shape suffices.
- Use conftest fixtures: `FAKE_HOST`, `FAKE_HOST_API`, `CURRENT_VERSION`,
`reset_env`, `client_library_server_*`, `mocked_session`, `test_data_dir`,
`respx_mock_with_labs`, `client_library`.
- Place JSON fixtures in `test_data/`; access via the `test_data_dir` fixture.
- Assign side-effect-only fixtures to `_`: `_ = client_library_server_current`.

## Assertions and Exceptions

- Assert concrete behavior (exact calls, exact exception types/messages).
- Use `pytest.raises(ExcType, match="regex")` for exception assertions.
- Capture expected warnings with `pytest.warns` / `pytest.deprecated_call`.
- Capture log output with `caplog.at_level(...)` and assert on `caplog.text`.

## Mocking

- Do not use `@respx.mock` unless the test body configures at least one route.
- Three valid `respx` patterns: fixture (`respx_mock: MockRouter`), decorator
(`@respx.mock`), or context manager (`with respx.mock(...)`).
- Patch at the import site, not the definition site
(e.g. `patch("virl2_client.models.node.time.sleep")`, not `patch("time.sleep")`).
- Use `patch.object(Cls, "attr")` as a context manager; never assign directly
(e.g. `Lab.sync = Mock()`) without cleanup.
- `assert_called_once_with(...)` already verifies call count — do not follow it with a
redundant `assert_called_once()`.
- Extract module-local helpers (`_make_node()`, `_new_event()`) for repeated setup;
promote to `helpers.py`/`conftest.py` only when shared across modules.

## Environment and Imports

- Never place `import` statements inside test function or fixture bodies; put them at
module level.
- Fixtures must not call `os.chdir` or directly mutate `os.environ`; use
`monkeypatch.chdir` / `monkeypatch.setenv` instead so teardown is guaranteed.
- For flaky paths (time/async/threading), patch clocks/sleeps and use controlled mocks.

## Optional Dependencies

- Gate modules requiring optional packages with `pytest.importorskip("pkg")` at
module level.

## Coverage

- Keep coverage complete for touched branches and verify with:
- `pytest -n auto --cov=virl2_client --cov-report=term-missing`

## File Placement

Place new tests in the closest domain-specific `test_<domain>.py` file.
The `_runtime` suffix marks runtime/integration-level tests for the same domain.
See [Test Module Reference](README.md#test-module-reference) for the full
module-to-scope mapping.

> For full explanations and examples, see [README.md](README.md).
Loading