4e12902492
CI / quality (pull_request) Successful in 35s
CI / security (pull_request) Successful in 54s
CI / lint (pull_request) Successful in 58s
CI / build (pull_request) Successful in 42s
CI / typecheck (pull_request) Successful in 59s
CI / integration_tests (pull_request) Successful in 1m14s
CI / unit_tests (pull_request) Successful in 3m48s
CI / coverage (pull_request) Successful in 3m34s
CI / status-check (pull_request) Successful in 3s
CI / quality (push) Successful in 33s
CI / lint (push) Successful in 38s
CI / typecheck (push) Successful in 48s
CI / security (push) Successful in 48s
CI / build (push) Successful in 41s
CI / integration_tests (push) Successful in 1m15s
CI / unit_tests (push) Successful in 3m37s
CI / coverage (push) Successful in 3m34s
CI / status-check (push) Successful in 3s
Introduce ReferenceResolver implementing the Package Registry Standard v1.0.0 §5.3 and §4.2: ReferenceResolver: - parse(ref_str) → PackageReference handles all three reference schemes (registry, ID, local), wrapping ValueError into InvalidPackageReferenceError - resolve(ref_str) → PackageId resolves references to concrete IDs: ID refs directly, registry refs via RegistryClient, local refs reserved - Integrates with RegistryClient for remote HTTP resolution Version resolution (§4.2): - resolve_version(version, available_versions) resolves concrete (vX.Y.Z) and mutable aliases (latest, vx, x, vX.x, vX.Y.x) - is_concrete_version() / is_version_alias() classification helpers Integration tests: 35 Robot Framework test cases against fake registry server with version alias resolution support Benchmarks: 7 ASV benchmark classes covering parse, resolve, and version resolution across small/medium/large version lists ISSUES CLOSED: #26
199 lines
8.1 KiB
Python
199 lines
8.1 KiB
Python
"""Robot Framework keyword library for ReferenceResolver integration tests.
|
|
|
|
Provides keywords for testing the ReferenceResolver against a fake
|
|
registry server (started by the test suite).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from collections.abc import Awaitable, Callable
|
|
|
|
from cleveractors.registry.client import RegistryClient
|
|
from cleveractors.registry.exceptions import InvalidPackageReferenceError
|
|
from cleveractors.registry.resolver import (
|
|
ReferenceResolver,
|
|
is_concrete_version,
|
|
is_version_alias,
|
|
resolve_version,
|
|
)
|
|
from cleveractors.registry.types import PackageId, PackageReference, ReferenceType
|
|
|
|
ROBOT_LIBRARY_SCOPE = "TEST SUITE" # pragma: no cover - integration test library
|
|
|
|
|
|
class ResolverLib: # pragma: no cover - integration test library
|
|
"""Keyword library for ReferenceResolver Robot Framework integration tests."""
|
|
|
|
def __init__(self) -> None:
|
|
self._resolver: ReferenceResolver | None = None
|
|
self._client: RegistryClient | None = None
|
|
self._parsed_ref: PackageReference | None = None
|
|
self._resolved_id: PackageId | None = None
|
|
self._result: str | bool | None = None
|
|
self._error: Exception | None = None
|
|
|
|
# ── setup / teardown ────────────────────────────────────────────────
|
|
|
|
def create_resolver_without_client(self) -> None:
|
|
self._resolver = ReferenceResolver()
|
|
|
|
def create_resolver_with_client(self, base_url: str) -> None:
|
|
self._client = RegistryClient(base_url=base_url)
|
|
self._resolver = ReferenceResolver(client=self._client)
|
|
|
|
def close_resolver_client(self) -> None:
|
|
async def _close() -> None:
|
|
if self._client is not None:
|
|
await self._client.close()
|
|
|
|
self._run(_close)
|
|
|
|
# ── parse ───────────────────────────────────────────────────────────
|
|
|
|
def parse_reference(self, ref_str: str) -> None:
|
|
self._error = None
|
|
try:
|
|
self._parsed_ref = ReferenceResolver.parse(ref_str)
|
|
except InvalidPackageReferenceError as exc:
|
|
self._error = exc
|
|
|
|
def parsed_reference_type_should_be(self, expected: str) -> None:
|
|
if self._parsed_ref is None:
|
|
raise AssertionError("No parsed reference available")
|
|
actual = self._parsed_ref.reference_type.value
|
|
if actual != expected:
|
|
raise AssertionError(
|
|
f"Expected reference_type {expected!r}, got {actual!r}"
|
|
)
|
|
|
|
def parsed_server_should_be(self, expected: str) -> None:
|
|
actual = self._parsed_ref.server
|
|
if actual != expected:
|
|
raise AssertionError(f"Expected server {expected!r}, got {actual!r}")
|
|
|
|
def parsed_namespace_should_be(self, expected: str) -> None:
|
|
actual = self._parsed_ref.namespace
|
|
if actual != expected:
|
|
raise AssertionError(f"Expected namespace {expected!r}, got {actual!r}")
|
|
|
|
def parsed_name_should_be(self, expected: str) -> None:
|
|
actual = self._parsed_ref.name
|
|
if actual != expected:
|
|
raise AssertionError(f"Expected name {expected!r}, got {actual!r}")
|
|
|
|
def parsed_version_should_be(self, expected: str) -> None:
|
|
actual = self._parsed_ref.version
|
|
if actual != expected:
|
|
raise AssertionError(f"Expected version {expected!r}, got {actual!r}")
|
|
|
|
def parsed_id_string_should_be(self, expected: str) -> None:
|
|
actual = self._parsed_ref.id_string
|
|
if actual != expected:
|
|
raise AssertionError(f"Expected id_string {expected!r}, got {actual!r}")
|
|
|
|
def parsed_original_reference_should_be(self, expected: str) -> None:
|
|
actual = self._parsed_ref.original_reference
|
|
if actual != expected:
|
|
raise AssertionError(
|
|
f"Expected original_reference {expected!r}, got {actual!r}"
|
|
)
|
|
|
|
def parsed_server_should_be_none(self) -> None:
|
|
if self._parsed_ref.server is not None:
|
|
raise AssertionError(
|
|
f"Expected server None, got {self._parsed_ref.server!r}"
|
|
)
|
|
|
|
def parsed_namespace_should_be_none(self) -> None:
|
|
if self._parsed_ref.namespace is not None:
|
|
raise AssertionError(
|
|
f"Expected namespace None, got {self._parsed_ref.namespace!r}"
|
|
)
|
|
|
|
def parse_should_have_raised(self, phrase: str) -> None:
|
|
if self._error is None:
|
|
raise AssertionError("Expected error but none was raised")
|
|
if phrase not in str(self._error):
|
|
raise AssertionError(
|
|
f"Expected error containing {phrase!r}, got {self._error!r}"
|
|
)
|
|
|
|
# ── resolve ─────────────────────────────────────────────────────────
|
|
|
|
def resolve_reference(self, ref_str: str) -> None:
|
|
self._error = None
|
|
|
|
async def _resolve() -> None:
|
|
try:
|
|
self._resolved_id = await self._resolver.resolve(ref_str)
|
|
except InvalidPackageReferenceError as exc:
|
|
self._error = exc
|
|
|
|
self._run(_resolve)
|
|
|
|
def resolved_package_id_should_be(self, expected: str) -> None:
|
|
if self._resolved_id is None:
|
|
raise AssertionError("No resolved package ID available")
|
|
actual = self._resolved_id.id_string
|
|
if actual != expected:
|
|
raise AssertionError(f"Expected resolved id {expected!r}, got {actual!r}")
|
|
|
|
def resolve_should_have_raised(self, phrase: str) -> None:
|
|
if self._error is None:
|
|
raise AssertionError("Expected error but none was raised")
|
|
if phrase not in str(self._error):
|
|
raise AssertionError(
|
|
f"Expected error containing {phrase!r}, got {self._error!r}"
|
|
)
|
|
|
|
# ── version resolution ──────────────────────────────────────────────
|
|
|
|
def resolve_version_against(self, version: str, versions_csv: str) -> None:
|
|
self._error = None
|
|
available = (
|
|
[v.strip() for v in versions_csv.split(",") if v.strip()]
|
|
if versions_csv.strip()
|
|
else []
|
|
)
|
|
try:
|
|
self._result = resolve_version(version, available)
|
|
except InvalidPackageReferenceError as exc:
|
|
self._error = exc
|
|
|
|
def resolved_version_should_be(self, expected: str) -> None:
|
|
if self._result != expected:
|
|
raise AssertionError(
|
|
f"Expected resolved version {expected!r}, got {self._result!r}"
|
|
)
|
|
|
|
def resolve_version_should_have_raised(self, phrase: str) -> None:
|
|
if self._error is None:
|
|
raise AssertionError("Expected error but none was raised")
|
|
if phrase not in str(self._error):
|
|
raise AssertionError(
|
|
f"Expected error containing {phrase!r}, got {self._error!r}"
|
|
)
|
|
|
|
# ── version classification ──────────────────────────────────────────
|
|
|
|
def check_is_concrete_version(self, version: str) -> None:
|
|
self._result = is_concrete_version(version)
|
|
|
|
def check_is_version_alias(self, version: str) -> None:
|
|
self._result = is_version_alias(version)
|
|
|
|
def classification_result_should_be_true(self) -> None:
|
|
if self._result is not True:
|
|
raise AssertionError(f"Expected True, got {self._result!r}")
|
|
|
|
def classification_result_should_be_false(self) -> None:
|
|
if self._result is not False:
|
|
raise AssertionError(f"Expected False, got {self._result!r}")
|
|
|
|
# ── async runner ────────────────────────────────────────────────────
|
|
|
|
@staticmethod
|
|
def _run(coro_fn: Callable[[], Awaitable[None]], timeout: float = 30.0) -> None:
|
|
asyncio.run(asyncio.wait_for(coro_fn(), timeout=timeout))
|