From 4d03e80ac1bc93f2738e18a64aed16eecaed4690 Mon Sep 17 00:00:00 2001 From: Stefaan Lippens Date: Tue, 12 Mar 2024 21:05:32 +0100 Subject: [PATCH] Issue #50 include openeo-api and openeo-process versions in reports --- src/openeo_test_suite/__init__.py | 2 +- src/openeo_test_suite/lib/pytest_plugin.py | 15 ++- src/openeo_test_suite/lib/version.py | 122 ++++++++++++++++++++ src/openeo_test_suite/tests/test_version.py | 109 +++++++++++++++++ 4 files changed, 242 insertions(+), 6 deletions(-) create mode 100644 src/openeo_test_suite/lib/version.py create mode 100644 src/openeo_test_suite/tests/test_version.py diff --git a/src/openeo_test_suite/__init__.py b/src/openeo_test_suite/__init__.py index d3ec452..3ced358 100644 --- a/src/openeo_test_suite/__init__.py +++ b/src/openeo_test_suite/__init__.py @@ -1 +1 @@ -__version__ = "0.2.0" +__version__ = "0.2.1" diff --git a/src/openeo_test_suite/lib/pytest_plugin.py b/src/openeo_test_suite/lib/pytest_plugin.py index e65bede..fbc7813 100644 --- a/src/openeo_test_suite/lib/pytest_plugin.py +++ b/src/openeo_test_suite/lib/pytest_plugin.py @@ -1,6 +1,8 @@ import argparse +import json import shlex import sys +from pathlib import Path import openeo import pytest @@ -14,6 +16,7 @@ set_backend_under_test, ) from openeo_test_suite.lib.process_selection import set_process_selection_from_config +from openeo_test_suite.lib.version import get_openeo_versions def pytest_addoption(parser: pytest.Parser): @@ -84,10 +87,12 @@ def pytest_configure(config: pytest.Config): # Add some additional info to HTML report # https://pytest-html.readthedocs.io/en/latest/user_guide.html#environment - config.stash[pytest_metadata.plugin.metadata_key]["Invocation"] = _invocation() - config.stash[pytest_metadata.plugin.metadata_key][ - "openEO Test Suite version" - ] = openeo_test_suite.__version__ + config.stash[pytest_metadata.plugin.metadata_key].update( + { + "Invocation": _invocation(), + "openEO versions": get_openeo_versions(), + } + ) def _invocation() -> str: @@ -99,7 +104,7 @@ def pytest_report_header(config: pytest.Config): """Implementation of `pytest_report_header` hook.""" # Add info to terminal report return [ - f"openEO Test Suite {openeo_test_suite.__version__}", + f"openEO versions: {get_openeo_versions()}", f"Invoked with: {_invocation()}", ] diff --git a/src/openeo_test_suite/lib/version.py b/src/openeo_test_suite/lib/version.py new file mode 100644 index 0000000..4286fd4 --- /dev/null +++ b/src/openeo_test_suite/lib/version.py @@ -0,0 +1,122 @@ +import json +import logging +import subprocess +from pathlib import Path +from typing import Dict, List, Optional, Union + +import openeo_test_suite + +_log = logging.getLogger(__name__) + + +_ON_ERROR_IGNORE = "ignore" +_ON_ERROR_WARN = "warn" +_ON_ERROR_FAIL = "fail" + + +# TODO: make this compatible with packaging? +PROJECT_ROOT = Path(openeo_test_suite.__file__).parents[2] + + +def get_openeo_versions() -> Dict[str, str]: + return { + "openeo-test-suite": get_openeo_test_suite_version(name_prefix=None), + "openeo-api": get_openeo_api_spec_version(name_prefix=None), + "openeo-processes": get_openeo_processes_spec_version(name_prefix=None), + } + + +def _join_non_empties(*parts: str, glue: str = " ") -> str: + """Join non-empty parts""" + return glue.join(p for p in parts if p) + + +def get_openeo_test_suite_version( + *, + name_prefix: Optional[str] = "openeo-test-suite", + on_error: str = _ON_ERROR_WARN, + git_describe: bool = True, +) -> str: + """Build openeo-test-suite version, optionally with name prefix and git rev suffix (if possible)""" + return _join_non_empties( + name_prefix, + openeo_test_suite.__version__, + _git_describe(PROJECT_ROOT, on_error=on_error) if git_describe else None, + ) + + +def get_openeo_api_spec_version( + *, + name_prefix: Optional[str] = "openeo-api", + on_error: str = _ON_ERROR_WARN, + git_describe: bool = True, +) -> str: + """Build version of current openeo-api submodule, optionally with name prefix and git rev suffix (if possible)""" + package_path = PROJECT_ROOT / "assets" / "openeo-api" / "package.json" + return _join_non_empties( + name_prefix, + _get_js_package_version(package_path, on_error=on_error), + _git_describe(package_path, on_error=on_error) if git_describe else None, + ) + + +def get_openeo_processes_spec_version( + *, + name_prefix: Optional[str] = "openeo-processes", + on_error: str = _ON_ERROR_WARN, + git_describe: bool = True, +) -> str: + """Build version of current openeo-processes submodule, optionally with name prefix and git rev suffix (if possible)""" + package_path = PROJECT_ROOT / "assets" / "processes" / "dev" / "package.json" + return _join_non_empties( + name_prefix, + _get_js_package_version(package_path, on_error=on_error), + _git_describe(package_path, on_error=on_error) if git_describe else None, + ) + + +def _get_js_package_version( + package_path: Path, on_error: str = _ON_ERROR_WARN, default: str = "unknown" +) -> str: + """ + Get version of a "package.json" JS project + + :param package_path: path to package.json file + """ + try: + package_metadata = json.loads(package_path.read_text()) + version = package_metadata["version"] + except Exception as e: + if on_error == _ON_ERROR_WARN: + _log.warning(f"Failed to parse package version from {package_path}: {e}") + elif on_error == _ON_ERROR_IGNORE: + pass + else: + raise + version = default + return version + + +def _git_describe( + path: Path, *, wrap: str = "()", on_error: str = _ON_ERROR_WARN +) -> Union[str, None]: + """ + Get short git rev description (possibly with "dirty" indicator), e.g. 'abc123' or 'abc123-dirty' + """ + try: + if not path.is_dir(): + path = path.parent + assert path.is_dir() + command = ["git", "describe", "--always", "--dirty"] + description = subprocess.check_output(command, cwd=path).strip().decode("utf-8") + if wrap: + description = f"{wrap[0]}{description}{wrap[-1]}" + except Exception: + if on_error == _ON_ERROR_WARN: + _log.warning(f"Failed to git-describe {path}") + elif on_error == _ON_ERROR_IGNORE: + pass + else: + raise + description = None + return description diff --git a/src/openeo_test_suite/tests/test_version.py b/src/openeo_test_suite/tests/test_version.py new file mode 100644 index 0000000..6a21bb6 --- /dev/null +++ b/src/openeo_test_suite/tests/test_version.py @@ -0,0 +1,109 @@ +import re +import subprocess + +import pytest + +from openeo_test_suite.lib.version import ( + _ON_ERROR_FAIL, + _ON_ERROR_IGNORE, + _ON_ERROR_WARN, + PROJECT_ROOT, + _get_js_package_version, + _git_describe, + get_openeo_api_spec_version, + get_openeo_processes_spec_version, + get_openeo_test_suite_version, +) + + +def test_get_openeo_test_suite_version(): + assert re.fullmatch( + r"openeo-test-suite [0-9.]+ \([0-9a-f]+(-dirty)?\)", + get_openeo_test_suite_version(), + ) + assert re.fullmatch( + r"[0-9.]+ \([0-9a-f]+(-dirty)?\)", + get_openeo_test_suite_version(name_prefix=None), + ) + assert re.fullmatch( + r"[0-9.]+", get_openeo_test_suite_version(name_prefix=None, git_describe=False) + ) + + +def test_get_openeo_api_spec_version(): + assert re.fullmatch( + r"openeo-api [0-9.a-z-]+ \([0-9a-f]+\)", get_openeo_api_spec_version() + ) + assert re.fullmatch( + r"[0-9.a-z-]+", + get_openeo_api_spec_version(name_prefix=None, git_describe=False), + ) + + +def test_get_openeo_processes_spec_version(): + assert re.fullmatch( + r"openeo-processes [0-9.a-z-]+ \([0-9a-f]+\)", + get_openeo_processes_spec_version(), + ) + assert re.fullmatch( + r"[0-9.a-z-]+", + get_openeo_processes_spec_version(name_prefix=None, git_describe=False), + ) + + +def test_get_js_package_version(tmp_path): + path = tmp_path / "package.json" + path.write_text('{"version":"1.2.3"}') + assert _get_js_package_version(path) == "1.2.3" + + +def test_get_js_package_version_on_error_default(tmp_path, caplog): + path = tmp_path / "package.json" + assert _get_js_package_version(path) == "unknown" + assert "Failed to parse package version from" in caplog.text + + +def test_get_js_package_version_on_error_ignore(tmp_path, caplog): + path = tmp_path / "package.json" + assert _get_js_package_version(path, on_error=_ON_ERROR_IGNORE) == "unknown" + assert caplog.text == "" + + +def test_get_js_package_version_on_error_warn(tmp_path, caplog): + path = tmp_path / "package.json" + assert _get_js_package_version(path, on_error=_ON_ERROR_WARN) == "unknown" + assert "Failed to parse package version from" in caplog.text + + +def test_get_js_package_version_on_error_fail(tmp_path, caplog): + path = tmp_path / "package.json" + with pytest.raises(FileNotFoundError): + _ = _get_js_package_version(path, on_error=_ON_ERROR_FAIL) + + +def test_git_describe(): + assert re.fullmatch(r"\([0-9a-f]+(-dirty)?\)", _git_describe(PROJECT_ROOT)) + + +def test_git_describe_wrap(): + assert re.fullmatch(r"<[0-9a-f]+(-dirty)?>", _git_describe(PROJECT_ROOT, wrap="<>")) + + +def test_git_describe_on_error_default(tmp_path, caplog): + assert _git_describe(tmp_path) is None + assert "Failed to git-describe" in caplog.text + + +def test_git_describe_on_error_ignore(tmp_path, caplog): + assert _git_describe(tmp_path, on_error=_ON_ERROR_IGNORE) is None + assert caplog.text == "" + + +def test_git_describe_on_error_warn(tmp_path, caplog): + assert _git_describe(tmp_path, on_error=_ON_ERROR_WARN) is None + assert "Failed to git-describe" in caplog.text + + +def test_git_describe_on_error_fail(tmp_path, caplog): + with pytest.raises(subprocess.CalledProcessError): + _ = _git_describe(tmp_path, on_error=_ON_ERROR_FAIL)