diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b967aa..609c532 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,7 @@ jobs: test: strategy: + fail-fast: false matrix: runner: [ubuntu-latest, macos-latest, windows-latest] name: test-${{ matrix.runner }} diff --git a/docs/api.md b/docs/api.md index c85e52f..5cf66ad 100644 --- a/docs/api.md +++ b/docs/api.md @@ -6,6 +6,18 @@ SPDX-License-Identifier: MIT # Python API +## Querying the JDK index + +```{eval-rst} +.. autofunction:: cjdk.list_vendors +.. versionadded:: 0.4.0 +``` + +```{eval-rst} +.. autofunction:: cjdk.list_jdks +.. versionadded:: 0.4.0 +``` + ## Working with cached JDKs ```{eval-rst} diff --git a/docs/changelog.md b/docs/changelog.md index 7e58b78..298e398 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,7 +10,15 @@ See also the section on [versioning](versioning-scheme). ## [Unreleased] -- No notable changes yet. +### Added + +- Python API functions `list_jdks()` and `list_vendors()`. +- Command line commands `ls` and `ls-vendors`. +- Light postprocessing of vendor names, notably `ibm-semeru-openj9`. + +### Changed + +- Command line command `cache-jdk` renamed to `cache`. ## [0.3.0] - 2022-07-09 diff --git a/docs/cli.md b/docs/cli.md index e4794e9..135df44 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -24,6 +24,26 @@ More details about the choices and defaults for [`VENDOR`](./vendors.md), [`VERSION`](./versions.md), and [`--cache_dir`](./cachedir.md) are available on separate pages. +## Querying the JDK index + +### `ls` + +```{command-output} cjdk ls --help +``` + +```{eval-rst} +.. versionadded:: 0.4.0 +``` + +### `ls-vendors` + +```{command-output} cjdk ls-vendors --help +``` + +```{eval-rst} +.. versionadded:: 0.4.0 +``` + ## Working with cached JDKs ### `exec` @@ -53,13 +73,14 @@ $ cjdk --jdk temurin-jre:17.0.3 java-home (The output will depend on your operating system and configuration; the example shown was on macOS.) -### `cache-jdk` +### `cache` -```{command-output} cjdk cache-jdk --help +```{command-output} cjdk cache --help ``` ```{eval-rst} -.. versionadded:: 0.2.0 +.. versionchanged:: 0.4.0 + Renamed from ``cache-jdk``. ``` ## Caching arbitrary files and packages diff --git a/src/cjdk/__init__.py b/src/cjdk/__init__.py index 1773436..4d4a54c 100644 --- a/src/cjdk/__init__.py +++ b/src/cjdk/__init__.py @@ -2,7 +2,15 @@ # Copyright 2022 Board of Regents of the University of Wisconsin System # SPDX-License-Identifier: MIT -from ._api import cache_file, cache_jdk, cache_package, java_env, java_home +from ._api import ( + cache_file, + cache_jdk, + cache_package, + java_env, + java_home, + list_jdks, + list_vendors, +) from ._version import __version__ as __version__ __all__ = [ @@ -11,4 +19,6 @@ "cache_package", "java_env", "java_home", + "list_jdks", + "list_vendors", ] diff --git a/src/cjdk/__main__.py b/src/cjdk/__main__.py index c49dd62..0aab32c 100644 --- a/src/cjdk/__main__.py +++ b/src/cjdk/__main__.py @@ -64,14 +64,43 @@ def main(ctx, jdk, cache_dir, index_url, index_ttl, os, arch, progress): ) +@click.command(short_help="List available JDK vendors.") +@click.pass_context +def ls_vendors(ctx): + """ + Print the list of available JDK vendors. + """ + vendors = _api.list_vendors(**ctx.obj) + if vendors: + print("\n".join(vendors)) + + +@click.command(short_help="List cached or available JDKs matching criteria.") +@click.pass_context +@click.option( + "--cached/--available", + default=True, + help="Show only already-cached JDKs, or show all available JDKs from the index (default cached only).", +) +def ls(ctx, cached: bool = False): + """ + Print the list of JDKs matching the given criteria. + + See 'cjdk --help' for the common options used to specify the criteria. + """ + jdks = _api.list_jdks(**ctx.obj, cached_only=cached) + if jdks: + print("\n".join(jdks)) + + @click.command(short_help="Ensure the requested JDK is cached.") @click.pass_context -def cache_jdk(ctx): +def cache(ctx): """ Download and extract the requested JDK if it is not already cached. Usually there is no need to invoke this command on its own, but it may be - useful if you want any potentil JDK download to happen at a controlled + useful if you want any potential JDK download to happen at a controlled point in time. See 'cjdk --help' for the common options used to specify the JDK and how it @@ -80,6 +109,15 @@ def cache_jdk(ctx): _api.cache_jdk(**ctx.obj) +@click.command(hidden=True) +@click.pass_context +def cache_jdk(ctx): + """ + Deprecated. Use cache function instead. + """ + _api.cache_jdk(**ctx.obj) + + @click.command( short_help="Print the Java home directory for the requested JDK." ) @@ -108,7 +146,7 @@ def exec(ctx, prog, args): """ Run PROG with the environment variables set for the requested JDK. - The JDK is download if not already cached. + The JDK is downloaded if not already cached. See 'cjdk --help' for the common options used to specify the JDK and how it is obtained. @@ -221,12 +259,17 @@ def cache_package(ctx, url, name, sha1, sha256, sha512): ) +# Register current commands. main.add_command(java_home) main.add_command(exec) -main.add_command(cache_jdk) +main.add_command(ls_vendors) +main.add_command(ls) +main.add_command(cache) main.add_command(cache_file) main.add_command(cache_package) +# Register hidden/deprecated commands, for backwards compatibility. +main.add_command(cache_jdk) if __name__ == "__main__": main() diff --git a/src/cjdk/_api.py b/src/cjdk/_api.py index ba7f9da..02875b1 100644 --- a/src/cjdk/_api.py +++ b/src/cjdk/_api.py @@ -6,17 +6,78 @@ import os from contextlib import contextmanager -from . import _conf, _install, _jdk +from . import _cache, _conf, _index, _install, _jdk __all__ = [ + "cache_file", "cache_jdk", + "cache_package", "java_env", "java_home", - "cache_file", - "cache_package", + "list_jdks", + "list_vendors", ] +def list_vendors(**kwargs): + """ + Return the list of available JDK vendors. + + Parameters + ---------- + None + + Other Parameters + ---------------- + index_url : str, optional + Alternative URL for the JDK index. + + Returns + ------- + list[str] + The available JDK vendors. + """ + return sorted(_get_vendors(**kwargs)) + + +def list_jdks(*, vendor=None, version=None, cached_only=True, **kwargs): + """ + Return the list of JDKs matching the given criteria. + + Parameters + ---------- + vendor : str, optional + JDK vendor name, such as "adoptium". + version : str, optional + JDK version expression, such as "17+". + cached_only : bool, optional + If True, list only already-cached JDKs. + If False, list all matching JDKs in the index. + + Other Parameters + ---------------- + jdk : str, optional + JDK vendor and version, such as "adoptium:17+". Cannot be specified + together with `vendor` or `version`. + cache_dir : pathlib.Path or str, optional + Override the root cache directory. + index_url : str, optional + Alternative URL for the JDK index. + os : str, optional + Operating system for the JDK (default: current operating system). + arch : str, optional + CPU architecture for the JDK (default: current architecture). + + Returns + ------- + list[str] + JDKs (vendor:version) matching the criteria. + """ + return _get_jdks( + vendor=vendor, version=version, cached_only=cached_only, **kwargs + ) + + def cache_jdk(*, vendor=None, version=None, **kwargs): """ Download and extract the given JDK if it is not already cached. @@ -196,6 +257,76 @@ def cache_package(name, url, **kwargs): ) +def _get_vendors(**kwargs): + conf = _conf.configure(**kwargs) + index = _index.jdk_index(conf) + return { + vendor.replace("jdk@", "") + for osys in index + for arch in index[osys] + for vendor in index[osys][arch] + } + + +def _get_jdks(*, vendor=None, version=None, cached_only=True, **kwargs): + conf = _conf.configure( + vendor=vendor, + version=version, + fallback_to_default_vendor=False, + **kwargs, + ) + if conf.vendor is None: + # Search across all vendors. + kwargs.pop("jdk", None) # It was already parsed. + return [ + jdk + for v in sorted(_get_vendors()) + for jdk in _get_jdks( + vendor=v, + version=conf.version, + cached_only=cached_only, + **kwargs, + ) + ] + index = _index.jdk_index(conf) + jdks = _index.available_jdks(index, conf) + versions = _index._get_versions(jdks, conf) + matched = _index._match_versions(conf.vendor, versions, conf.version) + + if cached_only: + # Filter matches by existing key directories. + def is_cached(v): + url = _index.jdk_url(index, conf, v) + key = (_jdk._JDK_KEY_PREFIX, _cache._key_for_url(url)) + keydir = _cache._key_directory(conf.cache_dir, key) + return keydir.exists() + + matched = {k: v for k, v in matched.items() if is_cached(v)} + + class VersionElement: + def __init__(self, value): + self.value = value + self.is_int = isinstance(value, int) + + def __eq__(self, other): + if self.is_int and other.is_int: + return self.value == other.value + return str(self.value) == str(other.value) + + def __lt__(self, other): + if self.is_int and other.is_int: + return self.value < other.value + return str(self.value) < str(other.value) + + def version_key(version_tuple): + return tuple(VersionElement(elem) for elem in version_tuple[0]) + + return [ + f"{conf.vendor}:{v}" + for k, v in sorted(matched.items(), key=version_key) + ] + + def _make_hash_checker(hashes): checks = [ (hashes.pop("sha1", None), hashlib.sha1), diff --git a/src/cjdk/_cache.py b/src/cjdk/_cache.py index 458be91..1e9bc59 100644 --- a/src/cjdk/_cache.py +++ b/src/cjdk/_cache.py @@ -59,7 +59,7 @@ def atomic_file( ttl, timeout_for_fetch_elsewhere=10, timeout_for_read_elsewhere=2.5, -): +) -> Path: """ Retrieve cached file for key, fetching with fetchfunc if necessary. @@ -150,7 +150,7 @@ def permanent_directory( return keydir -def _file_exists_and_is_fresh(file, ttl): +def _file_exists_and_is_fresh(file, ttl) -> bool: if not file.is_file(): return False now = time.time() @@ -185,11 +185,11 @@ def _create_key_tmpdir(cache_dir, key): shutil.rmtree(tmpdir) -def _key_directory(cache_dir, key): +def _key_directory(cache_dir: Path, key) -> Path: return cache_dir / "v0" / Path(*key) -def _key_tmpdir(cache_dir, key): +def _key_tmpdir(cache_dir: Path, key) -> Path: return cache_dir / "v0" / Path("fetching", *key) diff --git a/src/cjdk/_conf.py b/src/cjdk/_conf.py index 499e56d..df2b35d 100644 --- a/src/cjdk/_conf.py +++ b/src/cjdk/_conf.py @@ -39,10 +39,15 @@ def configure(**kwargs): raise ValueError("Cannot specify jdk= together with version=") kwargs["vendor"], kwargs["version"] = _parse_vendor_version(jdk) + default_vendor = ( + _default_vendor() + if kwargs.pop("fallback_to_default_vendor", True) + else None + ) conf = Configuration( os=_canonicalize_os(kwargs.pop("os", None)), arch=_canonicalize_arch(kwargs.pop("arch", None)), - vendor=kwargs.pop("vendor", None) or _default_vendor(), + vendor=kwargs.pop("vendor", None) or default_vendor, version=kwargs.pop("version", "") or "", cache_dir=kwargs.pop("cache_dir", None) or _default_cachedir(), index_url=kwargs.pop("index_url", None) or _default_index_url(), diff --git a/src/cjdk/_index.py b/src/cjdk/_index.py index 1a10198..b0fb0b2 100644 --- a/src/cjdk/_index.py +++ b/src/cjdk/_index.py @@ -6,6 +6,7 @@ import json import re import warnings +from pathlib import Path from . import _install from ._conf import Configuration @@ -22,14 +23,21 @@ _INDEX_FILENAME = "jdk-index.json" -def jdk_index(conf: Configuration): +# Type alias declarations. +Versions = dict[str, str] # key = version, value = archive URL +Vendors = dict[str, Versions] # key = vendor name +Arches = dict[str, Vendors] # key = arch name +Index = dict[str, Arches] # key = os name + + +def jdk_index(conf: Configuration) -> Index: """ Get the JDK index, from cache if possible. """ - return _read_index(_cached_index(conf)) + return _read_index(_cached_index_path(conf)) -def available_jdks(index, conf: Configuration): +def available_jdks(index: Index, conf: Configuration) -> tuple[str, str]: """ Find in index the available JDK vendor-version combinations. @@ -39,8 +47,7 @@ def available_jdks(index, conf: Configuration): index -- The JDK index (nested dict) """ try: - # jdks is dict: vendor -> (version -> url) - jdks = index[conf.os][conf.arch] + jdks: Vendors = index[conf.os][conf.arch] except KeyError: return [] @@ -51,28 +58,25 @@ def available_jdks(index, conf: Configuration): ) -def resolve_jdk_version(index, conf: Configuration): +def resolve_jdk_version(index: Index, conf: Configuration) -> str: """ Find in index the exact JDK version for the given configuration. Arguments: - index -- The JDK index (dested dict) + index -- The JDK index (nested dict) """ jdks = available_jdks(index, conf) - versions = [i[1] for i in jdks if i[0] == conf.vendor] + versions = _get_versions(jdks, conf) if not versions: raise KeyError( f"No {conf.vendor} JDK is available for {conf.os}-{conf.arch}" ) - matched = _match_version(conf.vendor, versions, conf.version) - if not matched: - raise KeyError( - f"No JDK matching version {conf.version} for {conf.os}-{conf.arch}-{conf.vendor}" - ) - return matched + return _match_version(conf.vendor, versions, conf.version) -def jdk_url(index, conf: Configuration): +def jdk_url( + index: Index, conf: Configuration, exact_version: str = None +) -> str: """ Find in index the URL for the JDK binary for the given vendor and version. @@ -80,12 +84,14 @@ def jdk_url(index, conf: Configuration): Arguments: index -- The JDK index (nested dict) + exact_version (optional) -- The JDK version, or None to resolve it from the configuration """ - matched = resolve_jdk_version(index, conf) - return index[conf.os][conf.arch][f"jdk@{conf.vendor}"][matched] + if exact_version is None: + exact_version = resolve_jdk_version(index, conf) + return index[conf.os][conf.arch][f"jdk@{conf.vendor}"][exact_version] -def _cached_index(conf: Configuration): +def _cached_index_path(conf: Configuration) -> Path: def check_index(path): # Ensure valid JSON. _read_index(path) @@ -104,12 +110,59 @@ def check_index(path): ) -def _read_index(path): +def _read_index(path: Path) -> Index: with open(path, encoding="ascii") as infile: - return json.load(infile) + index = json.load(infile) + + return _postprocess_index(index) + +def _postprocess_index(index: Index) -> Index: + """ + Post-process the index to normalize the data. + + Some "vendors" include the major Java version, + so let's merge such entries. In particular: + + * ibm-semuru-openj9-java<##> + * graalvm-java<##> + + However: while the graalvm vendors follow this pattern, the version + numbers for graalvm are *not* JDK versions, but rather GraalVM versions, + which merely strongly resemble JDK version strings. For example, + graalvm-java17 version 22.3.3 bundles OpenJDK 17.0.8, but + unfortunately there is no way to know this from the index alone. + """ + + pattern = re.compile("^(jdk@ibm-semeru.*)-java\\d+$") + if not hasattr(index, "items"): + return index + for os, arches in index.items(): + if not hasattr(arches, "items"): + continue + for arch, vendors in arches.items(): + if not hasattr(vendors, "items"): + continue + for vendor, versions in vendors.copy().items(): + if not vendor.startswith("jdk@graalvm") and ( + m := pattern.match(vendor) + ): + true_vendor = m.group(1) + if true_vendor not in index[os][arch]: + index[os][arch][true_vendor] = {} + index[os][arch][true_vendor].update(versions) -def _match_version(vendor, candidates, requested): + return index + + +def _get_versions(jdks: tuple[str, str], conf) -> list[str]: + return [i[1] for i in jdks if i[0] == conf.vendor] + + +def _match_versions( + vendor, candidates: list[str], requested +) -> dict[tuple[int], str]: + # Find all candidates compatible with the request is_graal = "graalvm" in vendor.lower() normreq = _normalize_version(requested, remove_prefix_1=not is_graal) normcands = {} @@ -126,26 +179,32 @@ def _match_version(vendor, candidates, requested): continue # Skip any non-numeric versions (not expected) normcands[normcand] = candidate - # Find the newest candidate compatible with the request - for normcand in sorted(normcands.keys(), reverse=True): - if _is_version_compatible_with_spec(normcand, normreq): - return normcands[normcand] - if normcand > normreq: - continue - break - raise LookupError(f"No matching version for '{vendor}:{requested}'") + return { + k: v + for k, v in normcands.items() + if _is_version_compatible_with_spec(k, normreq) + } + + +def _match_version(vendor, candidates: list[str], requested) -> str: + matched = _match_versions(vendor, candidates, requested) + if len(matched) == 0: + raise LookupError(f"No matching version for '{vendor}:{requested}'") -_VER_SEPS = re.compile(r"[.-]") + return matched[max(matched)] + + +_VER_SEPS = re.compile(r"[.+_-]") def _normalize_version(ver, *, remove_prefix_1=False): # Normalize requested version and candidates: # - Split at dots and dashes (so we don't distinguish between '.' and '-') - # - Convert elements to integers (so that we require numeric elements and - # compare them numerically) + # - Try to convert elements to integers (so that we can compare elements + # numerically where feasible) # - If remove_prefix_1 and first element is 1, remove it (so JDK 1.8 == 8) - # - Return as tuple of ints (so that we compare lexicographically) + # - Return as a tuple (so that we compare element by element) # - Trailing zero elements are NOT removed, so, e.g., 11 < 11.0 (for the # most part, the index uses versions with the same number of elements # within a given vendor; versions like "11" are outliers) @@ -154,11 +213,8 @@ def _normalize_version(ver, *, remove_prefix_1=False): if is_plus: ver = ver[:-1] if ver: - norm = re.split(_VER_SEPS, ver) - try: - norm = tuple(int(e) for e in norm) - except ValueError as err: - raise ValueError(f"Invalid version string: {ver}") from err + norm = tuple(re.split(_VER_SEPS, ver)) + norm = tuple(_intify(e) for e in norm) else: norm = () plus = ("+",) if is_plus else () @@ -167,6 +223,13 @@ def _normalize_version(ver, *, remove_prefix_1=False): return norm + plus +def _intify(s: str): + try: + return int(s) + except ValueError: + return s + + def _is_version_compatible_with_spec(version, spec): assert "+" not in version is_plus = spec and spec[-1] == "+" diff --git a/src/cjdk/_install.py b/src/cjdk/_install.py index 412a4d9..f8fdfa2 100644 --- a/src/cjdk/_install.py +++ b/src/cjdk/_install.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: MIT import sys +from pathlib import Path from . import _cache, _download @@ -12,7 +13,9 @@ ] -def install_file(prefix, name, url, filename, conf, *, ttl, checkfunc=None): +def install_file( + prefix, name, url, filename, conf, *, ttl, checkfunc=None +) -> Path: def fetch(dest): _print_progress_header(conf, name) _download.download_file( @@ -33,7 +36,7 @@ def fetch(dest): ) -def install_dir(prefix, name, url, conf, *, checkfunc=None): +def install_dir(prefix, name, url, conf, *, checkfunc=None) -> Path: def fetch(destdir): _print_progress_header(conf, name) _download.download_and_extract( diff --git a/tests/test_api.py b/tests/test_api.py index 87912b9..f2bb84a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -152,3 +152,37 @@ def test_env_var_set(): with f("CJDK_TEST_ENV_VAR", "testvalue"): assert os.environ["CJDK_TEST_ENV_VAR"] == "testvalue" assert "CJDK_TEST_ENV_VAR" not in os.environ + + +def test_get_vendors(): + vendors = _api._get_vendors() + assert vendors is not None + assert "adoptium" in vendors + assert "corretto" in vendors + assert "graalvm" in vendors + assert "ibm-semeru-openj9" in vendors + assert "java-oracle" in vendors + assert "liberica" in vendors + assert "temurin" in vendors + assert "zulu" in vendors + + +def test_get_jdks(): + jdks = _api._get_jdks(cached_only=False) + assert jdks is not None + assert "adoptium:1.21.0.4" in jdks + assert "corretto:21.0.4.7.1" in jdks + assert "graalvm-community:21.0.2" in jdks + assert "graalvm-java21:21.0.2" in jdks + assert "liberica:22.0.2" in jdks + assert "temurin:1.21.0.4" in jdks + assert "zulu:8.0.362" in jdks + + cached_jdks = _api._get_jdks() + assert cached_jdks is not None + assert len(cached_jdks) < len(jdks) + + zulu_jdks = _api._get_jdks(vendor="zulu", cached_only=False) + assert zulu_jdks is not None + assert len(set(zulu_jdks)) + assert all(jdk.startswith("zulu:") for jdk in zulu_jdks) diff --git a/tests/test_conf.py b/tests/test_conf.py index 0c45a7a..8779ef3 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -30,6 +30,10 @@ def test_configure(): assert conf.vendor == _conf._default_vendor() assert not conf.version + conf = f(jdk=":", fallback_to_default_vendor=False) + assert conf.vendor is None + assert not conf.version + conf = f(cache_dir="abc") assert conf.cache_dir == Path("abc") diff --git a/tests/test_index.py b/tests/test_index.py index 1ab089a..1650f2d 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -87,9 +87,28 @@ def test_jdk_url(): ) == "tgz+https://example.com/a/b/c.tgz" ) + assert ( + _index.jdk_url( + index, + configure( + os="linux", arch="amd64", vendor="adoptium", version="17" + ), + ) + == "tgz+https://example.com/a/b/c.tgz" + ) + assert ( + _index.jdk_url( + index, + configure( + os="linux", arch="amd64", vendor="adoptium", version="11" + ), + exact_version="17.0.1", + ) + == "tgz+https://example.com/a/b/c.tgz" + ) -def test_cached_index(tmp_path): +def test_cached_index_path(tmp_path): with mock_server.start( endpoint="/index.json", data={"hello": "world"} ) as server: @@ -101,7 +120,7 @@ def test_cached_index(tmp_path): / _cache._key_for_url(url) / _index._INDEX_FILENAME ) - path = _index._cached_index( + path = _index._cached_index_path( configure( index_url=url, cache_dir=tmp_path, @@ -116,7 +135,7 @@ def test_cached_index(tmp_path): # Second time should return same data without fetching. assert server.request_count() == 1 - path2 = _index._cached_index( + path2 = _index._cached_index_path( configure( index_url=url, cache_dir=tmp_path, @@ -140,6 +159,62 @@ def test_read_index(tmp_path): assert _index._read_index(path) == data +def test_postprocess_index(): + index = { + "linux": { + "amd64": { + "jdk@ibm-semeru-openj9-java11": { + "11.0.21+9_openj9-0.41.0": "a", + "11.0.22+7_openj9-0.43.0": "b", + "11.0.23+9_openj9-0.44.0": "c", + }, + "jdk@ibm-semeru-openj9-java17": { + "17.0.1+12_openj9-0.29.1": "d", + "17.0.2+8_openj9-0.30.0": "e", + "17.0.3+7_openj9-0.32.0": "f", + }, + "jdk@ibm-semeru-openj9-java21": { + "21.0.1+12_openj9-0.42.0": "g", + "21.0.2+13_openj9-0.43.0": "h", + }, + "jdk@not-semeru": {"8.0.252": "i"}, + } + } + } + pp_index = _index._postprocess_index(index) + assert pp_index is index + assert "jdk@ibm-semeru-openj9" in index["linux"]["amd64"] + assert len(index["linux"]["amd64"]["jdk@ibm-semeru-openj9"]) == 8 + + +def test_match_versions(): + f = _index._match_versions + assert f("adoptium", ["10", "11.0", "11.1", "1.12.0"], "11") == { + (11, 0): "11.0", + (11, 1): "11.1", + } + assert f("adoptium", ["10", "11.0", "11.1", "1.12.0"], "12") == { + (12, 0): "1.12.0" + } + assert f("graalvm", ["10", "11.0", "11.1", "1.12.0"], "11") == { + (11, 0): "11.0", + (11, 1): "11.1", + } + assert f("graalvm", ["10", "11.0", "11.1", "1.12.0"], "1") == { + (1, 12, 0): "1.12.0" + } + assert f("temurin", ["11.0", "17.0", "18.0"], "") == { + (11, 0): "11.0", + (17, 0): "17.0", + (18, 0): "18.0", + } + assert f("temurin", ["11.0", "17.0", "18.0"], "17+") == { + (17, 0): "17.0", + (18, 0): "18.0", + } + assert f("temurin", ["11.0", "17.0", "18.0"], "19+") == {} + + def test_match_version(): f = _index._match_version assert f("adoptium", ["10", "11.0", "11.1", "1.12.0"], "11") == "11.1" @@ -160,8 +235,8 @@ def test_normalize_version(): assert f("1", remove_prefix_1=True) == () assert f("1.8", remove_prefix_1=True) == (8,) assert f("1.8.0", remove_prefix_1=True) == (8, 0) - with pytest.raises(ValueError): - f("1.8u300", remove_prefix_1=True) + assert f("1.8u300", remove_prefix_1=True) == ("8u300",) + assert f("21.0.1+12_openj9-0.42.0") == (21, 0, 1, 12, "openj9", 0, 42, 0) def test_is_version_compatible_with_spec():