@@ -22,18 +22,18 @@ jobs:
2222 contents : read
2323 steps :
2424 - name : Checkout code
25- uses : actions/checkout@v3
25+ uses : actions/checkout@v6
2626
2727 - name : Set up Python
28- uses : actions/setup-python@v4
28+ uses : actions/setup-python@v6
2929 with :
3030 python-version : ' 3.12'
3131
3232 - name : Set up QEMU
33- uses : docker/setup-qemu-action@v2
33+ uses : docker/setup-qemu-action@v4
3434
3535 - name : Set up Docker Buildx
36- uses : docker/setup-buildx-action@v2
36+ uses : docker/setup-buildx-action@v4
3737
3838 - name : Install dependencies
3939 run : |
@@ -44,16 +44,113 @@ jobs:
4444
4545 - name : Run tests
4646 run : |
47- pytest --cov --cov-branch --junitxml=junit.xml -o junit_family=legacy
47+ pytest --cov --cov-branch --cov-report=xml:coverage.xml --junitxml=junit.xml -o junit_family=legacy
48+
49+ - name : Write test summary
50+ if : ${{ always() }}
51+ run : |
52+ python - <<'PY'
53+ import os
54+ import sys
55+ import xml.etree.ElementTree as ET
56+ from pathlib import Path
57+
58+ summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
59+ if not summary_path:
60+ sys.exit(0)
61+
62+ junit_path = Path("junit.xml")
63+
64+ def md_escape(value):
65+ return str(value).replace("|", "\\|").replace("\n", "<br>")
66+
67+ lines = ["## Test summary", ""]
68+
69+ if not junit_path.exists():
70+ lines.extend([
71+ "No `junit.xml` test report was generated.",
72+ "",
73+ ])
74+ else:
75+ root = ET.parse(junit_path).getroot()
76+ suites = [root] if root.tag == "testsuite" else list(root.findall("testsuite"))
77+ if not suites:
78+ suites = [root]
79+
80+ totals = {"tests": 0, "failures": 0, "errors": 0, "skipped": 0, "time": 0.0}
81+ for suite in suites:
82+ totals["tests"] += int(suite.attrib.get("tests", 0))
83+ totals["failures"] += int(suite.attrib.get("failures", 0))
84+ totals["errors"] += int(suite.attrib.get("errors", 0))
85+ totals["skipped"] += int(suite.attrib.get("skipped", 0))
86+ totals["time"] += float(suite.attrib.get("time", 0.0))
87+
88+ passed = totals["tests"] - totals["failures"] - totals["errors"] - totals["skipped"]
89+ lines.extend([
90+ "| Total | Passed | Failed | Errors | Skipped | Duration |",
91+ "| ---: | ---: | ---: | ---: | ---: | ---: |",
92+ f"| {totals['tests']} | {passed} | {totals['failures']} | {totals['errors']} | {totals['skipped']} | {totals['time']:.2f}s |",
93+ "",
94+ "### Metadata",
95+ "",
96+ "| Key | Value |",
97+ "| --- | --- |",
98+ f"| Python | {md_escape(sys.version.split()[0])} |",
99+ f"| Runner OS | {md_escape(os.environ.get('RUNNER_OS', 'unknown'))} |",
100+ f"| Event | {md_escape(os.environ.get('GITHUB_EVENT_NAME', 'unknown'))} |",
101+ f"| Ref | {md_escape(os.environ.get('GITHUB_REF_NAME', 'unknown'))} |",
102+ f"| SHA | `{md_escape(os.environ.get('GITHUB_SHA', 'unknown'))}` |",
103+ f"| Actor | {md_escape(os.environ.get('GITHUB_ACTOR', 'unknown'))} |",
104+ "",
105+ ])
106+
107+ failed_cases = []
108+ for case in root.iter("testcase"):
109+ issue = case.find("failure")
110+ if issue is None:
111+ issue = case.find("error")
112+ if issue is not None:
113+ failed_cases.append((case.attrib, issue))
114+
115+ if failed_cases:
116+ lines.extend([
117+ "### Failed tests",
118+ "",
119+ "| Test | Type | Message |",
120+ "| --- | --- | --- |",
121+ ])
122+ for attrs, issue in failed_cases[:25]:
123+ classname = attrs.get("classname", "")
124+ name = attrs.get("name", "unknown")
125+ test_name = f"{classname}.{name}" if classname else name
126+ message = issue.attrib.get("message", "").strip() or (issue.text or "").strip().splitlines()[0:1]
127+ if isinstance(message, list):
128+ message = message[0] if message else ""
129+ lines.append(f"| `{md_escape(test_name)}` | {md_escape(issue.tag)} | {md_escape(message[:300])} |")
130+ if len(failed_cases) > 25:
131+ lines.append(f"| ... | ... | {len(failed_cases) - 25} more failing tests omitted from summary |")
132+ lines.append("")
133+
134+ with open(summary_path, "a", encoding="utf-8") as summary:
135+ summary.write("\n".join(lines))
136+ summary.write("\n")
137+ PY
48138
49139 - name : Upload coverage reports to Codecov
140+ if : ${{ !cancelled() }}
50141 uses : codecov/codecov-action@v5
51142 with :
52143 token : ${{ secrets.CODECOV_TOKEN }}
144+ files : coverage.xml
145+ disable_search : true
53146 fail_ci_if_error : false
54147
55148 - name : Upload test results to Codecov
56149 if : ${{ !cancelled() }}
57- uses : codecov/test-results- action@v1
150+ uses : codecov/codecov- action@v5
58151 with :
59152 token : ${{ secrets.CODECOV_TOKEN }}
153+ files : junit.xml
154+ disable_search : true
155+ report_type : test_results
156+ fail_ci_if_error : false
0 commit comments