ci: per-service unit test table in the run summary via TRX (refs #136)
This commit is contained in:
@@ -70,6 +70,12 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
nuget-${{ runner.os }}-
|
nuget-${{ runner.os }}-
|
||||||
- run: make unit
|
- run: make unit
|
||||||
|
# Job summary (#136): a per-service pass/fail table from the TRX `make unit` wrote.
|
||||||
|
- name: Unit test summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || exit 0
|
||||||
|
python3 infra/trx-summary.py TestResults >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
# Frontend (Nx/Angular) lane: install with pnpm, then Nx lint + test + build.
|
# Frontend (Nx/Angular) lane: install with pnpm, then Nx lint + test + build.
|
||||||
frontend:
|
frontend:
|
||||||
|
|||||||
@@ -58,3 +58,4 @@ tests/e2e/node_modules/
|
|||||||
tests/e2e/test-results/
|
tests/e2e/test-results/
|
||||||
tests/e2e/playwright-report/
|
tests/e2e/playwright-report/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
TestResults/
|
||||||
|
|||||||
@@ -70,8 +70,9 @@ build:
|
|||||||
dotnet build $(SLN) -c Release
|
dotnet build $(SLN) -c Release
|
||||||
|
|
||||||
## unit: run unit tests (excludes the container-backed Integration lane)
|
## unit: run unit tests (excludes the container-backed Integration lane)
|
||||||
|
# TRX per test project (→ TestResults/) feeds the CI per-service summary (#136); harmless locally.
|
||||||
unit:
|
unit:
|
||||||
dotnet test $(SLN) -c Release --filter "Category!=Integration"
|
dotnet test $(SLN) -c Release --filter "Category!=Integration" --logger trx --results-directory TestResults
|
||||||
|
|
||||||
## mutation: run the Stryker.NET ratchet on each service with branching logic (fails below baseline)
|
## mutation: run the Stryker.NET ratchet on each service with branching logic (fails below baseline)
|
||||||
# Stryker is pinned as a local dotnet tool (.config/dotnet-tools.json); `tool restore`
|
# Stryker is pinned as a local dotnet tool (.config/dotnet-tools.json); `tool restore`
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Render a per-test-project table from .trx files for a Gitea job summary (#136).
|
||||||
|
|
||||||
|
Reads every *.trx in the given directory (default: TestResults), pulls each project's
|
||||||
|
counters + assembly name, and prints a GitHub/Gitea-flavoured markdown table to stdout.
|
||||||
|
The CI step redirects that into $GITHUB_STEP_SUMMARY. Stdlib only.
|
||||||
|
"""
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
NS = {"t": "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"}
|
||||||
|
|
||||||
|
|
||||||
|
def project_name(root):
|
||||||
|
# The test assembly path, e.g. …/services/domain/Big.Tests/bin/…/big.tests.dll. Prefer the
|
||||||
|
# owning service folder (services/<name>) so "domain" shows rather than the opaque "big.tests";
|
||||||
|
# fall back to the assembly basename for projects outside services/ (e.g. tests/acceptance).
|
||||||
|
ut = root.find(".//t:TestDefinitions/t:UnitTest", NS)
|
||||||
|
storage = ut.get("storage") if ut is not None else None
|
||||||
|
if not storage:
|
||||||
|
return None
|
||||||
|
parts = storage.replace("\\", "/").split("/")
|
||||||
|
if "services" in parts:
|
||||||
|
return parts[parts.index("services") + 1]
|
||||||
|
base = os.path.basename(parts[-1])
|
||||||
|
return base[:-4] if base.lower().endswith(".dll") else base
|
||||||
|
|
||||||
|
|
||||||
|
def parse(path):
|
||||||
|
root = ET.parse(path).getroot()
|
||||||
|
c = root.find(".//t:ResultSummary/t:Counters", NS)
|
||||||
|
if c is None:
|
||||||
|
return None
|
||||||
|
total = int(c.get("total", 0))
|
||||||
|
if total == 0: # e.g. the Integration project, filtered out of the unit run
|
||||||
|
return None
|
||||||
|
executed = int(c.get("executed", 0))
|
||||||
|
passed = int(c.get("passed", 0))
|
||||||
|
failed = int(c.get("failed", 0)) + int(c.get("error", 0))
|
||||||
|
skipped = total - executed
|
||||||
|
return {
|
||||||
|
"name": project_name(root) or os.path.basename(path),
|
||||||
|
"passed": passed, "failed": failed, "skipped": skipped, "total": total,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main(results_dir):
|
||||||
|
rows = [r for r in (parse(p) for p in sorted(glob.glob(os.path.join(results_dir, "*.trx")))) if r]
|
||||||
|
if not rows:
|
||||||
|
print("_No test results found._")
|
||||||
|
return 0
|
||||||
|
rows.sort(key=lambda r: r["name"])
|
||||||
|
print("## ✅ Unit tests\n")
|
||||||
|
print("| Project | Result | Passed | Failed | Skipped | Total |")
|
||||||
|
print("| ------- | :----: | -----: | -----: | ------: | ----: |")
|
||||||
|
for r in rows:
|
||||||
|
status = "❌" if r["failed"] else "✅"
|
||||||
|
print(f"| {r['name']} | {status} | {r['passed']} | {r['failed']} | {r['skipped']} | {r['total']} |")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "TestResults"))
|
||||||
Reference in New Issue
Block a user