Files
driver-standard/tests/test_check.py
T
christianmanivong 0b21f62553 fix(check): SPDX license string; E501 off in tests (device-types pilot)
The pilot's build warned that setuptools refuses a license table and
License classifiers from 2027-02-18 (PEP 639); check now reports both and
the template builds with setuptools>=77. Recorded device output in tests
keeps its lines, so the template turns E501 off under tests/.
2026-10-22 19:08:06 +02:00

234 lines
8.5 KiB
Python

"""The conformance check: one test per rule, each breaking exactly that rule."""
from __future__ import annotations
import pytest
from napalm_standard.check import check_repo
from tests.conftest import PYPROJECT, RepoFactory
def rules(findings: list) -> set[str]:
return {f.rule for f in findings}
def test_a_conforming_repo_has_no_findings(make_repo: RepoFactory) -> None:
assert check_repo(make_repo()) == []
def test_a_conforming_repo_passes_at_its_own_tag(make_repo: RepoFactory) -> None:
assert check_repo(make_repo(), tag="v1.2.0") == []
@pytest.mark.parametrize("path", ["LICENSE", "README.md", "CHANGELOG.md"])
def test_a_missing_top_level_file_is_reported(make_repo: RepoFactory, path: str) -> None:
findings = check_repo(make_repo({path: None}))
assert rules(findings) == {"files"}
assert path in findings[0].message
def test_an_empty_readme_counts_as_missing(make_repo: RepoFactory) -> None:
assert rules(check_repo(make_repo({"README.md": "\n"}))) == {"files"}
def test_a_repo_without_pyproject_reports_only_that(make_repo: RepoFactory) -> None:
assert rules(check_repo(make_repo({"pyproject.toml": None}))) == {"pyproject"}
def test_an_unparsable_pyproject_is_reported(make_repo: RepoFactory) -> None:
assert rules(check_repo(make_repo({"pyproject.toml": "[project\n"}))) == {"pyproject"}
@pytest.mark.parametrize("field", ["name", "version", "description", "license"])
def test_a_missing_project_field_is_reported(make_repo: RepoFactory, field: str) -> None:
pyproject = "\n".join(
line for line in PYPROJECT.splitlines() if not line.startswith(f"{field} = ")
)
findings = check_repo(make_repo({"pyproject.toml": pyproject}))
assert rules(findings) == {"metadata"}
assert field in findings[0].message
@pytest.mark.parametrize(
"license_line",
['license = { text = "Apache-2.0" }', 'license = { file = "LICENSE" }'],
)
def test_a_license_table_is_reported(make_repo: RepoFactory, license_line: str) -> None:
"""setuptools refuses the table form from 2027-02-18 (PEP 639)."""
pyproject = PYPROJECT.replace('license = "Apache-2.0"', license_line)
findings = check_repo(make_repo({"pyproject.toml": pyproject}))
assert rules(findings) == {"metadata"}
assert "SPDX" in findings[0].message
def test_a_license_classifier_is_reported(make_repo: RepoFactory) -> None:
pyproject = PYPROJECT.replace(
'requires-python = ">=3.10"\n',
'requires-python = ">=3.10"\n'
'classifiers = ["License :: OSI Approved :: Apache Software License"]\n',
)
assert rules(check_repo(make_repo({"pyproject.toml": pyproject}))) == {"metadata"}
@pytest.mark.parametrize("spec", [">=3.8", ">=3.9", ""])
def test_python_below_310_or_unbounded_is_reported(make_repo: RepoFactory, spec: str) -> None:
pyproject = PYPROJECT.replace('requires-python = ">=3.10"', f'requires-python = "{spec}"')
assert rules(check_repo(make_repo({"pyproject.toml": pyproject}))) == {"python"}
def test_a_higher_python_floor_is_fine(make_repo: RepoFactory) -> None:
pyproject = PYPROJECT.replace('">=3.10"', '">=3.11"')
assert check_repo(make_repo({"pyproject.toml": pyproject})) == []
def test_a_driver_must_depend_on_device_types(make_repo: RepoFactory) -> None:
pyproject = PYPROJECT.replace(' "napalm-device-types>=4.21",\n', "")
assert rules(check_repo(make_repo({"pyproject.toml": pyproject}))) == {"device-types"}
@pytest.mark.parametrize("spec", ["napalm-device-types", "napalm-device-types<5"])
def test_device_types_without_a_floor_is_reported(make_repo: RepoFactory, spec: str) -> None:
pyproject = PYPROJECT.replace('"napalm-device-types>=4.21"', f'"{spec}"')
assert rules(check_repo(make_repo({"pyproject.toml": pyproject}))) == {"device-types"}
def test_device_types_is_matched_by_its_normalised_name(make_repo: RepoFactory) -> None:
pyproject = PYPROJECT.replace('"napalm-device-types>=4.21"', '"napalm_device_types>=4.21"')
assert check_repo(make_repo({"pyproject.toml": pyproject})) == []
def test_device_types_itself_needs_no_such_dependency_or_entry_point(
make_repo: RepoFactory,
) -> None:
pyproject = (
PYPROJECT.replace('name = "napalm-example"', 'name = "napalm-device-types"')
.replace(' "napalm-device-types>=4.21",\n', "")
.replace('[project.entry-points."napalm.drivers"]\n', "")
.replace('example = "napalm_example:ExampleDriver"\n', "")
.replace("NAPALM/napalm-example", "NAPALM/napalm-device-types")
)
repo = make_repo(
{
"pyproject.toml": pyproject,
"napalm_device_types/__init__.py": "",
"napalm_device_types/py.typed": "",
}
)
assert check_repo(repo) == []
def test_a_driver_without_napalm_entry_point_is_reported(make_repo: RepoFactory) -> None:
pyproject = PYPROJECT.replace('[project.entry-points."napalm.drivers"]\n', "").replace(
'example = "napalm_example:ExampleDriver"\n', ""
)
assert rules(check_repo(make_repo({"pyproject.toml": pyproject}))) == {"entry-point"}
def test_urls_must_name_a_repository(make_repo: RepoFactory) -> None:
pyproject = PYPROJECT.replace(
'Repository = "https://git.netork.io/NAPALM/napalm-example"\n', ""
)
assert rules(check_repo(make_repo({"pyproject.toml": pyproject}))) == {"urls"}
@pytest.mark.parametrize(
"url",
[
"https://github.com/chrismanivong/napalm-example",
"https://github.com/napalm-automation-community/napalm-example",
"https://git.netork.io/christianmanivong/napalm-example",
],
)
def test_urls_outside_the_napalm_org_are_reported(make_repo: RepoFactory, url: str) -> None:
pyproject = PYPROJECT.replace("https://git.netork.io/NAPALM/napalm-example/issues", url)
findings = check_repo(make_repo({"pyproject.toml": pyproject}))
assert rules(findings) == {"urls"}
assert url in findings[0].message
def test_a_package_without_py_typed_is_reported(make_repo: RepoFactory) -> None:
assert rules(check_repo(make_repo({"napalm_example/py.typed": None}))) == {"py.typed"}
def test_the_package_directory_can_be_named_explicitly(make_repo: RepoFactory) -> None:
pyproject = PYPROJECT + '\n[tool.napalm-standard]\npackage = "napalm_other"\n'
repo = make_repo(
{
"pyproject.toml": pyproject,
"napalm_example/py.typed": None,
"napalm_other/__init__.py": "",
"napalm_other/py.typed": "",
}
)
assert check_repo(repo) == []
def test_ci_must_live_under_gitea(make_repo: RepoFactory) -> None:
assert rules(check_repo(make_repo({".gitea/workflows/ci.yml": None}))) == {"ci"}
def test_a_leftover_github_workflow_is_reported(make_repo: RepoFactory) -> None:
findings = check_repo(make_repo({".github/workflows/ci.yml": "name: CI\n"}))
assert rules(findings) == {"ci"}
def test_mypy_must_be_strict(make_repo: RepoFactory) -> None:
pyproject = PYPROJECT.replace("[tool.mypy]\nstrict = true\n", "")
assert rules(check_repo(make_repo({"pyproject.toml": pyproject}))) == {"ratchet"}
def test_coverage_must_have_a_floor(make_repo: RepoFactory) -> None:
pyproject = PYPROJECT.replace("[tool.coverage.report]\nfail_under = 80\n", "")
assert rules(check_repo(make_repo({"pyproject.toml": pyproject}))) == {"ratchet"}
def test_a_tag_must_match_the_version(make_repo: RepoFactory) -> None:
findings = check_repo(make_repo(), tag="v1.3.0")
assert rules(findings) == {"release"}
assert "1.2.0" in findings[0].message
@pytest.mark.parametrize("tag", ["1.2.0", "v1.2", "release-1.2.0", "v1.2.0-rc1"])
def test_a_tag_that_is_no_release_tag_is_reported(make_repo: RepoFactory, tag: str) -> None:
assert rules(check_repo(make_repo(), tag=tag)) == {"release"}
def test_a_tag_needs_its_changelog_section(make_repo: RepoFactory) -> None:
changelog = "# Changelog\n\n## [1.1.0] - 2026-10-01\n\n- Facts.\n"
findings = check_repo(make_repo({"CHANGELOG.md": changelog}), tag="v1.2.0")
assert rules(findings) == {"release"}
assert "CHANGELOG" in findings[0].message
def test_an_empty_changelog_section_does_not_count(make_repo: RepoFactory) -> None:
changelog = "# Changelog\n\n## [1.2.0] - 2026-10-22\n\n## [1.1.0] - 2026-10-01\n\n- x\n"
assert rules(check_repo(make_repo({"CHANGELOG.md": changelog}), tag="v1.2.0")) == {"release"}
def test_coverage_must_name_its_source(make_repo: RepoFactory) -> None:
pyproject = PYPROJECT.replace('[tool.coverage.run]\nsource = ["napalm_example"]\n', "")
assert rules(check_repo(make_repo({"pyproject.toml": pyproject}))) == {"ratchet"}