This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/main by this push:
     new fe73b07  Add a summary of checks before the compose file list
fe73b07 is described below

commit fe73b070799ca919e4ea918adaf6fe3f2d7c711f
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Dec 23 15:10:16 2025 +0000

    Add a summary of checks before the compose file list
---
 atr/shared/__init__.py            | 56 ++++++++++++++++++++++++++
 atr/static/css/atr.css            | 23 +++++++++++
 atr/storage/readers/releases.py   | 83 ++++++++++++++++++++++++++++++++-------
 atr/storage/types.py              | 11 ++++++
 atr/templates/check-selected.html |  4 ++
 5 files changed, 163 insertions(+), 14 deletions(-)

diff --git a/atr/shared/__init__.py b/atr/shared/__init__.py
index dffeb9a..f3427d9 100644
--- a/atr/shared/__init__.py
+++ b/atr/shared/__init__.py
@@ -17,6 +17,8 @@
 
 from typing import TYPE_CHECKING, Final
 
+import htpy
+
 import atr.db as db
 import atr.db.interaction as interaction
 import atr.form as form
@@ -44,6 +46,7 @@ import atr.shared.user as user
 import atr.shared.vote as vote
 import atr.shared.voting as voting
 import atr.storage as storage
+import atr.storage.types as types
 import atr.template as template
 import atr.util as util
 import atr.web as web
@@ -167,6 +170,8 @@ async def check(
     strict_checking = release.project.policy_strict_checking
     strict_checking_errors = strict_checking and has_any_errors
 
+    checks_summary_html = _render_checks_summary(info, release.project.name, 
release.version)
+
     return await template.render(
         "check-selected.html",
         project_name=release.project.name,
@@ -198,9 +203,60 @@ async def check(
         strict_checking_errors=strict_checking_errors,
         can_vote=can_vote,
         can_resolve=can_resolve,
+        checks_summary_html=checks_summary_html,
     )
 
 
+def _checker_display_name(checker: str) -> str:
+    return checker.removeprefix("atr.tasks.checks.").replace("_", " 
").replace(".", " ").title()
+
+
+def _render_checks_summary(info: types.PathInfo | None, project_name: str, 
version_name: str) -> htm.Element | None:
+    if (info is None) or (not info.checker_stats):
+        return None
+
+    card = htm.Block(htm.div, classes=".card.mb-4")
+    card.div(".card-header")[htpy.h5(".mb-0")["Checks summary"]]
+
+    body = htm.Block(htm.div, classes=".card-body")
+    for i, stat in enumerate(info.checker_stats):
+        stripe_class = ".atr-stripe-odd" if ((i % 2) == 0) else 
".atr-stripe-even"
+        details = htm.Block(htm.details, classes=f".mb-0.p-2{stripe_class}")
+
+        summary_content: list[htm.Element | str] = []
+        if stat.warning_count > 0:
+            
summary_content.append(htpy.span(".badge.bg-warning.text-dark.me-2")[str(stat.warning_count)])
+        if stat.failure_count > 0:
+            
summary_content.append(htpy.span(".badge.bg-danger.me-2")[str(stat.failure_count)])
+        
summary_content.append(htpy.strong[_checker_display_name(stat.checker)])
+
+        details.summary[*summary_content]
+
+        files_div = htm.Block(htm.div, classes=".mt-2.atr-checks-files")
+        all_files = set(stat.failure_files.keys()) | 
set(stat.warning_files.keys())
+        for file_path in sorted(all_files):
+            report_url = f"/report/{project_name}/{version_name}/{file_path}"
+            error_count = stat.failure_files.get(file_path, 0)
+            warning_count = stat.warning_files.get(file_path, 0)
+
+            file_content: list[htm.Element | str] = []
+            if error_count > 0:
+                label = "error" if (error_count == 1) else "errors"
+                
file_content.append(htpy.span(".badge.bg-danger.me-2")[f"{error_count} 
{label}"])
+            if warning_count > 0:
+                label = "warning" if (warning_count == 1) else "warnings"
+                
file_content.append(htpy.span(".badge.bg-warning.text-dark.me-2")[f"{warning_count}
 {label}"])
+            
file_content.append(htpy.a(href=report_url)[htpy.strong[htpy.code[file_path]]])
+
+            files_div.div[*file_content]
+
+        details.append(files_div.collect())
+        body.append(details.collect())
+
+    card.append(body.collect())
+    return card.collect()
+
+
 def _warnings_from_vote_result(vote_task: sql.Task | None) -> list[str]:
     # TODO: Replace this with a schema.Strict model
     # But we'd still need to do some of this parsing and validation
diff --git a/atr/static/css/atr.css b/atr/static/css/atr.css
index 7003c24..a7ff400 100644
--- a/atr/static/css/atr.css
+++ b/atr/static/css/atr.css
@@ -522,3 +522,26 @@ span.warning {
     overflow-wrap: anywhere;
     word-break: break-all;
 }
+
+td.atr-shrink {
+    width: 1%;
+    white-space: nowrap;
+}
+
+.atr-stripe-odd {
+    background-color: #f8f9fa;
+}
+
+.atr-stripe-even {
+    background-color: #ffffff;
+}
+
+.atr-stripe-odd .badge,
+.atr-stripe-even .badge {
+    position: relative;
+    top: -1px;
+}
+
+.atr-checks-files {
+    line-height: 2.25;
+}
diff --git a/atr/storage/readers/releases.py b/atr/storage/readers/releases.py
index b1ecf6a..9d2a157 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 dataclasses
 import pathlib
 import re
 
@@ -28,6 +29,15 @@ import atr.storage as storage
 import atr.storage.types as types
 
 
[email protected]
+class CheckerAccumulator:
+    success: int = 0
+    warning: int = 0
+    failure: int = 0
+    warning_files: dict[str, int] = dataclasses.field(default_factory=dict)
+    failure_files: dict[str, int] = dataclasses.field(default_factory=dict)
+
+
 class GeneralPublic:
     def __init__(
         self,
@@ -55,8 +65,53 @@ class GeneralPublic:
                     info.artifacts.add(path)
                 elif search.group("metadata"):
                     info.metadata.add(path)
+        self.__compute_checker_stats(info, paths)
         return info
 
+    def __accumulate_results(
+        self,
+        results: dict[pathlib.Path, list[sql.CheckResult]],
+        paths_set: set[pathlib.Path],
+        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
+                else:
+                    acc.failure += 1
+                    acc.failure_files[path_str] = 
acc.failure_files.get(path_str, 0) + 1
+
+    def __compute_checker_stats(self, info: types.PathInfo, paths: 
list[pathlib.Path]) -> 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")
+
+        for checker, acc in sorted(checker_data.items()):
+            if (acc.warning == 0) and (acc.failure == 0):
+                continue
+            info.checker_stats.append(
+                types.CheckerStats(
+                    checker=checker,
+                    success_count=acc.success,
+                    warning_count=acc.warning,
+                    failure_count=acc.failure,
+                    warning_files=acc.warning_files,
+                    failure_files=acc.failure_files,
+                )
+            )
+
     async def __successes_errors_warnings(
         self, release: sql.Release, latest_revision_number: str, info: 
types.PathInfo
     ) -> None:
@@ -75,6 +130,20 @@ class GeneralPublic:
         await self.__warnings(cs)
         await self.__errors(cs)
 
+    async def __errors(self, cs: types.ChecksSubset) -> None:
+        errors = await self.__data.check_result(
+            release_name=cs.release.name,
+            revision_number=cs.latest_revision_number,
+            member_rel_path=None,
+            status=sql.CheckResultStatus.FAILURE,
+        ).all()
+        for error in errors:
+            if cs.match_ignore(error):
+                cs.info.ignored_errors.append(error)
+                continue
+            if primary_rel_path := error.primary_rel_path:
+                cs.info.errors.setdefault(pathlib.Path(primary_rel_path), 
[]).append(error)
+
     async def __successes(self, cs: types.ChecksSubset) -> None:
         successes = await self.__data.check_result(
             release_name=cs.release.name,
@@ -100,17 +169,3 @@ class GeneralPublic:
                 continue
             if primary_rel_path := warning.primary_rel_path:
                 cs.info.warnings.setdefault(pathlib.Path(primary_rel_path), 
[]).append(warning)
-
-    async def __errors(self, cs: types.ChecksSubset) -> None:
-        errors = await self.__data.check_result(
-            release_name=cs.release.name,
-            revision_number=cs.latest_revision_number,
-            member_rel_path=None,
-            status=sql.CheckResultStatus.FAILURE,
-        ).all()
-        for error in errors:
-            if cs.match_ignore(error):
-                cs.info.ignored_errors.append(error)
-                continue
-            if primary_rel_path := error.primary_rel_path:
-                cs.info.errors.setdefault(pathlib.Path(primary_rel_path), 
[]).append(error)
diff --git a/atr/storage/types.py b/atr/storage/types.py
index f08985c..22c5bf7 100644
--- a/atr/storage/types.py
+++ b/atr/storage/types.py
@@ -25,6 +25,16 @@ import atr.models.sql as sql
 import atr.storage.outcome as outcome
 
 
[email protected]
+class CheckerStats:
+    checker: str
+    success_count: int
+    warning_count: int
+    failure_count: int
+    warning_files: dict[str, int]
+    failure_files: dict[str, int]
+
+
 @dataclasses.dataclass
 class CheckResults:
     primary_results_list: list[sql.CheckResult]
@@ -52,6 +62,7 @@ class LinkedCommittee:
 
 class PathInfo(schema.Strict):
     artifacts: set[pathlib.Path] = schema.factory(set)
+    checker_stats: list[CheckerStats] = schema.factory(list)
     errors: dict[pathlib.Path, list[sql.CheckResult]] = schema.factory(dict)
     ignored_errors: list[sql.CheckResult] = schema.factory(list)
     ignored_warnings: list[sql.CheckResult] = schema.factory(list)
diff --git a/atr/templates/check-selected.html 
b/atr/templates/check-selected.html
index e74102e..a435da7 100644
--- a/atr/templates/check-selected.html
+++ b/atr/templates/check-selected.html
@@ -97,6 +97,10 @@
     </div>
   </div>
 
+  {% if checks_summary_html %}
+    {{ checks_summary_html|safe }}
+  {% endif %}
+
   <div class="card mb-4">
     <div class="card-header d-flex justify-content-between align-items-center">
       <h5 class="mb-0">


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to