diff --git a/.github/workflows/internal_tests.yml b/.github/workflows/internal_tests.yml new file mode 100644 index 00000000000..c1d0f518313 --- /dev/null +++ b/.github/workflows/internal_tests.yml @@ -0,0 +1,24 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +name: Internal Tooling Tests +on: + pull_request: + types: [opened, reopened, synchronize] + push: + branches: + - main +jobs: + internal_tests: + uses: eclipse-score/cicd-workflows/.github/workflows/tests.yml@main + with: + bazel-target: "test //scripts/tooling:tooling_tests" diff --git a/.github/workflows/test_and_docs.yml b/.github/workflows/test_and_docs.yml index 0d0f1574259..ad27a1fcb00 100644 --- a/.github/workflows/test_and_docs.yml +++ b/.github/workflows/test_and_docs.yml @@ -30,6 +30,8 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: ${{ github.ref_name != 'main' && !startsWith(github.ref_name, 'release/') }} +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: test_and_docs: runs-on: ubuntu-22.04 @@ -111,6 +113,8 @@ jobs: --github_user=${{ github.repository_owner }} \ --github_repo=${{ github.event.repository.name }} + CURRENT=$(realpath .) + bazel run //scripts/tooling -- misc html_report --output ${CURRENT}/_build/status_dashboard.html tar -cf github-pages.tar _build - name: Upload documentation artifact uses: actions/upload-artifact@v4.4.0 diff --git a/BUILD b/BUILD index 12c556345e6..6ef3b7969df 100644 --- a/BUILD +++ b/BUILD @@ -67,4 +67,5 @@ use_format_targets() exports_files([ "MODULE.bazel", + "pyproject.toml", ]) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 991e8543a17..d3c9febd033 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -59,7 +59,8 @@ "https://bcr.bazel.build/modules/aspect_rules_lint/2.0.0/source.json": "3c3a55b5b424100feca2fd656dcdcd8a0c9fd3304ce609ce71a4d6d46d00a03c", "https://bcr.bazel.build/modules/aspect_rules_py/1.0.0/MODULE.bazel": "8eb29876512d3242af50a424300bec5c5f8957b455963df5f618cb7fd4e8ae19", "https://bcr.bazel.build/modules/aspect_rules_py/1.4.0/MODULE.bazel": "6fd29b93207a31445d5d3ab9d9882fd5511e43c95e8e82e7492872663720fd44", - "https://bcr.bazel.build/modules/aspect_rules_py/1.4.0/source.json": "fb1ba946478fb6dbb26d49307d756b0fd2ff88be339af23c39c0397d59143d2c", + "https://bcr.bazel.build/modules/aspect_rules_py/1.5.2/MODULE.bazel": "7e34964847c5ddf391a927a765d8b26147df5b25242f2da4fa3c4a77671383bb", + "https://bcr.bazel.build/modules/aspect_rules_py/1.5.2/source.json": "020d2bd6b07210cee97226ee2215d80c4cf91e455a6702c9af1e5788d072e673", "https://bcr.bazel.build/modules/aspect_rules_ts/3.6.0/MODULE.bazel": "d0045b5eabb012be550a609589b3e5e47eba682344b19cfd9365d4d896ed07df", "https://bcr.bazel.build/modules/aspect_rules_ts/3.6.0/source.json": "5593e3f1cd0dd5147f7748e163307fd5c2e1077913d6945b58739ad8d770a290", "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.2.5/MODULE.bazel": "5a3c8013c3ba9ebc0a65efda40e4376b869e1260873c98020504feed55244ce8", @@ -583,6 +584,7 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_rules_lint/2.0.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_rules_py/1.0.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_rules_py/1.4.0/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_rules_py/1.5.2/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_rules_ts/3.6.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_tools_telemetry/0.2.5/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_tools_telemetry/0.2.8/MODULE.bazel": "not found", @@ -1141,8 +1143,8 @@ }, "@@aspect_rules_py+//py:extensions.bzl%py_tools": { "general": { - "bzlTransitiveDigest": "dAWBz6nfg2GIP7kchPavcBdWoHNQ67aXhXo4MPJJqNk=", - "usagesDigest": "NC1b49l5tenTBVWEUGzzC0j5Kg1GH+l5lBw5JRCldIU=", + "bzlTransitiveDigest": "i1RUlZ7o0KqC5rka4jpWVnev/OpAD7TaATTPe15qmRw=", + "usagesDigest": "NF8rrFAtM0OxyfflUbQNlgnkIH3YSHU4jCm3dhvpGY4=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, @@ -1190,25 +1192,25 @@ } }, "rules_py_tools.darwin_amd64": { - "repoRuleId": "@@aspect_rules_py+//py/private/toolchain:tools.bzl%prebuilt_tool_repo", + "repoRuleId": "@@aspect_rules_py+//py/private/toolchain:repo.bzl%prebuilt_tool_repo", "attributes": { "platform": "darwin_amd64" } }, "rules_py_tools.darwin_arm64": { - "repoRuleId": "@@aspect_rules_py+//py/private/toolchain:tools.bzl%prebuilt_tool_repo", + "repoRuleId": "@@aspect_rules_py+//py/private/toolchain:repo.bzl%prebuilt_tool_repo", "attributes": { "platform": "darwin_arm64" } }, "rules_py_tools.linux_amd64": { - "repoRuleId": "@@aspect_rules_py+//py/private/toolchain:tools.bzl%prebuilt_tool_repo", + "repoRuleId": "@@aspect_rules_py+//py/private/toolchain:repo.bzl%prebuilt_tool_repo", "attributes": { "platform": "linux_amd64" } }, "rules_py_tools.linux_arm64": { - "repoRuleId": "@@aspect_rules_py+//py/private/toolchain:tools.bzl%prebuilt_tool_repo", + "repoRuleId": "@@aspect_rules_py+//py/private/toolchain:repo.bzl%prebuilt_tool_repo", "attributes": { "platform": "linux_arm64" } @@ -1241,6 +1243,11 @@ "aspect_bazel_lib", "aspect_bazel_lib+" ], + [ + "aspect_rules_py+", + "bazel_skylib", + "bazel_skylib+" + ], [ "aspect_rules_py+", "bazel_tools", diff --git a/README.md b/README.md index 5069bbeba85..bdcf8d7cf1f 100644 --- a/README.md +++ b/README.md @@ -171,3 +171,8 @@ local_path_override(module_name = "score_tooling", path = "../tooling") ### Rust Use `scripts/generate_rust_analyzer_support.sh` to generate rust_analyzer settings that will let VS Code work. + +## Internal tooling + +Internal tooling scripts are currently under development to provide user single point of interaction with all +created goods. More detailed readme can be found in scripts: [Tooling README](scripts/tooling/README.md) diff --git a/bazel_common/score_python.MODULE.bazel b/bazel_common/score_python.MODULE.bazel index 994e9a6af24..08ceadcd674 100644 --- a/bazel_common/score_python.MODULE.bazel +++ b/bazel_common/score_python.MODULE.bazel @@ -11,6 +11,7 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* bazel_dep(name = "rules_python", version = "1.8.3") +bazel_dep(name = "aspect_rules_py", version = "1.5.2") PYTHON_VERSION = "3.12" @@ -28,3 +29,12 @@ pip.parse( requirements_lock = "//feature_integration_tests/test_cases:requirements.txt.lock", ) use_repo(pip, "pip_score_venv_test") + +pip.parse( + envsubst = ["PIP_INDEX_URL"], + extra_pip_args = ["--index-url=${PIP_INDEX_URL:-https://pypi.org/simple/}"], + hub_name = "ref_int_scripts_env", + python_version = PYTHON_VERSION, + requirements_lock = "//scripts/tooling:requirements.txt", +) +use_repo(pip, "ref_int_scripts_env") diff --git a/docs/index.rst b/docs/index.rst index 188e48d8a8f..c99a0f4638b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,6 +30,11 @@ Newest Release Notes newest_release_note = max(all_release_notes, key=lambda s: int(re.search(r'v(\d+)', s["id"]).group(1))) results = [newest_release_note] +Current Integration Status Overview +----------------------------------- + +`View dashboard (points always to main for now) `_ + Explore the documentation ------------------------- .. toctree:: diff --git a/pyproject.toml b/pyproject.toml index 2317e7d70ee..6b8b4dba378 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,10 @@ +[tool.pytest] +pythonpath = [ + ".", + "scripts/tooling", +] +testpaths = ["scripts/tooling/tests"] + [tool.ruff] # Exclude a variety of commonly ignored directories. exclude = [ @@ -75,7 +82,7 @@ select = [ # pyupgrade "UP", ] -ignore = ["F401", "PTH123", "ARG002", "T201"] +ignore = ["F401", "PTH123", "ARG002", "T201", "TC003"] # Allow fix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] diff --git a/scripts/tooling/BUILD b/scripts/tooling/BUILD new file mode 100644 index 00000000000..40748469520 --- /dev/null +++ b/scripts/tooling/BUILD @@ -0,0 +1,73 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("@ref_int_scripts_env//:requirements.bzl", "all_requirements") +load("@rules_python//python:defs.bzl", "py_binary", "py_library") +load("@rules_python//python:pip.bzl", "compile_pip_requirements") +load("@score_tooling//python_basics:defs.bzl", "score_py_pytest") + +# In order to update the requirements, change the `requirements.in` file and run: +# `bazel run //src:requirements.update`. +# This will update the `requirements.txt` file. +# To upgrade all dependencies to their latest versions, run: +# `bazel run //src:requirements.update -- --upgrade`. +compile_pip_requirements( + name = "requirements", + srcs = [ + "requirements.in", + "@score_tooling//python_basics:requirements.txt", + ], + requirements_txt = "requirements.txt", + tags = [ + "manual", + ], +) + +# Library target +py_library( + name = "lib", + srcs = glob(["lib/**/*.py"]), + visibility = ["//visibility:public"], +) + +# CLI library target (shared between binary and tests) +py_library( + name = "cli", + srcs = glob(["cli/**/*.py"]), + data = [ + ":cli/misc/assets/report_template.html", + ], + deps = [":lib"] + all_requirements, +) + +# CLI binary target +py_binary( + name = "tooling", + srcs = ["cli/main.py"], + main = "cli/main.py", + visibility = ["//visibility:public"], + deps = [":cli"], +) + +# Tests target +score_py_pytest( + name = "tooling_tests", + srcs = glob(["tests/**/*.py"]), + data = [ + ":cli/misc/assets/report_template.html", + ], + pytest_config = "//:pyproject.toml", + deps = [ + ":cli", + ":lib", + ] + all_requirements, +) diff --git a/scripts/tooling/README.md b/scripts/tooling/README.md new file mode 100644 index 00000000000..68466ecebf2 --- /dev/null +++ b/scripts/tooling/README.md @@ -0,0 +1,32 @@ +# Tooling scripts + +## Running tooling CLI + +```bash +bazel run //scripts/tooling -- --help +``` + +```bash +bazel run //scripts/tooling -- misc --help +``` + +## Creating HTML report + +```bash +bazel run //scripts/tooling -- misc html_report +``` + +## Running tests + +```bash +bazel test //scripts/tooling_tests +``` + +## Updating dependencies + +Update a list of requirements in [requirements.in](requirements.in) file and then +regenerate lockfile [requirements.txt](requirements.txt) with: + +```bash +bazel run //scripts/tooling:requirements.update +``` diff --git a/scripts/tooling/cli/__init__.py b/scripts/tooling/cli/__init__.py new file mode 100644 index 00000000000..ca5de742e9c --- /dev/null +++ b/scripts/tooling/cli/__init__.py @@ -0,0 +1,12 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* diff --git a/scripts/tooling/cli/main.py b/scripts/tooling/cli/main.py new file mode 100644 index 00000000000..f66e6a5c8cb --- /dev/null +++ b/scripts/tooling/cli/main.py @@ -0,0 +1,31 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +import argparse +import sys + + +def main() -> None: + parser = argparse.ArgumentParser(prog="tooling") + subparsers = parser.add_subparsers(dest="group", metavar="GROUP") + subparsers.required = True + + from scripts.tooling.cli.misc import register as _register_misc + + _register_misc(subparsers) + + args = parser.parse_args() + sys.exit(args.func(args)) + + +if __name__ == "__main__": + main() diff --git a/scripts/tooling/cli/misc/__init__.py b/scripts/tooling/cli/misc/__init__.py new file mode 100644 index 00000000000..01ca28786ab --- /dev/null +++ b/scripts/tooling/cli/misc/__init__.py @@ -0,0 +1,23 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +import argparse + + +def register(subparsers: argparse._SubParsersAction) -> None: + misc_parser = subparsers.add_parser("misc", help="Miscellaneous utilities") + misc_sub = misc_parser.add_subparsers(dest="command", metavar="COMMAND") + misc_sub.required = True + + from scripts.tooling.cli.misc.html_report import register as _register_html_report + + _register_html_report(misc_sub) diff --git a/scripts/tooling/cli/misc/assets/report_template.html b/scripts/tooling/cli/misc/assets/report_template.html new file mode 100644 index 00000000000..02380cfedfd --- /dev/null +++ b/scripts/tooling/cli/misc/assets/report_template.html @@ -0,0 +1,583 @@ + + + + + + Known Good Status + + + +
+

Known Good Status

+

Snapshot: {{ timestamp }}

+ +
+
+
+
+ +
+
+
+ +
+ 🔒 Add a GitHub personal access token (PAT) for live status — exact commit counts fetched directly from GitHub. + Enter it in the token field above, or + create one here. + Without a token, up-to-date / behind status is still shown from data embedded at report generation time. + Your PAT is not sent anywhere — it is only kept in the local cache of this page. +
+ +
+
+ + + + + + diff --git a/scripts/tooling/cli/misc/html_report.py b/scripts/tooling/cli/misc/html_report.py new file mode 100644 index 00000000000..5fa83771e8a --- /dev/null +++ b/scripts/tooling/cli/misc/html_report.py @@ -0,0 +1,138 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +from __future__ import annotations + +import argparse +import json +import logging +import os +import sys +from pathlib import Path +from typing import Any, Optional + +from jinja2 import Environment, FileSystemLoader, select_autoescape + +from scripts.tooling.lib.github import fetch_compare +from scripts.tooling.lib.known_good import KnownGood, load_known_good + +_LOG = logging.getLogger(__name__) + +TEMPLATE_DIR = Path(__file__).parent / "assets" + + +def _find_repo_root() -> Path: + candidate = Path(__file__).resolve() + for parent in candidate.parents: + if (parent / "known_good.json").exists(): + return parent + return Path.cwd() + + +def _resolve_path_from_bazel(path: Path) -> Path: + if not path.is_absolute(): + build_working_dir = os.environ.get("BUILD_WORKING_DIRECTORY") + if build_working_dir: + return (Path(build_working_dir) / path).resolve() + return path.resolve() + + +def _collect_entries(known_good: KnownGood) -> list[dict[str, Any]]: + entries = [] + for group_name, group_modules in known_good.modules.items(): + for module in group_modules.values(): + try: + owner_repo = module.owner_repo + except ValueError: + owner_repo = None + entries.append( + { + "name": module.name, + "group": group_name, + "repo": module.repo, + "owner_repo": owner_repo, + "hash": module.hash, + "version": module.version, + "branch": module.branch, + "current_hash": None, + "behind_by": None, + "compare_status": None, + } + ) + return entries + + +def _enrich_with_compare_data(entries: list[dict[str, Any]], token: str) -> None: + for entry in entries: + if not entry.get("owner_repo") or not entry.get("hash") or entry.get("version"): + continue + result = fetch_compare(entry["owner_repo"], entry["hash"], entry["branch"], token) + if result: + entry["current_hash"] = result.head_sha + entry["behind_by"] = result.ahead_by + entry["compare_status"] = result.status + else: + _LOG.warning("Could not fetch compare data for %s@%s", entry["owner_repo"], entry["branch"]) + + +def generate_report(known_good: KnownGood, token: Optional[str] = None) -> str: + entries = _collect_entries(known_good) + if token: + _enrich_with_compare_data(entries, token) + env = Environment( + loader=FileSystemLoader(TEMPLATE_DIR), + autoescape=select_autoescape(["html"]), + ) + tmpl = env.get_template("report_template.html") + return tmpl.render( + modules_json=json.dumps(entries, indent=2), + timestamp=known_good.timestamp, + ) + + +def write_report(known_good: KnownGood, output_path: Path, token: Optional[str] = None) -> None: + Path(output_path).write_text(generate_report(known_good, token), encoding="utf-8") + + +def register(subparsers: argparse._SubParsersAction) -> None: + parser = subparsers.add_parser("html_report", help="Generate an HTML status report from known_good.json") + parser.add_argument( + "--known_good", + metavar="PATH", + default=str(_find_repo_root()), + help="Directory containing known_good.json (default: repo root)", + ) + parser.add_argument( + "--output", + metavar="FILE", + default="report.html", + help="Output HTML file path (default: report.html)", + ) + parser.set_defaults(func=_run) + + +def _run(args: argparse.Namespace) -> int: + known_good_path = Path(args.known_good) / "known_good.json" + try: + known_good = load_known_good(known_good_path) + except (FileNotFoundError, ValueError) as exc: + print(f"error: {exc}", file=sys.stderr) + return 1 + + token = os.environ.get("GITHUB_TOKEN") + output = _resolve_path_from_bazel(Path(args.output)) + write_report(known_good, output, token=token) + if token: + print(f"Report written to {output} (current hashes fetched from GitHub)") + else: + print(f"Report written to {output} (set GITHUB_TOKEN to embed current hashes)") + return 0 diff --git a/scripts/tooling/lib/__init__.py b/scripts/tooling/lib/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/scripts/tooling/lib/github.py b/scripts/tooling/lib/github.py new file mode 100644 index 00000000000..7c07f25038a --- /dev/null +++ b/scripts/tooling/lib/github.py @@ -0,0 +1,95 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""GitHub REST API helpers. + +Lightweight wrappers around the GitHub API using only stdlib (urllib). +No third-party dependencies are required. +""" + +from __future__ import annotations + +import json +import logging +import urllib.error +import urllib.request +from dataclasses import dataclass +from typing import Optional + +_LOG = logging.getLogger(__name__) + +_API_BASE = "https://api.github.com" + + +def _make_request(url: str, token: Optional[str]) -> urllib.request.Request: + req = urllib.request.Request(url) + req.add_header("Accept", "application/vnd.github+json") + req.add_header("X-GitHub-Api-Version", "2022-11-28") + if token: + req.add_header("Authorization", f"Bearer {token}") + return req + + +@dataclass +class CompareResult: + """Result of comparing a pinned commit against a branch HEAD.""" + + ahead_by: int + """How many commits the branch HEAD is ahead of the pinned hash (i.e. commits behind).""" + status: str + """GitHub compare status: ``"identical"``, ``"ahead"``, ``"behind"``, or ``"diverged"``.""" + head_sha: str + """Current HEAD SHA of the target branch.""" + + +def fetch_compare( + owner_repo: str, + base_hash: str, + branch: str, + token: Optional[str] = None, +) -> Optional[CompareResult]: + """Compare *base_hash* against the HEAD of *branch* in *owner_repo*. + + Returns a :class:`CompareResult` with the number of commits the pinned hash + is behind and the current HEAD SHA, or ``None`` on any error. + + Args: + owner_repo: GitHub ``owner/repo`` slug (e.g. ``"eclipse-score/baselibs"``). + base_hash: The pinned commit SHA to compare from. + branch: Branch name to compare against. + token: Optional GitHub PAT or ``GITHUB_TOKEN``. + Without a token requests are unauthenticated (60 req/h rate limit). + """ + url = f"{_API_BASE}/repos/{owner_repo}/compare/{base_hash}...{branch}" + req = _make_request(url, token) + try: + with urllib.request.urlopen(req, timeout=10) as resp: + data = json.loads(resp.read()) + commits = data.get("commits", []) + head_sha = commits[-1]["sha"] if commits else base_hash + return CompareResult( + ahead_by=data.get("ahead_by", 0), + status=data.get("status", ""), + head_sha=head_sha, + ) + except urllib.error.HTTPError as exc: + _LOG.debug( + "GitHub compare HTTP %s for %s %s...%s: %s", + exc.code, + owner_repo, + base_hash[:10], + branch, + exc.reason, + ) + except OSError as exc: + _LOG.debug("GitHub compare network error for %s: %s", owner_repo, exc) + return None diff --git a/scripts/tooling/lib/known_good/__init__.py b/scripts/tooling/lib/known_good/__init__.py new file mode 100644 index 00000000000..ff5ddeb436f --- /dev/null +++ b/scripts/tooling/lib/known_good/__init__.py @@ -0,0 +1,18 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""known_good parsing utilities.""" + +from .known_good import KnownGood, load_known_good +from .module import Metadata, Module + +__all__ = ["KnownGood", "load_known_good", "Module", "Metadata"] diff --git a/scripts/tooling/lib/known_good/known_good.py b/scripts/tooling/lib/known_good/known_good.py new file mode 100644 index 00000000000..1d6bdfe0f5b --- /dev/null +++ b/scripts/tooling/lib/known_good/known_good.py @@ -0,0 +1,72 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""KnownGood model and loader for score reference integration.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict + +from .module import Module + + +@dataclass +class KnownGood: + """Parsed contents of known_good.json. + + modules: {"group_name": {"module_name": Module, ...}, ...} + """ + + modules: Dict[str, Dict[str, Module]] + timestamp: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> KnownGood: + parsed: Dict[str, Dict[str, Module]] = {} + for group_name, group_modules in data.get("modules", {}).items(): + if isinstance(group_modules, dict): + parsed[group_name] = {m.name: m for m in Module.parse_modules(group_modules)} + return cls(modules=parsed, timestamp=data.get("timestamp", "")) + + +def load_known_good(path: Path) -> KnownGood: + """Parse known_good.json at *path* and return a typed :class:`KnownGood`. + + Args: + path: Path to the known_good.json file. + + Returns: + Fully-typed KnownGood instance. + + Raises: + ValueError: On malformed JSON or unexpected top-level structure. + FileNotFoundError: If *path* does not exist. + """ + text = Path(path).read_text(encoding="utf-8") + try: + data = json.loads(text) + except json.JSONDecodeError as e: + lines = text.splitlines() + line = lines[e.lineno - 1] if 0 <= e.lineno - 1 < len(lines) else "" + pointer = " " * (e.colno - 1) + "^" + hint = "Possible causes: trailing comma, missing value, or extra comma." if "Expecting value" in e.msg else "" + raise ValueError( + f"Invalid JSON at line {e.lineno}, column {e.colno}\n{line}\n{pointer}\n{e.msg}. {hint}" + ) from None + + if not isinstance(data, dict) or not isinstance(data.get("modules"), dict): + raise ValueError(f"Invalid known_good.json at {path}: expected object with 'modules' dict") + + return KnownGood.from_dict(data) diff --git a/scripts/tooling/lib/known_good/module.py b/scripts/tooling/lib/known_good/module.py new file mode 100644 index 00000000000..c9c562ece05 --- /dev/null +++ b/scripts/tooling/lib/known_good/module.py @@ -0,0 +1,127 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""Module dataclass for score reference integration.""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass, field +from typing import Any, Dict, List +from urllib.parse import urlparse + + +@dataclass +class Metadata: + """Metadata configuration for a module.""" + + code_root_path: str = "//score/..." + extra_test_config: list[str] = field(default_factory=list) + exclude_test_targets: list[str] = field(default_factory=list) + langs: list[str] = field(default_factory=lambda: ["cpp", "rust"]) + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> Metadata: + return cls( + code_root_path=data.get("code_root_path", "//score/..."), + extra_test_config=data.get("extra_test_config", []), + exclude_test_targets=data.get("exclude_test_targets", []), + langs=data.get("langs", ["cpp", "rust"]), + ) + + def to_dict(self) -> Dict[str, Any]: + return { + "code_root_path": self.code_root_path, + "extra_test_config": self.extra_test_config, + "exclude_test_targets": self.exclude_test_targets, + "langs": self.langs, + } + + +@dataclass +class Module: + """A single known-good module entry.""" + + name: str + hash: str + repo: str + version: str | None = None + bazel_patches: list[str] | None = None + metadata: Metadata = field(default_factory=Metadata) + branch: str = "main" + pin_version: bool = False + + @classmethod + def from_dict(cls, name: str, data: Dict[str, Any]) -> Module: + repo = data.get("repo", "") + commit_hash = data.get("hash") or data.get("commit", "") + version = data.get("version") + + if commit_hash and version: + raise ValueError( + f"Module '{name}' has both 'hash' and 'version' set. " + "Use either 'hash' (git_override) or 'version' (single_version_override), not both." + ) + + bazel_patches = data.get("bazel_patches") or data.get("patches", []) + + metadata_data = data.get("metadata") + metadata = Metadata.from_dict(metadata_data) if metadata_data is not None else Metadata() + + return cls( + name=name, + hash=commit_hash, + repo=repo, + version=version, + bazel_patches=bazel_patches if bazel_patches else None, + metadata=metadata, + branch=data.get("branch", "main"), + pin_version=data.get("pin_version", False), + ) + + @classmethod + def parse_modules(cls, modules_dict: Dict[str, Any]) -> List[Module]: + modules = [] + for name, module_data in modules_dict.items(): + module = cls.from_dict(name, module_data) + if not module.repo and not module.version: + logging.warning("Skipping module %s with missing repo", name) + continue + modules.append(module) + return modules + + @property + def owner_repo(self) -> str: + """Return 'owner/repo' extracted from a GitHub HTTPS URL.""" + parsed = urlparse(self.repo) + if parsed.netloc != "github.com": + raise ValueError(f"Not a GitHub URL: {self.repo}") + path = parsed.path.lstrip("/").removesuffix(".git") + parts = path.split("/", 2) + if len(parts) != 2: + raise ValueError(f"Cannot parse owner/repo from: {self.repo}") + return f"{parts[0]}/{parts[1]}" + + def to_dict(self) -> Dict[str, Any]: + result: Dict[str, Any] = {"repo": self.repo} + if self.version: + result["version"] = self.version + else: + result["hash"] = self.hash + result["metadata"] = self.metadata.to_dict() + if self.bazel_patches: + result["bazel_patches"] = self.bazel_patches + if self.branch and self.branch != "main": + result["branch"] = self.branch + if self.pin_version: + result["pin_version"] = True + return result diff --git a/scripts/known_good/requirements.txt b/scripts/tooling/requirements.in similarity index 57% rename from scripts/known_good/requirements.txt rename to scripts/tooling/requirements.in index 1c871cd93f8..c6057dcbb85 100644 --- a/scripts/known_good/requirements.txt +++ b/scripts/tooling/requirements.in @@ -1 +1,2 @@ +jinja2 >= 3 PyGithub>=2.1.1 diff --git a/scripts/tooling/requirements.txt b/scripts/tooling/requirements.txt new file mode 100644 index 00000000000..b4eff3ee51c --- /dev/null +++ b/scripts/tooling/requirements.txt @@ -0,0 +1,426 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# bazel run //scripts/tooling:requirements.update +# +basedpyright==1.35.0 \ + --hash=sha256:2a7e0bd476623d48499e2b18ff6ed19dc28c51909cf9e1152ad355b5809049ad \ + --hash=sha256:4f4f84023df5a0cd4ee154916ba698596682ac98bacfa22c941ed6aaf07bba4e +certifi==2026.2.25 \ + --hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \ + --hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7 +cffi==2.0.0 \ + --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ + --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ + --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ + --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ + --hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \ + --hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \ + --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ + --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ + --hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \ + --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ + --hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \ + --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ + --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ + --hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \ + --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ + --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ + --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ + --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ + --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ + --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ + --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ + --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ + --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ + --hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \ + --hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \ + --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ + --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ + --hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \ + --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ + --hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \ + --hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \ + --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ + --hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \ + --hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \ + --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ + --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ + --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ + --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ + --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ + --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ + --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ + --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ + --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ + --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ + --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ + --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ + --hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \ + --hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \ + --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ + --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ + --hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \ + --hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \ + --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ + --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ + --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ + --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ + --hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \ + --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ + --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ + --hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \ + --hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \ + --hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \ + --hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \ + --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ + --hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \ + --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ + --hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \ + --hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \ + --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ + --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ + --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ + --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ + --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ + --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ + --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ + --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ + --hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \ + --hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \ + --hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \ + --hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \ + --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ + --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \ + --hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \ + --hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf +charset-normalizer==3.4.5 \ + --hash=sha256:014837af6fabf57121b6254fa8ade10dceabc3528b27b721a64bbc7b8b1d4eb4 \ + --hash=sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66 \ + --hash=sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54 \ + --hash=sha256:02a9d1b01c1e12c27883b0c9349e0bcd9ae92e727ff1a277207e1a262b1cbf05 \ + --hash=sha256:036c079aa08a6a592b82487f97c60b439428320ed1b2ea0b3912e99d30c77765 \ + --hash=sha256:039215608ac7b358c4da0191d10fc76868567fbf276d54c14721bdedeb6de064 \ + --hash=sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819 \ + --hash=sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e \ + --hash=sha256:0b362bcd27819f9c07cbf23db4e0e8cd4b44c5ecd900c2ff907b2b92274a7412 \ + --hash=sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc \ + --hash=sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e \ + --hash=sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281 \ + --hash=sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af \ + --hash=sha256:14498a429321de554b140013142abe7608f9d8ccc04d7baf2ad60498374aefa2 \ + --hash=sha256:149ec69866c3d6c2fb6f758dbc014ecb09f30b35a5ca90b6a8a2d4e54e18fdfe \ + --hash=sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8 \ + --hash=sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262 \ + --hash=sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac \ + --hash=sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85 \ + --hash=sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c \ + --hash=sha256:1f2da5cbb9becfcd607757a169e38fb82aa5fd86fae6653dea716e7b613fe2cf \ + --hash=sha256:259cd1ca995ad525f638e131dbcc2353a586564c038fc548a3fe450a91882139 \ + --hash=sha256:2820a98460c83663dd8ec015d9ddfd1e4879f12e06bb7d0500f044fb477d2770 \ + --hash=sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d \ + --hash=sha256:2b970382e4a36bed897c19f310f31d7d13489c11b4f468ddfba42d41cddfb918 \ + --hash=sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3 \ + --hash=sha256:30987f4a8ed169983f93e1be8ffeea5214a779e27ed0b059835c7afe96550ad7 \ + --hash=sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39 \ + --hash=sha256:340810d34ef83af92148e96e3e44cb2d3f910d2bf95e5618a5c467d9f102231d \ + --hash=sha256:3f64c6bf8f32f9133b668c7f7a7cbdbc453412bc95ecdbd157f3b1e377a92990 \ + --hash=sha256:4167a621a9a1a986c73777dbc15d4b5eac8ac5c10393374109a343d4013ec765 \ + --hash=sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1 \ + --hash=sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa \ + --hash=sha256:4b8551b6e6531e156db71193771c93bda78ffc4d1e6372517fe58ad3b91e4659 \ + --hash=sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d \ + --hash=sha256:50bcbca6603c06a1dcc7b056ed45c37715fb5d2768feb3bcd37d2313c587a5b9 \ + --hash=sha256:530beedcec9b6e027e7a4b6ce26eed36678aa39e17da85e6e03d7bd9e8e9d7c9 \ + --hash=sha256:568e3c34b58422075a1b49575a6abc616d9751b4d61b23f712e12ebb78fe47b2 \ + --hash=sha256:573ef5814c4b7c0d59a7710aa920eaaaef383bd71626aa420fba27b5cab92e8d \ + --hash=sha256:58ad8270cfa5d4bef1bc85bd387217e14ff154d6630e976c6f56f9a040757475 \ + --hash=sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c \ + --hash=sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81 \ + --hash=sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67 \ + --hash=sha256:5fea359734b140d0d6741189fea5478c6091b54ffc69d7ce119e0a05637d8c99 \ + --hash=sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5 \ + --hash=sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694 \ + --hash=sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf \ + --hash=sha256:65b3c403a5b6b8034b655e7385de4f72b7b244869a22b32d4030b99a60593eca \ + --hash=sha256:66dee73039277eb35380d1b82cccc69cc82b13a66f9f4a18da32d573acf02b7c \ + --hash=sha256:708c7acde173eedd4bfa4028484426ba689d2103b28588c513b9db2cd5ecde9c \ + --hash=sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636 \ + --hash=sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f \ + --hash=sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02 \ + --hash=sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497 \ + --hash=sha256:76a9d0de4d0eab387822e7b35d8f89367dd237c72e82ab42b9f7bf5e15ada00f \ + --hash=sha256:77be992288f720306ab4108fe5c74797de327f3248368dfc7e1a916d6ed9e5a2 \ + --hash=sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d \ + --hash=sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873 \ + --hash=sha256:82cc7c2ad42faec8b574351f8bc2a0c049043893853317bd9bb309f5aba6cb5a \ + --hash=sha256:8a28afb04baa55abf26df544e3e5c6534245d3daa5178bc4a8eeb48202060d0e \ + --hash=sha256:8b78d8a609a4b82c273257ee9d631ded7fac0d875bdcdccc109f3ee8328cfcb1 \ + --hash=sha256:8ce11cd4d62d11166f2b441e30ace226c19a3899a7cf0796f668fba49a9fb123 \ + --hash=sha256:8fff79bf5978c693c9b1a4d71e4a94fddfb5fe744eb062a318e15f4a2f63a550 \ + --hash=sha256:92263f7eca2f4af326cd20de8d16728d2602f7cfea02e790dcde9d83c365d7cc \ + --hash=sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36 \ + --hash=sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644 \ + --hash=sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4 \ + --hash=sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0 \ + --hash=sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e \ + --hash=sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f \ + --hash=sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4 \ + --hash=sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98 \ + --hash=sha256:aa2f963b4da26daf46231d9b9e0e2c9408a751f8f0d0f44d2de56d3caf51d294 \ + --hash=sha256:aa92ec1102eaff840ccd1021478af176a831f1bccb08e526ce844b7ddda85c22 \ + --hash=sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23 \ + --hash=sha256:ae8b03427410731469c4033934cf473426faff3e04b69d2dfb64a4281a3719f8 \ + --hash=sha256:afca7f78067dd27c2b848f1b234623d26b87529296c6c5652168cc1954f2f3b2 \ + --hash=sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362 \ + --hash=sha256:b3e71afc578b98512bfe7bdb822dd6bc57d4b0093b4b6e5487c1e96ad4ace242 \ + --hash=sha256:ba20bdf69bd127f66d0174d6f2a93e69045e0b4036dc1ca78e091bcc765830c4 \ + --hash=sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95 \ + --hash=sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d \ + --hash=sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94 \ + --hash=sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6 \ + --hash=sha256:c7e84e0c0005e3bdc1a9211cd4e62c78ba80bc37b2365ef4410cd2007a9047f2 \ + --hash=sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4 \ + --hash=sha256:cd2d0f0ec9aa977a27731a3209ebbcacebebaf41f902bd453a928bfd281cf7f8 \ + --hash=sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e \ + --hash=sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a \ + --hash=sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce \ + --hash=sha256:d29dd9c016f2078b43d0c357511e87eee5b05108f3dd603423cb389b89813969 \ + --hash=sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f \ + --hash=sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923 \ + --hash=sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6 \ + --hash=sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee \ + --hash=sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6 \ + --hash=sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467 \ + --hash=sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f \ + --hash=sha256:e22d1059b951e7ae7c20ef6b06afd10fb95e3c41bf3c4fbc874dba113321c193 \ + --hash=sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7 \ + --hash=sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9 \ + --hash=sha256:e545b51da9f9af5c67815ca0eb40676c0f016d0b0381c86f20451e35696c5f95 \ + --hash=sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763 \ + --hash=sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7 \ + --hash=sha256:ec56a2266f32bc06ed3c3e2a8f58417ce02f7e0356edc89786e52db13c593c98 \ + --hash=sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60 \ + --hash=sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade \ + --hash=sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c \ + --hash=sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2 \ + --hash=sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f \ + --hash=sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a \ + --hash=sha256:fc1c64934b8faf7584924143eb9db4770bbdb16659626e1a1a4d9efbcb68d947 \ + --hash=sha256:ff95a9283de8a457e6b12989de3f9f5193430f375d64297d323a615ea52cbdb3 +cryptography==46.0.5 \ + --hash=sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72 \ + --hash=sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235 \ + --hash=sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9 \ + --hash=sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356 \ + --hash=sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257 \ + --hash=sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad \ + --hash=sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4 \ + --hash=sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c \ + --hash=sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614 \ + --hash=sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed \ + --hash=sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31 \ + --hash=sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229 \ + --hash=sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0 \ + --hash=sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731 \ + --hash=sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b \ + --hash=sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4 \ + --hash=sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4 \ + --hash=sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263 \ + --hash=sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595 \ + --hash=sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1 \ + --hash=sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678 \ + --hash=sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48 \ + --hash=sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76 \ + --hash=sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0 \ + --hash=sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18 \ + --hash=sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d \ + --hash=sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d \ + --hash=sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1 \ + --hash=sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981 \ + --hash=sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7 \ + --hash=sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82 \ + --hash=sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2 \ + --hash=sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4 \ + --hash=sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663 \ + --hash=sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c \ + --hash=sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d \ + --hash=sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a \ + --hash=sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a \ + --hash=sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d \ + --hash=sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b \ + --hash=sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a \ + --hash=sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826 \ + --hash=sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee \ + --hash=sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9 \ + --hash=sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648 \ + --hash=sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da \ + --hash=sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2 \ + --hash=sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2 \ + --hash=sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87 +idna==3.11 \ + --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ + --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +iniconfig==2.3.0 \ + --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ + --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 +markupsafe==3.0.3 \ + --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ + --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ + --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ + --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ + --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ + --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ + --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ + --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ + --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ + --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ + --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ + --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ + --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ + --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ + --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ + --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ + --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ + --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ + --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ + --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ + --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ + --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ + --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ + --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ + --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ + --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ + --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ + --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ + --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ + --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ + --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ + --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ + --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ + --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ + --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ + --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ + --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ + --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ + --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ + --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ + --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ + --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ + --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ + --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ + --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ + --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ + --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ + --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ + --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ + --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 +nodejs-wheel-binaries==24.11.1 \ + --hash=sha256:0e14874c3579def458245cdbc3239e37610702b0aa0975c1dc55e2cb80e42102 \ + --hash=sha256:10197b1c9c04d79403501766f76508b0dac101ab34371ef8a46fcf51773497d0 \ + --hash=sha256:376b9ea1c4bc1207878975dfeb604f7aa5668c260c6154dcd2af9d42f7734116 \ + --hash=sha256:413dfffeadfb91edb4d8256545dea797c237bba9b3faefea973cde92d96bb922 \ + --hash=sha256:5ef598101b0fb1c2bf643abb76dfbf6f76f1686198ed17ae46009049ee83c546 \ + --hash=sha256:78bc5bb889313b565df8969bb7423849a9c7fc218bf735ff0ce176b56b3e96f0 \ + --hash=sha256:c2741525c9874b69b3e5a6d6c9179a6fe484ea0c3d5e7b7c01121c8e5d78b7e2 \ + --hash=sha256:c79a7e43869ccecab1cae8183778249cceb14ca2de67b5650b223385682c6239 \ + --hash=sha256:cde41d5e4705266688a8d8071debf4f8a6fcea264c61292782672ee75a6905f9 +packaging==25.0 \ + --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ + --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f +pluggy==1.6.0 \ + --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ + --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 +pygithub==2.8.1 \ + --hash=sha256:23a0a5bca93baef082e03411bf0ce27204c32be8bfa7abc92fe4a3e132936df0 \ + --hash=sha256:341b7c78521cb07324ff670afd1baa2bf5c286f8d9fd302c1798ba594a5400c9 +pygments==2.19.2 \ + --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ + --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b +pyjwt[crypto]==2.11.0 \ + --hash=sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623 \ + --hash=sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469 +pynacl==1.6.2 \ + --hash=sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c \ + --hash=sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574 \ + --hash=sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4 \ + --hash=sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130 \ + --hash=sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b \ + --hash=sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590 \ + --hash=sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444 \ + --hash=sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634 \ + --hash=sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87 \ + --hash=sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa \ + --hash=sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594 \ + --hash=sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0 \ + --hash=sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e \ + --hash=sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c \ + --hash=sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0 \ + --hash=sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c \ + --hash=sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577 \ + --hash=sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145 \ + --hash=sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88 \ + --hash=sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14 \ + --hash=sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6 \ + --hash=sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465 \ + --hash=sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0 \ + --hash=sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2 \ + --hash=sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9 +pytest==9.0.1 \ + --hash=sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8 \ + --hash=sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad +requests==2.32.5 \ + --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ + --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf +typing-extensions==4.15.0 \ + --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ + --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 +urllib3==2.6.3 \ + --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ + --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 diff --git a/scripts/tooling/tests/__init__.py b/scripts/tooling/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/scripts/tooling/tests/test_known_good.py b/scripts/tooling/tests/test_known_good.py new file mode 100644 index 00000000000..797a3992fc0 --- /dev/null +++ b/scripts/tooling/tests/test_known_good.py @@ -0,0 +1,232 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +import json +import pytest +from pathlib import Path + +from lib.known_good import KnownGood, Metadata, Module, load_known_good + + +KNOWN_GOOD_JSON = Path(__file__).parents[3] / "known_good.json" + +MINIMAL_JSON = { + "modules": { + "target_sw": { + "score_baselibs": { + "repo": "https://github.com/eclipse-score/baselibs.git", + "hash": "abc123", + } + } + }, + "timestamp": "2026-01-01T00:00:00+00:00Z", +} + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture +def minimal_json_file(tmp_path: Path) -> Path: + p = tmp_path / "known_good.json" + p.write_text(json.dumps(MINIMAL_JSON)) + return p + + +@pytest.fixture +def full_json_file(tmp_path: Path) -> Path: + """Copy the real known_good.json into a temp location.""" + content = KNOWN_GOOD_JSON.read_text(encoding="utf-8") + p = tmp_path / "known_good.json" + p.write_text(content) + return p + + +# --------------------------------------------------------------------------- +# load_known_good – happy path +# --------------------------------------------------------------------------- + + +class TestLoadKnownGood: + def test_returns_known_good_instance(self, minimal_json_file: Path): + result = load_known_good(minimal_json_file) + assert isinstance(result, KnownGood) + + def test_timestamp_is_string(self, minimal_json_file: Path): + result = load_known_good(minimal_json_file) + assert isinstance(result.timestamp, str) + assert result.timestamp == "2026-01-01T00:00:00+00:00Z" + + def test_modules_is_dict_of_dicts(self, minimal_json_file: Path): + result = load_known_good(minimal_json_file) + assert isinstance(result.modules, dict) + for group in result.modules.values(): + assert isinstance(group, dict) + + def test_module_values_are_module_instances(self, minimal_json_file: Path): + result = load_known_good(minimal_json_file) + module = result.modules["target_sw"]["score_baselibs"] + assert isinstance(module, Module) + + def test_module_fields_are_typed(self, minimal_json_file: Path): + m = load_known_good(minimal_json_file).modules["target_sw"]["score_baselibs"] + assert isinstance(m.name, str) + assert isinstance(m.hash, str) + assert isinstance(m.repo, str) + assert m.version is None + assert m.bazel_patches is None + assert isinstance(m.metadata, Metadata) + assert isinstance(m.branch, str) + assert isinstance(m.pin_version, bool) + + def test_module_field_values(self, minimal_json_file: Path): + m = load_known_good(minimal_json_file).modules["target_sw"]["score_baselibs"] + assert m.name == "score_baselibs" + assert m.hash == "abc123" + assert m.repo == "https://github.com/eclipse-score/baselibs.git" + assert m.branch == "main" + assert m.pin_version is False + + +# --------------------------------------------------------------------------- +# load_known_good – real file +# --------------------------------------------------------------------------- + + +@pytest.mark.skipif(not KNOWN_GOOD_JSON.exists(), reason="known_good.json not found") +class TestLoadKnownGoodRealFile: + def test_loads_without_error(self, full_json_file: Path): + load_known_good(full_json_file) + + def test_groups_present(self, full_json_file: Path): + kg = load_known_good(full_json_file) + assert "target_sw" in kg.modules + assert "tooling" in kg.modules + + def test_score_baselibs_fields(self, full_json_file: Path): + m = load_known_good(full_json_file).modules["target_sw"]["score_baselibs"] + assert m.repo == "https://github.com/eclipse-score/baselibs.git" + assert len(m.hash) == 40 # SHA-1 + assert m.bazel_patches is not None + assert isinstance(m.metadata.extra_test_config, list) + assert isinstance(m.metadata.exclude_test_targets, list) + + def test_tooling_module_no_metadata_defaults(self, full_json_file: Path): + m = load_known_good(full_json_file).modules["tooling"]["score_crates"] + assert isinstance(m.metadata, Metadata) + + def test_all_modules_have_repo(self, full_json_file: Path): + kg = load_known_good(full_json_file) + for group in kg.modules.values(): + for m in group.values(): + assert m.repo, f"Module {m.name} is missing a repo" + + def test_owner_repo_property(self, full_json_file: Path): + m = load_known_good(full_json_file).modules["target_sw"]["score_baselibs"] + assert m.owner_repo == "eclipse-score/baselibs" + + +# --------------------------------------------------------------------------- +# Metadata defaults +# --------------------------------------------------------------------------- + + +class TestMetadataDefaults: + def test_defaults_when_metadata_key_absent(self, tmp_path: Path): + data = { + "modules": {"g": {"mod": {"repo": "https://github.com/a/b.git", "hash": "deadbeef"}}}, + "timestamp": "", + } + p = tmp_path / "kg.json" + p.write_text(json.dumps(data)) + m = load_known_good(p).modules["g"]["mod"] + assert m.metadata.code_root_path == "//score/..." + assert m.metadata.extra_test_config == [] + assert m.metadata.exclude_test_targets == [] + assert m.metadata.langs == ["cpp", "rust"] + + def test_metadata_fields_typed(self, tmp_path: Path): + data = { + "modules": { + "g": { + "mod": { + "repo": "https://github.com/a/b.git", + "hash": "deadbeef", + "metadata": { + "code_root_path": "//src/...", + "extra_test_config": ["//flag:val"], + "exclude_test_targets": ["//some:test"], + "langs": ["rust"], + }, + } + } + }, + "timestamp": "", + } + p = tmp_path / "kg.json" + p.write_text(json.dumps(data)) + meta = load_known_good(p).modules["g"]["mod"].metadata + assert isinstance(meta.code_root_path, str) + assert isinstance(meta.extra_test_config, list) + assert isinstance(meta.exclude_test_targets, list) + assert isinstance(meta.langs, list) + assert meta.langs == ["rust"] + + +# --------------------------------------------------------------------------- +# Error handling +# --------------------------------------------------------------------------- + + +class TestLoadKnownGoodErrors: + def test_file_not_found(self, tmp_path: Path): + with pytest.raises(FileNotFoundError): + load_known_good(tmp_path / "nonexistent.json") + + def test_invalid_json_raises_value_error(self, tmp_path: Path): + p = tmp_path / "bad.json" + p.write_text("{invalid json,}") + with pytest.raises(ValueError, match="Invalid JSON"): + load_known_good(p) + + def test_missing_modules_key_raises_value_error(self, tmp_path: Path): + p = tmp_path / "bad.json" + p.write_text(json.dumps({"timestamp": "2026-01-01"})) + with pytest.raises(ValueError, match="modules"): + load_known_good(p) + + def test_hash_and_version_both_set_raises(self, tmp_path: Path): + data = { + "modules": { + "g": { + "mod": { + "repo": "https://github.com/a/b.git", + "hash": "abc", + "version": "1.0.0", + } + } + }, + "timestamp": "", + } + p = tmp_path / "kg.json" + p.write_text(json.dumps(data)) + with pytest.raises(ValueError, match="both 'hash' and 'version'"): + load_known_good(p) + + def test_bazel_patches_are_list(self, minimal_json_file: Path): + data = json.loads(minimal_json_file.read_text()) + data["modules"]["target_sw"]["score_baselibs"]["bazel_patches"] = ["//patch:foo.patch"] + minimal_json_file.write_text(json.dumps(data)) + m = load_known_good(minimal_json_file).modules["target_sw"]["score_baselibs"] + assert m.bazel_patches == ["//patch:foo.patch"] diff --git a/scripts/tooling/tests/test_report.py b/scripts/tooling/tests/test_report.py new file mode 100644 index 00000000000..5b9a0201778 --- /dev/null +++ b/scripts/tooling/tests/test_report.py @@ -0,0 +1,309 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +import json +import re +from pathlib import Path + +import pytest + +from cli.misc.html_report import TEMPLATE_DIR, generate_report, write_report +from lib.known_good import KnownGood, load_known_good +from lib.known_good.module import Metadata, Module + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +KNOWN_GOOD_JSON = Path(__file__).parents[3] / "known_good.json" + + +def _make_known_good(**overrides) -> KnownGood: + """Build a minimal KnownGood with one module.""" + module_data = { + "repo": "https://github.com/eclipse-score/baselibs.git", + "hash": "abc123def456abc123def456abc123def456abc123", + **overrides, + } + module = Module.from_dict("score_baselibs", module_data) + return KnownGood( + modules={"target_sw": {"score_baselibs": module}}, + timestamp="2026-01-01T00:00:00+00:00Z", + ) + + +@pytest.fixture +def minimal_known_good() -> KnownGood: + return _make_known_good() + + +@pytest.fixture +def multi_group_known_good() -> KnownGood: + m1 = Module.from_dict( + "score_baselibs", + { + "repo": "https://github.com/eclipse-score/baselibs.git", + "hash": "aaa", + }, + ) + m2 = Module.from_dict( + "score_crates", + { + "repo": "https://github.com/eclipse-score/score-crates.git", + "hash": "bbb", + }, + ) + return KnownGood( + modules={ + "target_sw": {"score_baselibs": m1}, + "tooling": {"score_crates": m2}, + }, + timestamp="2026-02-01T00:00:00+00:00Z", + ) + + +@pytest.fixture +def real_known_good() -> KnownGood: + return load_known_good(KNOWN_GOOD_JSON) + + +# --------------------------------------------------------------------------- +# generate_report – return type and structure +# --------------------------------------------------------------------------- + + +class TestGenerateReportStructure: + def test_returns_string(self, minimal_known_good): + assert isinstance(generate_report(minimal_known_good, TEMPLATE_DIR), str) + + def test_is_html(self, minimal_known_good): + html = generate_report(minimal_known_good, TEMPLATE_DIR) + assert html.strip().startswith("") + assert "" in html + + def test_contains_title(self, minimal_known_good): + assert "Known Good Status" in generate_report(minimal_known_good, TEMPLATE_DIR) + + def test_contains_timestamp(self, minimal_known_good): + html = generate_report(minimal_known_good, TEMPLATE_DIR) + assert "2026-01-01T00:00:00+00:00Z" in html + + def test_contains_module_json(self, minimal_known_good): + html = generate_report(minimal_known_good, TEMPLATE_DIR) + assert "score_baselibs" in html + assert "eclipse-score/baselibs" in html + + def test_embedded_json_is_valid(self, minimal_known_good): + html = generate_report(minimal_known_good, TEMPLATE_DIR) + # Extract the JS array assigned to MODULES + match = re.search(r"const MODULES\s*=\s*(\[.*?\]);", html, re.DOTALL) + assert match, "MODULES array not found in HTML" + data = json.loads(match.group(1)) + assert isinstance(data, list) + assert len(data) == 1 + + def test_module_entry_fields(self, minimal_known_good): + html = generate_report(minimal_known_good, TEMPLATE_DIR) + match = re.search(r"const MODULES\s*=\s*(\[.*?\]);", html, re.DOTALL) + entry = json.loads(match.group(1))[0] + assert entry["name"] == "score_baselibs" + assert entry["group"] == "target_sw" + assert entry["repo"] == "https://github.com/eclipse-score/baselibs.git" + assert entry["owner_repo"] == "eclipse-score/baselibs" + assert entry["hash"] == "abc123def456abc123def456abc123def456abc123" + assert entry["branch"] == "main" + assert entry["version"] is None + + +# --------------------------------------------------------------------------- +# generate_report – GitHub API integration in HTML +# --------------------------------------------------------------------------- + + +class TestGenerateReportGitHubIntegration: + def test_github_api_url_pattern_present(self, minimal_known_good): + html = generate_report(minimal_known_good, TEMPLATE_DIR) + assert "api.github.com" in html + + def test_compare_endpoint_referenced(self, minimal_known_good): + html = generate_report(minimal_known_good, TEMPLATE_DIR) + assert "/compare/" in html + + def test_commits_endpoint_referenced(self, minimal_known_good): + html = generate_report(minimal_known_good, TEMPLATE_DIR) + assert "/commits/" in html + + def test_ahead_by_field_referenced(self, minimal_known_good): + html = generate_report(minimal_known_good, TEMPLATE_DIR) + assert "ahead_by" in html + + def test_token_stored_in_localstorage(self, minimal_known_good): + html = generate_report(minimal_known_good, TEMPLATE_DIR) + assert "gh_token" in html + assert "localStorage" in html + + def test_cache_ttl_present(self, minimal_known_good): + html = generate_report(minimal_known_good, TEMPLATE_DIR) + assert "CACHE_TTL_MS" in html + + def test_cache_functions_present(self, minimal_known_good): + html = generate_report(minimal_known_good, TEMPLATE_DIR) + assert "loadFromCache" in html + assert "saveToCache" in html + + def test_no_unauthenticated_api_calls(self, minimal_known_good): + html = generate_report(minimal_known_good, TEMPLATE_DIR) + # Token is always attached — no conditional empty headers + assert "headers: {}" not in html + assert "authHeaders()" in html + + def test_no_token_shows_requires_pat(self, minimal_known_good): + html = generate_report(minimal_known_good, TEMPLATE_DIR) + assert "requires PAT" in html + + def test_token_input_always_present(self, minimal_known_good): + html = generate_report(minimal_known_good, TEMPLATE_DIR) + assert "gh-token" in html + + def test_no_cooldown_logic(self, minimal_known_good): + html = generate_report(minimal_known_good, TEMPLATE_DIR) + assert "remainingCooldownMs" not in html + assert "startCooldownUI" not in html + + def test_no_oauth_code(self, minimal_known_good): + html = generate_report(minimal_known_good, TEMPLATE_DIR) + assert "CLIENT_ID" not in html + assert "startOAuth" not in html + assert "sha256base64url" not in html + assert "pkce_state" not in html + + +# --------------------------------------------------------------------------- +# generate_report – multi-group +# --------------------------------------------------------------------------- + + +class TestGenerateReportMultiGroup: + def test_all_modules_in_json(self, multi_group_known_good): + html = generate_report(multi_group_known_good, TEMPLATE_DIR) + match = re.search(r"const MODULES\s*=\s*(\[.*?\]);", html, re.DOTALL) + data = json.loads(match.group(1)) + names = {e["name"] for e in data} + assert "score_baselibs" in names + assert "score_crates" in names + + def test_both_groups_present(self, multi_group_known_good): + html = generate_report(multi_group_known_good, TEMPLATE_DIR) + match = re.search(r"const MODULES\s*=\s*(\[.*?\]);", html, re.DOTALL) + data = json.loads(match.group(1)) + groups = {e["group"] for e in data} + assert groups == {"target_sw", "tooling"} + + def test_filter_buttons_in_html(self, multi_group_known_good): + html = generate_report(multi_group_known_good, TEMPLATE_DIR) + assert "target_sw" in html + assert "tooling" in html + + +# --------------------------------------------------------------------------- +# generate_report – edge cases +# --------------------------------------------------------------------------- + + +class TestGenerateReportEdgeCases: + def test_module_with_no_metadata(self): + module = Module.from_dict( + "score_crates", + { + "repo": "https://github.com/eclipse-score/score-crates.git", + "hash": "deadbeef", + }, + ) + kg = KnownGood(modules={"tooling": {"score_crates": module}}, timestamp="") + html = generate_report(kg, TEMPLATE_DIR) + match = re.search(r"const MODULES\s*=\s*(\[.*?\]);", html, re.DOTALL) + entry = json.loads(match.group(1))[0] + assert entry["owner_repo"] == "eclipse-score/score-crates" + + def test_non_github_repo_owner_repo_is_none(self): + module = Module.from_dict( + "custom_mod", + { + "repo": "https://gitlab.com/some/repo.git", + "hash": "abc", + }, + ) + kg = KnownGood(modules={"g": {"custom_mod": module}}, timestamp="") + html = generate_report(kg, TEMPLATE_DIR) + match = re.search(r"const MODULES\s*=\s*(\[.*?\]);", html, re.DOTALL) + entry = json.loads(match.group(1))[0] + assert entry["owner_repo"] is None + + def test_empty_modules(self): + kg = KnownGood(modules={}, timestamp="2026-01-01") + html = generate_report(kg, TEMPLATE_DIR) + match = re.search(r"const MODULES\s*=\s*(\[.*?\]);", html, re.DOTALL) + assert json.loads(match.group(1)) == [] + + +# --------------------------------------------------------------------------- +# write_report +# --------------------------------------------------------------------------- + + +class TestWriteReport: + def test_creates_file(self, tmp_path, minimal_known_good): + out = tmp_path / "report.html" + write_report(minimal_known_good, out, TEMPLATE_DIR) + assert out.exists() + + def test_file_content_matches_generate(self, tmp_path, minimal_known_good): + out = tmp_path / "report.html" + write_report(minimal_known_good, out, TEMPLATE_DIR) + assert out.read_text(encoding="utf-8") == generate_report(minimal_known_good, TEMPLATE_DIR) + + def test_creates_parent_dirs_via_path(self, tmp_path, minimal_known_good): + out = tmp_path / "sub" / "report.html" + out.parent.mkdir(parents=True) + write_report(minimal_known_good, out, TEMPLATE_DIR) + assert out.exists() + + +# --------------------------------------------------------------------------- +# Integration: real known_good.json +# --------------------------------------------------------------------------- + + +@pytest.mark.skipif(not KNOWN_GOOD_JSON.exists(), reason="known_good.json not found") +class TestReportFromRealFile: + def test_generates_without_error(self, real_known_good): + html = generate_report(real_known_good, TEMPLATE_DIR) + assert len(html) > 1000 + + def test_all_modules_embedded(self, real_known_good): + html = generate_report(real_known_good, TEMPLATE_DIR) + match = re.search(r"const MODULES\s*=\s*(\[.*?\]);", html, re.DOTALL) + data = json.loads(match.group(1)) + total = sum(len(g) for g in real_known_good.modules.values()) + assert len(data) == total + + def test_all_entries_have_required_fields(self, real_known_good): + html = generate_report(real_known_good, TEMPLATE_DIR) + match = re.search(r"const MODULES\s*=\s*(\[.*?\]);", html, re.DOTALL) + for entry in json.loads(match.group(1)): + assert "name" in entry + assert "group" in entry + assert "repo" in entry + assert "hash" in entry + assert "branch" in entry + assert "owner_repo" in entry