diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 3846726..43e775b 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -346,14 +346,32 @@ jobs: - test-unrealircd-atheme runs-on: ubuntu-latest steps: + - uses: actions/checkout@v2 - name: Download Artifacts uses: actions/download-artifact@v2 with: path: artifacts - - name: Publish Unit Test Results - uses: EnricoMi/publish-unit-test-result-action@v1 + - if: github.event_name == 'pull_request' + name: Publish Unit Test Results + uses: actions/github-script@v4 with: - files: artifacts/**/*.xml + result-encoding: string + script: | + let body = ''; + const options = {}; + options.listeners = { + stdout: (data) => { + body += data.toString(); + } + }; + await exec.exec('bash', ['-c', 'shopt -s globstar; python3 report.py artifacts/**/*.xml'], options); + github.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body, + }); + return body; test-bahamut: needs: - build-bahamut diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml index d91a5c5..92fc3cc 100644 --- a/.github/workflows/test-devel_release.yml +++ b/.github/workflows/test-devel_release.yml @@ -78,14 +78,32 @@ jobs: - test-inspircd-atheme runs-on: ubuntu-latest steps: + - uses: actions/checkout@v2 - name: Download Artifacts uses: actions/download-artifact@v2 with: path: artifacts - - name: Publish Unit Test Results - uses: EnricoMi/publish-unit-test-result-action@v1 + - if: github.event_name == 'pull_request' + name: Publish Unit Test Results + uses: actions/github-script@v4 with: - files: artifacts/**/*.xml + result-encoding: string + script: | + let body = ''; + const options = {}; + options.listeners = { + stdout: (data) => { + body += data.toString(); + } + }; + await exec.exec('bash', ['-c', 'shopt -s globstar; python3 report.py artifacts/**/*.xml'], options); + github.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body, + }); + return body; test-inspircd: needs: - build-inspircd diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 5d2b654..20f253e 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -389,14 +389,32 @@ jobs: - test-unrealircd-atheme runs-on: ubuntu-latest steps: + - uses: actions/checkout@v2 - name: Download Artifacts uses: actions/download-artifact@v2 with: path: artifacts - - name: Publish Unit Test Results - uses: EnricoMi/publish-unit-test-result-action@v1 + - if: github.event_name == 'pull_request' + name: Publish Unit Test Results + uses: actions/github-script@v4 with: - files: artifacts/**/*.xml + result-encoding: string + script: | + let body = ''; + const options = {}; + options.listeners = { + stdout: (data) => { + body += data.toString(); + } + }; + await exec.exec('bash', ['-c', 'shopt -s globstar; python3 report.py artifacts/**/*.xml'], options); + github.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body, + }); + return body; test-bahamut: needs: - build-bahamut diff --git a/make_workflows.py b/make_workflows.py index f6c0741..4a74f90 100644 --- a/make_workflows.py +++ b/make_workflows.py @@ -10,6 +10,7 @@ and keep them in sync. import enum import pathlib +import textwrap import yaml @@ -357,6 +358,7 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor): # this job then "if": "success() || failure()", "steps": [ + {"uses": "actions/checkout@v2"}, { "name": "Download Artifacts", "uses": "actions/download-artifact@v2", @@ -364,8 +366,32 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor): }, { "name": "Publish Unit Test Results", - "uses": "EnricoMi/publish-unit-test-result-action@v1", - "with": {"files": "artifacts/**/*.xml"}, + "uses": "actions/github-script@v4", + "if": "github.event_name == 'pull_request'", + "with": { + "result-encoding": "string", + "script": script( + textwrap.dedent( + """\ + let body = ''; + const options = {}; + options.listeners = { + stdout: (data) => { + body += data.toString(); + } + }; + await exec.exec('bash', ['-c', 'shopt -s globstar; python3 report.py artifacts/**/*.xml'], options); + github.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body, + }); + return body; + """ + ) + ), + }, }, ], } diff --git a/report.py b/report.py new file mode 100644 index 0000000..744fb7c --- /dev/null +++ b/report.py @@ -0,0 +1,179 @@ +""" +Reads pytest's XML output, and produces a report of the test run(s). +""" + +import dataclasses +import functools +import itertools +import re +import sys +import textwrap +from typing import Optional, Set +import xml.etree.ElementTree as ET + + +def visit_bottomup(f, d): + """Visits and rewrites a nested-dict ``d`` from the bottom to the top, + using the ``f`` predicate.""" + if isinstance(d, dict): + return f({k: visit_bottomup(f, v) for (k, v) in d.items()}) + else: + return f(d) + + +@dataclasses.dataclass +class CaseResult: + success: bool + skipped: bool + type: Optional[str] = None + message: Optional[str] = None + + +@dataclasses.dataclass +class CompactedResult: + success: bool + count: int + nb_skipped: int + messages: Set[str] + + +def partial_compaction(d): + # Group all the perfect successes together, but keep those with skipped + # tests separate + compacted_d = {} + successes = [] + for (k, v) in d.items(): + if isinstance(v, CompactedResult) and v.success and v.nb_skipped == 0: + successes.append((k, v)) + else: + compacted_d[k] = v + if len(successes) == 0: + pass + elif len(successes) == 1: + ((k, v),) = successes + compacted_d[k] = v + else: + compacted_d["(others)"] = CompactedResult( + success=True, + count=sum(res.count for (_, res) in successes), + nb_skipped=0, + messages=set(), + ) + return compacted_d + + +def compact_results(d): + """Rewrite the nested dict ``d`` of CaseResult in a more compact form; + by folding successful subtrees.""" + if isinstance(d, dict): + if set(d) == {None}: + return d[None] + while len(d) == 1 and all(isinstance(v, dict) for v in d.values()): + (key,) = d + d = {f"{key}::{k}": v for (k, v) in d[key].items()} + if not all(isinstance(v, CompactedResult) for v in d.values()): + # Some children are not compactable, so this subtree isn't either + return partial_compaction(d) + statuses = {v.success for v in d.values()} + if len(statuses) == 1: + (status,) = statuses + return CompactedResult( + success=status, + count=sum(v.count for v in d.values()), + nb_skipped=sum(v.nb_skipped for v in d.values()), + messages=set( + itertools.chain.from_iterable(v.messages for v in d.values()) + ), + ) + else: + return partial_compaction(d) + elif isinstance(d, CaseResult): + return CompactedResult( + success=d.success, + count=1, + nb_skipped=int(d.skipped), + messages={d.message} if d.message else set(), + ) + else: + assert False, repr(d) + + +def format_results(d) -> str: + """Using the nested dict ``d`` of CaseResult, formats a string report.""" + if isinstance(d, dict): + items = [f"