# Copyright Kevin Deldycke <kevin@deldycke.com> 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.
from __future__ import annotations
from dataclasses import asdict
from itertools import chain
from string import ascii_lowercase, digits
import pytest
from meta_package_manager.pool import pool
from meta_package_manager.specifier import PURL_MAP, EmptyReduction, Solver, Specifier
[docs]
def test_purl_map():
# Check pURL type IDs. See:
# https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst#rules-for-each-purl-component
for purl_type in PURL_MAP:
assert purl_type.isascii()
assert purl_type[0] in ascii_lowercase
assert set(purl_type).issubset(ascii_lowercase + digits + ".+-")
# Check that keys are sorted.
assert list(PURL_MAP.keys()) == sorted(PURL_MAP.keys())
# Check that all registered managers are known to the pool.
referenced_manager_ids = set(
chain.from_iterable(ids for ids in PURL_MAP.values() if ids is not None)
)
assert set(pool.all_manager_ids).issuperset(referenced_manager_ids), (
"Unknown manager IDs found in PURL_MAP: "
+ ", ".join(referenced_manager_ids.difference(pool.all_manager_ids))
)
# Check that each manager whose ID is also a pURL type is registered with itself.
for manager_id in pool.all_manager_ids:
if manager_id in PURL_MAP:
purl_managers = PURL_MAP[manager_id]
if purl_managers is not None:
assert manager_id in purl_managers, (
f"Manager ID '{manager_id}' is not registered with itself in PURL_MAP."
)
[docs]
def props(spec: Specifier):
"""Utility to help compares specifiers between themselves.
I.e. all properties of specifiers are the same but the `raw_specs` property.
"""
return sorted((k, v) for k, v in asdict(spec).items() if k != "raw_spec")
[docs]
@pytest.mark.parametrize(
("spec_string", "expected"),
(
pytest.param(
"leftpad",
({"package_id": "leftpad", "manager_id": None, "version": None},),
id="ascii",
),
pytest.param(
"left_pad",
({"package_id": "left_pad", "manager_id": None, "version": None},),
id="underscore",
),
pytest.param(
"left.pad",
({"package_id": "left.pad", "manager_id": None, "version": None},),
id="dot",
),
pytest.param(
"left-pad",
({"package_id": "left-pad", "manager_id": None, "version": None},),
id="dash",
),
pytest.param(
"left/pad",
({"package_id": "left/pad", "manager_id": None, "version": None},),
id="slash",
),
pytest.param(
"left-pad@",
({"package_id": "left-pad@", "manager_id": None, "version": None},),
id="version_separator",
),
pytest.param(
"left-pad@8.a7",
({"package_id": "left-pad", "manager_id": None, "version": "8.a7"},),
id="version_spec",
),
pytest.param(
"left-pad@8.a7@1.2.3",
({"package_id": "left-pad@8.a7", "manager_id": None, "version": "1.2.3"},),
id="double_version_separator",
),
pytest.param(
"@eslint/json",
({"package_id": "@eslint/json", "manager_id": None, "version": None},),
id="starting_with_at_sign",
),
pytest.param(
"@eslint/json@0.9.0",
({"package_id": "@eslint/json", "manager_id": None, "version": "0.9.0"},),
id="at_sign_and_version",
),
pytest.param(
"pkg:gem/dummy",
({"package_id": "dummy", "manager_id": "gem", "version": None},),
id="purl_with_identical_manager",
),
pytest.param(
"pkg:gem/dummy@1.2.3",
({"package_id": "dummy", "manager_id": "gem", "version": "1.2.3"},),
id="purl_with_version",
),
pytest.param(
"pkg:rubygems/dummy@21.0-b",
({"package_id": "dummy", "manager_id": "gem", "version": "21.0-b"},),
id="purl_single_alias",
),
pytest.param(
"pkg:steamcmd/half-life@3",
({"package_id": "half-life", "manager_id": "steamcmd", "version": "3"},),
id="purl_unregistered_manager",
),
pytest.param(
"pkg:npm/left-pad@2011-04.gamma",
(
{
"package_id": "left-pad",
"manager_id": "bun",
"version": "2011-04.gamma",
},
{
"package_id": "left-pad",
"manager_id": "npm",
"version": "2011-04.gamma",
},
{
"package_id": "left-pad",
"manager_id": "pnpm",
"version": "2011-04.gamma",
},
{
"package_id": "left-pad",
"manager_id": "volta",
"version": "2011-04.gamma",
},
{
"package_id": "left-pad",
"manager_id": "yarn",
"version": "2011-04.gamma",
},
{
"package_id": "left-pad",
"manager_id": "yarn-berry",
"version": "2011-04.gamma",
},
),
id="purl_multiple_aliases",
),
),
)
def test_parse_specs(spec_string, expected):
specs = tuple(Specifier.from_string(spec_string))
spec_props = []
for spec in specs:
assert spec.raw_spec == spec_string
# Serialize each specifier into an hashable set without the 'raw_spec'
# field (which we checked above).
spec_props.append(props(spec))
assert sorted(spec_props) == sorted(sorted(s.items()) for s in expected)
[docs]
@pytest.mark.parametrize(
("spec_strings", "target_managers", "expected"),
(
pytest.param(
("leftpad",),
None,
Specifier.from_string("leftpad"),
id="tuple_specs",
),
pytest.param(
["leftpad"],
None,
Specifier.from_string("leftpad"),
id="list_specs",
),
pytest.param(
{"leftpad"},
None,
Specifier.from_string("leftpad"),
id="set_specs",
),
pytest.param(
("leftpad", "leftpad"),
None,
Specifier.from_string("leftpad"),
id="duplicates",
),
pytest.param(
{"left-pad@8.a7"},
None,
Specifier.from_string("left-pad@8.a7"),
id="version_spec",
),
pytest.param(
{"left-pad", "left-pad@8.23.a7"},
None,
Specifier.from_string("left-pad@8.23.a7"),
id="version_and_unversioned_mix",
),
pytest.param(
{"pkg:gem/leftpad"},
None,
Specifier.from_string("pkg:gem/leftpad"),
id="purl_with_manager",
),
pytest.param(
{"pkg:gem/leftpad@1.2.3"},
None,
Specifier.from_string("pkg:gem/leftpad@1.2.3"),
id="purl_with_version",
),
pytest.param(
{"pkg:rubygems/dummy@21.0-b"},
None,
Specifier.from_string("pkg:gem/dummy@21.0-b"),
id="purl_single_alias",
),
pytest.param(
{"pkg:rpm/ping@2011-04.gamma"},
["dnf", "yum", "zypper"],
Specifier.from_string("pkg:dnf/ping@2011-04.gamma"),
id="purl_multiple_aliases_priority1",
),
pytest.param(
{"pkg:rpm/ping@2011-04.gamma"},
["zypper", "yum"],
Specifier.from_string("pkg:zypper/ping@2011-04.gamma"),
id="purl_multiple_aliases_priority2",
),
pytest.param(
{"pkg:rpm/ping@2011-04.gamma"},
["pypi"],
EmptyReduction,
id="purl_multiple_aliases_unmatching_manager_skip_package",
),
pytest.param(
{"pkg:rpm/ping@2011-04.gamma"},
None,
ValueError,
id="unresolvable_multiple_aliases",
),
pytest.param(
{"pkg:gem/leftpad", "leftpad@77.10.0"},
None,
Specifier.from_string("pkg:gem/leftpad"),
id="purl_takes_precedence",
),
pytest.param(
{"leftpad", "leftpad@1.7.3", "pkg:gem/leftpad@1.7.3"},
None,
Specifier.from_string("pkg:gem/leftpad@1.7.3"),
id="mixed_package_specs",
),
pytest.param(
{"left-pad@33.1.a", "left-pad@0100"},
None,
Specifier.from_string("left-pad@0100"),
id="multiple_versions",
),
pytest.param(
{"left-pad@99.00"},
None,
Specifier.from_string("left-pad@99.00"),
id="duplicate_version_spec",
),
pytest.param(
{"left-pad@99.00", "left-pad@99"},
None,
Specifier.from_string("left-pad@99.00"),
id="similar_version_spec",
),
pytest.param(
{"left-pad@99.00", "left-pad@0099.00"},
None,
ValueError,
id="equivalent_version_spec",
),
),
)
def test_reduce_specs(spec_strings, target_managers, expected):
solver = Solver(spec_strings, manager_priority=target_managers)
if isinstance(expected, type) and issubclass(expected, Exception):
with pytest.raises(expected):
solver.reduce_specs(solver.spec_pool)
else:
reduced_spec = solver.reduce_specs(solver.spec_pool)
expected = tuple(expected)
assert len(expected) == 1
assert props(reduced_spec) == props(expected[0])
[docs]
@pytest.mark.parametrize(
("spec_strings", "manager_priority", "expected"),
(
pytest.param(
{"pkg:uv/rich", "pkg:brew/rich"},
["uv", "brew"],
{"pkg:uv/rich", "pkg:brew/rich"},
id="distinct_explicit_managers_each_kept",
),
pytest.param(
{"pkg:rpm/ping@1.0"},
["dnf", "yum"],
{"pkg:dnf/ping@1.0"},
id="alias_purl_reduced_to_top_priority",
),
pytest.param(
{"rich", "rich@1.0"},
None,
{"rich@1.0"},
id="version_constraints_merged",
),
pytest.param(
{"pkg:gem/leftpad", "leftpad@77.10.0"},
None,
{"pkg:gem/leftpad"},
id="explicit_manager_wins_over_manager_less",
),
pytest.param(
{"rich"},
None,
{"rich"},
id="single_manager_less",
),
),
)
def test_resolve_package_specs(spec_strings, manager_priority, expected):
"""Distinct explicit managers each survive as their own target, while alias
pURLs, version constraints and manager-less specs still reduce to one."""
solver = Solver(spec_strings, manager_priority=manager_priority)
resolved = {str(spec) for _package_id, spec in solver.resolve_package_specs()}
assert resolved == expected