You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by sb...@apache.org on 2026/05/12 13:34:49 UTC

(tooling-trusted-releases) 01/01: Summarise check results in an extensible counter

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

commit 75a668f411cd6b2809b51c3f480cc9017c9f5be9
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Tue May 12 14:34:23 2026 +0100

    Summarise check results in an extensible counter
---
 atr/shared/web.py                | 28 +++++++++----
 atr/storage/readers/releases.py  | 49 ++++++++--------------
 atr/storage/types.py             | 10 ++---
 tests/unit/test_checker_stats.py | 88 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 128 insertions(+), 47 deletions(-)

diff --git a/atr/shared/web.py b/atr/shared/web.py
index 809b227c..dc544d86 100644
--- a/atr/shared/web.py
+++ b/atr/shared/web.py
@@ -15,6 +15,8 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import collections
+
 import htpy
 
 import atr.htm as htm
@@ -68,11 +70,12 @@ def _render_checker_entry(
     details = htm.Block(htm.details, classes=f".mb-0.p-2{stripe_class}")
 
     checker = stat.checker if (stat is not None) else release_errors[0].checker
-    release_failure_count = sum(1 for e in release_errors if e.status != sql.CheckResultStatus.BLOCKER)
-    release_blocker_count = sum(1 for e in release_errors if e.status == sql.CheckResultStatus.BLOCKER)
-    warning_count = stat.warning_count if (stat is not None) else 0
-    failure_count = (stat.failure_count if (stat is not None) else 0) + release_failure_count
-    blocker_count = (stat.blocker_count if (stat is not None) else 0) + release_blocker_count
+    release_failure_count = sum(1 for e in release_errors if (e.status != sql.CheckResultStatus.BLOCKER))
+    release_blocker_count = sum(1 for e in release_errors if (e.status == sql.CheckResultStatus.BLOCKER))
+    counts: collections.Counter[sql.CheckResultStatus] = stat.counts if (stat is not None) else collections.Counter()
+    warning_count = counts[sql.CheckResultStatus.WARNING]
+    failure_count = counts[sql.CheckResultStatus.FAILURE] + release_failure_count
+    blocker_count = counts[sql.CheckResultStatus.BLOCKER] + release_blocker_count
 
     summary_content: list[htm.Element | str] = []
     if warning_count > 0:
@@ -97,12 +100,19 @@ def _render_checker_entry(
         ]
 
     if stat is not None:
-        all_files = set(stat.failure_files.keys()) | set(stat.warning_files.keys()) | set(stat.blocker_files.keys())
+        all_files = (
+            set(stat.files.get(sql.CheckResultStatus.FAILURE, {}))
+            | set(stat.files.get(sql.CheckResultStatus.WARNING, {}))
+            | set(stat.files.get(sql.CheckResultStatus.BLOCKER, {}))
+        )
+        failure_files = stat.files.get(sql.CheckResultStatus.FAILURE, {})
+        blocker_files = stat.files.get(sql.CheckResultStatus.BLOCKER, {})
+        warning_files = stat.files.get(sql.CheckResultStatus.WARNING, {})
         for file_path in sorted(all_files):
             report_url = f"/report/{project_key!s}/{version_key!s}/{file_path}"
-            file_error_count = stat.failure_files.get(file_path, 0)
-            file_blocker_count = stat.blocker_files.get(file_path, 0)
-            file_warning_count = stat.warning_files.get(file_path, 0)
+            file_error_count = failure_files.get(file_path, 0)
+            file_blocker_count = blocker_files.get(file_path, 0)
+            file_warning_count = warning_files.get(file_path, 0)
 
             file_content: list[htm.Element | str] = []
             if file_error_count > 0:
diff --git a/atr/storage/readers/releases.py b/atr/storage/readers/releases.py
index a50dfe44..a59d3358 100644
--- a/atr/storage/readers/releases.py
+++ b/atr/storage/readers/releases.py
@@ -18,6 +18,7 @@
 # Removing this will cause circular imports
 from __future__ import annotations
 
+import collections
 import dataclasses
 
 import atr.classify as classify
@@ -32,13 +33,8 @@ import atr.storage.types as types
 
 @dataclasses.dataclass
 class CheckerAccumulator:
-    success: int = 0
-    warning: int = 0
-    failure: int = 0
-    blocker: int = 0
-    warning_files: dict[str, int] = dataclasses.field(default_factory=dict)
-    failure_files: dict[str, int] = dataclasses.field(default_factory=dict)
-    blocker_files: dict[str, int] = dataclasses.field(default_factory=dict)
+    counts: collections.Counter[sql.CheckResultStatus] = dataclasses.field(default_factory=collections.Counter)
+    files: dict[sql.CheckResultStatus, dict[str, int]] = dataclasses.field(default_factory=dict)
 
 
 class GeneralPublic:
@@ -86,47 +82,38 @@ class GeneralPublic:
         results: dict[safe.RelPath, list[sql.CheckResult]],
         paths_set: set[safe.RelPath],
         checker_data: dict[str, CheckerAccumulator],
-        kind: str,
     ) -> None:
         for path, results_list in results.items():
             if path not in paths_set:
                 continue
             for result in results_list:
                 acc = checker_data.setdefault(result.checker, CheckerAccumulator())
-                path_str = str(path)
-                if kind == "success":
-                    acc.success += 1
-                elif kind == "warning":
-                    acc.warning += 1
-                    acc.warning_files[path_str] = acc.warning_files.get(path_str, 0) + 1
-                elif result.status == sql.CheckResultStatus.BLOCKER:
-                    acc.blocker += 1
-                    acc.blocker_files[path_str] = acc.blocker_files.get(path_str, 0) + 1
-                else:
-                    acc.failure += 1
-                    acc.failure_files[path_str] = acc.failure_files.get(path_str, 0) + 1
+                status = result.status
+                acc.counts[status] += 1
+                if status != sql.CheckResultStatus.SUCCESS:
+                    path_str = str(path)
+                    files_for_status = acc.files.setdefault(status, {})
+                    files_for_status[path_str] = files_for_status.get(path_str, 0) + 1
 
     def __compute_checker_stats(self, info: types.PathInfo, paths: list[safe.RelPath]) -> None:
         paths_set = set(paths)
         checker_data: dict[str, CheckerAccumulator] = {}
 
-        self.__accumulate_results(info.successes, paths_set, checker_data, "success")
-        self.__accumulate_results(info.warnings, paths_set, checker_data, "warning")
-        self.__accumulate_results(info.errors, paths_set, checker_data, "failure")
+        self.__accumulate_results(info.successes, paths_set, checker_data)
+        self.__accumulate_results(info.warnings, paths_set, checker_data)
+        self.__accumulate_results(info.errors, paths_set, checker_data)
 
         for checker, acc in sorted(checker_data.items()):
-            if (acc.warning == 0) and (acc.failure == 0) and (acc.blocker == 0):
+            non_success_total = sum(
+                count for status, count in acc.counts.items() if (status != sql.CheckResultStatus.SUCCESS)
+            )
+            if non_success_total == 0:
                 continue
             info.checker_stats.append(
                 types.CheckerStats(
                     checker=checker,
-                    success_count=acc.success,
-                    warning_count=acc.warning,
-                    failure_count=acc.failure,
-                    blocker_count=acc.blocker,
-                    warning_files=acc.warning_files,
-                    failure_files=acc.failure_files,
-                    blocker_files=acc.blocker_files,
+                    counts=acc.counts,
+                    files=acc.files,
                 )
             )
 
diff --git a/atr/storage/types.py b/atr/storage/types.py
index 3ee474d3..1c2af97a 100644
--- a/atr/storage/types.py
+++ b/atr/storage/types.py
@@ -15,6 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import collections
 import dataclasses
 import datetime
 import enum
@@ -30,13 +31,8 @@ import atr.storage.outcome as outcome
 @dataclasses.dataclass
 class CheckerStats:
     checker: str
-    success_count: int
-    warning_count: int
-    failure_count: int
-    blocker_count: int
-    warning_files: dict[str, int]
-    failure_files: dict[str, int]
-    blocker_files: dict[str, int]
+    counts: collections.Counter[sql.CheckResultStatus]
+    files: dict[sql.CheckResultStatus, dict[str, int]]
 
 
 @dataclasses.dataclass
diff --git a/tests/unit/test_checker_stats.py b/tests/unit/test_checker_stats.py
new file mode 100644
index 00000000..93064e73
--- /dev/null
+++ b/tests/unit/test_checker_stats.py
@@ -0,0 +1,88 @@
+# 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 collections
+import datetime
+import unittest.mock as mock
+
+import atr.models.safe as safe
+import atr.models.sql as sql
+import atr.storage.readers.releases as releases
+import atr.storage.types as types
+
+
+def test_checker_stats_counts_and_files_by_status():
+    path = safe.RelPath("apache-test-1.0-source.tar.gz")
+    success = _make_check_result(sql.CheckResultStatus.SUCCESS, "Success", primary_rel_path=str(path))
+    warning = _make_check_result(sql.CheckResultStatus.WARNING, "Warning", primary_rel_path=str(path))
+    failure = _make_check_result(sql.CheckResultStatus.FAILURE, "Failure", primary_rel_path=str(path))
+    blocker = _make_check_result(sql.CheckResultStatus.BLOCKER, "Blocker", primary_rel_path=str(path))
+    info = types.PathInfo(
+        successes={path: [success]},
+        warnings={path: [warning]},
+        errors={path: [failure, blocker]},
+    )
+    reader = _make_reader()
+    compute_checker_stats = getattr(reader, "_GeneralPublic__compute_checker_stats")
+
+    compute_checker_stats(info, [path])
+
+    assert len(info.checker_stats) == 1
+    stat = info.checker_stats[0]
+    assert stat.checker == "atr.tasks.checks.paths.check_errors"
+    assert stat.counts == collections.Counter(
+        {
+            sql.CheckResultStatus.SUCCESS: 1,
+            sql.CheckResultStatus.WARNING: 1,
+            sql.CheckResultStatus.FAILURE: 1,
+            sql.CheckResultStatus.BLOCKER: 1,
+        }
+    )
+    assert stat.files == {
+        sql.CheckResultStatus.WARNING: {str(path): 1},
+        sql.CheckResultStatus.FAILURE: {str(path): 1},
+        sql.CheckResultStatus.BLOCKER: {str(path): 1},
+    }
+
+
+def _make_check_result(
+    status: sql.CheckResultStatus,
+    message: str,
+    primary_rel_path: str | None = None,
+) -> sql.CheckResult:
+    return sql.CheckResult(
+        id=0,
+        release_key="test-1.0",
+        revision_number="00001",
+        checker="atr.tasks.checks.paths.check_errors",
+        checker_version="3",
+        primary_rel_path=primary_rel_path,
+        member_rel_path=None,
+        created=datetime.datetime.now(datetime.UTC),
+        status=status,
+        message=message,
+        data={},
+        inputs_hash=None,
+    )
+
+
+def _make_reader() -> releases.GeneralPublic:
+    read = mock.MagicMock()
+    read.authorisation.asf_uid = "testuser"
+    read_as = mock.MagicMock()
+    data = mock.AsyncMock()
+    return releases.GeneralPublic(read, read_as, data)


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org