This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
The following commit(s) were added to refs/heads/sbp by this push:
new f50e425 Add unit tests for the tree comparison check
f50e425 is described below
commit f50e425ab792af985cee06e18f1275c47bb25f81
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Feb 5 14:39:25 2026 +0000
Add unit tests for the tree comparison check
---
tests/unit/test_checks_compare.py | 235 ++++++++++++++++++++++++++++++++++++++
1 file changed, 235 insertions(+)
diff --git a/tests/unit/test_checks_compare.py
b/tests/unit/test_checks_compare.py
new file mode 100644
index 0000000..4bd6f5c
--- /dev/null
+++ b/tests/unit/test_checks_compare.py
@@ -0,0 +1,235 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pathlib
+
+import aiofiles.os
+import pytest
+
+import atr.sbom.models.github
+import atr.tasks.checks
+import atr.tasks.checks.compare
+
+
+class CheckoutRecorder:
+ def __init__(self, return_value: str | None = None) -> None:
+ self.checkout_dir: pathlib.Path | None = None
+ self.return_value = return_value
+
+ async def __call__(
+ self,
+ payload: atr.sbom.models.github.TrustedPublisherPayload,
+ checkout_dir: pathlib.Path,
+ ) -> str | None:
+ self.checkout_dir = checkout_dir
+ assert await aiofiles.os.path.exists(checkout_dir)
+ if self.return_value is not None:
+ return self.return_value
+ return str(checkout_dir)
+
+
+class CloneRecorder:
+ def __init__(self) -> None:
+ self.calls: list[tuple[str, str, str | None, pathlib.Path]] = []
+
+ def __call__(self, repo_url: str, sha: str, branch: str | None,
checkout_dir: pathlib.Path) -> None:
+ self.calls.append((repo_url, sha, branch, checkout_dir))
+
+
+class PayloadLoader:
+ def __init__(self, payload: atr.sbom.models.github.TrustedPublisherPayload
| None) -> None:
+ self.payload = payload
+
+ async def __call__(
+ self, project_name: str, version_name: str, revision_number: str
+ ) -> atr.sbom.models.github.TrustedPublisherPayload | None:
+ return self.payload
+
+
+class RaiseAsync:
+ def __init__(self, message: str) -> None:
+ self.message = message
+
+ async def __call__(self, *args: object, **kwargs: object) -> None:
+ raise AssertionError(self.message)
+
+
+class RaiseSync:
+ def __init__(self, message: str) -> None:
+ self.message = message
+
+ def __call__(self, *args: object, **kwargs: object) -> None:
+ raise AssertionError(self.message)
+
+
+class RecorderFactory:
+ def __init__(self, recorder: atr.tasks.checks.Recorder) -> None:
+ self._recorder = recorder
+
+ async def __call__(self) -> atr.tasks.checks.Recorder:
+ return self._recorder
+
+
+class RecorderStub(atr.tasks.checks.Recorder):
+ def __init__(self, is_source: bool) -> None:
+ super().__init__(
+ checker="compare.source_trees",
+ project_name="project",
+ version_name="version",
+ revision_number="00001",
+ primary_rel_path="artifact.tar.gz",
+ member_rel_path=None,
+ afresh=False,
+ )
+ self._is_source = is_source
+
+ async def primary_path_is_source(self) -> bool:
+ return self._is_source
+
+
+class ReturnValue:
+ def __init__(self, value: pathlib.Path) -> None:
+ self.value = value
+
+ def __call__(self) -> pathlib.Path:
+ return self.value
+
+
[email protected]
+async def test_checkout_github_source_uses_provided_dir(
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
+) -> None:
+ payload = _make_payload()
+ checkout_dir = tmp_path / "checkout"
+ clone_recorder = CloneRecorder()
+
+ monkeypatch.setattr(atr.tasks.checks.compare, "_clone_repo",
clone_recorder)
+
+ result = await atr.tasks.checks.compare._checkout_github_source(payload,
checkout_dir)
+
+ assert result == str(checkout_dir)
+ assert len(clone_recorder.calls) == 1
+ repo_url, sha, branch, called_dir = clone_recorder.calls[0]
+ assert repo_url == "https://github.com/apache/test.git"
+ assert sha == "0000000000000000000000000000000000000000"
+ assert branch == "main"
+ assert called_dir == checkout_dir
+
+
[email protected]
+async def test_source_trees_creates_temp_workspace_and_cleans_up(
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
+) -> None:
+ recorder = RecorderStub(True)
+ args = _make_args(recorder)
+ payload = _make_payload()
+ checkout = CheckoutRecorder()
+ tmp_root = tmp_path / "temporary-root"
+
+ monkeypatch.setattr(atr.tasks.checks.compare, "_load_tp_payload",
PayloadLoader(payload))
+ monkeypatch.setattr(atr.tasks.checks.compare, "_checkout_github_source",
checkout)
+ monkeypatch.setattr(atr.tasks.checks.compare.util, "get_tmp_dir",
ReturnValue(tmp_root))
+
+ await atr.tasks.checks.compare.source_trees(args)
+
+ assert checkout.checkout_dir is not None
+ checkout_dir = checkout.checkout_dir
+ assert checkout_dir.name == "github"
+ assert checkout_dir.parent.parent == tmp_root
+ assert checkout_dir.parent.name.startswith("trees-")
+ assert await aiofiles.os.path.exists(tmp_root)
+ assert not await aiofiles.os.path.exists(checkout_dir.parent)
+
+
[email protected]
+async def test_source_trees_payload_none_skips_temp_workspace(monkeypatch:
pytest.MonkeyPatch) -> None:
+ recorder = RecorderStub(True)
+ args = _make_args(recorder)
+
+ monkeypatch.setattr(atr.tasks.checks.compare, "_load_tp_payload",
PayloadLoader(None))
+ monkeypatch.setattr(
+ atr.tasks.checks.compare,
+ "_checkout_github_source",
+ RaiseAsync("_checkout_github_source should not be called"),
+ )
+ monkeypatch.setattr(atr.tasks.checks.compare.util, "get_tmp_dir",
RaiseSync("get_tmp_dir should not be called"))
+
+ await atr.tasks.checks.compare.source_trees(args)
+
+
[email protected]
+async def test_source_trees_skips_when_not_source(monkeypatch:
pytest.MonkeyPatch) -> None:
+ recorder = RecorderStub(False)
+ args = _make_args(recorder)
+
+ monkeypatch.setattr(
+ atr.tasks.checks.compare, "_load_tp_payload",
RaiseAsync("_load_tp_payload should not be called")
+ )
+
+ await atr.tasks.checks.compare.source_trees(args)
+
+
+def _make_args(recorder: atr.tasks.checks.Recorder) ->
atr.tasks.checks.FunctionArguments:
+ return atr.tasks.checks.FunctionArguments(
+ recorder=RecorderFactory(recorder),
+ asf_uid="test",
+ project_name="project",
+ version_name="version",
+ revision_number="00001",
+ primary_rel_path="artifact.tar.gz",
+ extra_args={},
+ )
+
+
+def _make_payload(
+ repository: str = "apache/test",
+ ref: str = "refs/heads/main",
+ sha: str = "0000000000000000000000000000000000000000",
+) -> atr.sbom.models.github.TrustedPublisherPayload:
+ payload = {
+ "actor": "actor",
+ "actor_id": "1",
+ "aud": "audience",
+ "base_ref": "",
+ "check_run_id": "123",
+ "enterprise": "",
+ "enterprise_id": "",
+ "event_name": "push",
+ "exp": 1,
+ "head_ref": "",
+ "iat": 1,
+ "iss": "issuer",
+ "job_workflow_ref": "refs/heads/main",
+ "job_workflow_sha": "ffffffffffffffffffffffffffffffffffffffff",
+ "jti": "token-id",
+ "nbf": None,
+ "ref": ref,
+ "ref_protected": "false",
+ "ref_type": "branch",
+ "repository": repository,
+ "repository_owner": "apache",
+ "repository_visibility": "public",
+ "run_attempt": "1",
+ "run_number": "1",
+ "runner_environment": "github-hosted",
+ "sha": sha,
+ "sub": "repo:apache/test:ref:refs/heads/main",
+ "workflow": "build",
+ "workflow_ref": "refs/heads/main",
+ "workflow_sha": "ffffffffffffffffffffffffffffffffffffffff",
+ }
+ return
atr.sbom.models.github.TrustedPublisherPayload.model_validate(payload)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]