Source code for tests.test_purl_spec

# Copyright Kevin Deldycke <[email protected]> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
"""Run the test suite of purl-spec against the pURL parser and renderer of `mpm`.

`tests/purl-spec/` copies the `LICENSE`, the type definitions of `types/` and the
test suite of `tests/` from the
[purl-spec `v1.1.0` release](https://github.com/package-url/purl-spec/releases/tag/v1.1.0).
To move to a newer release:

1. Replace the three with those of the release.
2. Format the JSON files with `repomatic run biome -- format --write tests/purl-spec`.
3. Fix the rows of `PURL_TYPES` that `test_purl_types_match_their_definitions` names.

typos skips the copy, per `[tool.typos]` in `pyproject.toml`.
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from meta_package_manager.package import PURL_TYPES, PurlType, parse_purl, render_purl

PURL_SPEC = Path(__file__).parent / "purl-spec"

_UPPERCASE_KEY = (
    "Clause 5.6.6 of ECMA-427 allows lowercase qualifier keys only, and the required"
    " gem and rpm parse cases refuse an uppercase one, but this case accepts it:"
    " package-url/purl-spec#942 and package-url/purl-spec#1027."
)

SPEC_CONTRADICTIONS = {
    "git-test-0": (
        "The git definition keeps the case of the namespace and the name, but this"
        " case lowercases them."
    ),
    "maven-test-8": _UPPERCASE_KEY,
    "maven-test-9": _UPPERCASE_KEY,
    "maven-test-12": _UPPERCASE_KEY,
    "maven-test-13": _UPPERCASE_KEY,
    "gem-test-0": _UPPERCASE_KEY,
    "rpm-test-0": _UPPERCASE_KEY,
}
"""Cases of the suite that contradict the standard or another case, by ID."""


def _definitions() -> list[dict]:
    return [
        json.loads(path.read_text(encoding="UTF-8"))
        for path in sorted((PURL_SPEC / "types").glob("*-definition.json"))
    ]


def _cases(test_type: str) -> list:
    """The cases of `test_type` in the suite, one parameter each."""
    params = []
    for path in sorted((PURL_SPEC / "tests").rglob("*-test.json")):
        cases = json.loads(path.read_text(encoding="UTF-8"))["tests"]
        for index, case in enumerate(cases):
            if case["test_type"] != test_type:
                continue
            case_id = f"{path.stem}-{index}"
            marks: tuple[pytest.MarkDecorator, ...] = ()
            if case_id in SPEC_CONTRADICTIONS:
                reason = SPEC_CONTRADICTIONS[case_id]
                marks = (pytest.mark.xfail(reason=reason, strict=True),)
            params.append(pytest.param(case, id=case_id, marks=marks))
    return params


def _parts(components: dict) -> tuple:
    """The parts `parse_purl()` returns, from the components of a test case."""
    return tuple(
        components.get(key) or None for key in ("type", "namespace", "name", "version")
    )


[docs] def test_purl_types_match_their_definitions(): """Each registered type has a row stating the rules of its definition.""" expected = {} for definition in _definitions(): lowercase = tuple( component for component in ("namespace", "name", "version") if definition.get(f"{component}_definition", {}).get("case_sensitive") is False ) qualifiers = tuple( qualifier["key"] for qualifier in definition.get("qualifiers_definition", []) if qualifier.get("requirement") == "required" ) namespace = definition.get("namespace_definition", {}) expected[definition["type"]] = PurlType( namespace.get("requirement", "optional"), lowercase, qualifiers ) assert PURL_TYPES == expected
[docs] @pytest.mark.parametrize("case", _cases("parse")) def test_parse_case(case): """`parse_purl()` splits each valid pURL of the suite, and refuses the others.""" if case["expected_failure"]: with pytest.raises(ValueError): parse_purl(case["input"]) else: assert parse_purl(case["input"]) == _parts(case["expected_output"])
[docs] @pytest.mark.parametrize("case", _cases("validate")) def test_validate_case(case): """A pURL reads like its canonical form, and an invalid one is refused.""" if case["expected_failure"]: with pytest.raises(ValueError): parse_purl(case["input"]) else: assert parse_purl(case["input"]) == parse_purl(case["expected_output"])
[docs] @pytest.mark.parametrize("case", _cases("build")) def test_build_case(case): """`render_purl()` renders each valid set of parts, and refuses the others.""" parts = case["input"] def build() -> str: return render_purl( parts.get("type") or "", parts.get("namespace"), parts.get("name") or "", parts.get("version"), parts.get("qualifiers"), parts.get("subpath"), ) if case["expected_failure"]: with pytest.raises(ValueError): build() else: assert build() == case["expected_output"]