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