Skip to content

Commit d7931f9

Browse files
authored
use pytest-reportlog to generate upstream-dev CI failure reports (#6699)
* rely on pytest-reportlog to produce the log file * use the new script to generate the report * install the dependencies of the parser script * temporarily enable report generation [test-upstream] * actually upload the log file [skip-ci][test-upstream] * print the created report [skip-ci][test-upstream] * Undo the temporary changes [test-upstream]
1 parent 2b6e729 commit d7931f9

File tree

2 files changed

+96
-45
lines changed

2 files changed

+96
-45
lines changed

.github/workflows/parse_logs.py

Lines changed: 84 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,102 @@
11
# type: ignore
22
import argparse
3-
import itertools
3+
import functools
4+
import json
45
import pathlib
56
import textwrap
7+
from dataclasses import dataclass
68

7-
parser = argparse.ArgumentParser()
8-
parser.add_argument("filepaths", nargs="+", type=pathlib.Path)
9-
args = parser.parse_args()
9+
from pytest import CollectReport, TestReport
1010

11-
filepaths = sorted(p for p in args.filepaths if p.is_file())
1211

12+
@dataclass
13+
class SessionStart:
14+
pytest_version: str
15+
outcome: str = "status"
1316

14-
def extract_short_test_summary_info(lines):
15-
up_to_start_of_section = itertools.dropwhile(
16-
lambda l: "=== short test summary info ===" not in l,
17-
lines,
18-
)
19-
up_to_section_content = itertools.islice(up_to_start_of_section, 1, None)
20-
section_content = itertools.takewhile(
21-
lambda l: l.startswith("FAILED") or l.startswith("ERROR"), up_to_section_content
22-
)
23-
content = "\n".join(section_content)
17+
@classmethod
18+
def _from_json(cls, json):
19+
json_ = json.copy()
20+
json_.pop("$report_type")
21+
return cls(**json_)
2422

25-
return content
2623

24+
@dataclass
25+
class SessionFinish:
26+
exitstatus: str
27+
outcome: str = "status"
2728

28-
def format_log_message(path):
29-
py_version = path.name.split("-")[1]
30-
summary = f"Python {py_version} Test Summary Info"
31-
with open(path) as f:
32-
data = extract_short_test_summary_info(line.rstrip() for line in f)
33-
message = (
34-
textwrap.dedent(
35-
"""\
36-
<details><summary>{summary}</summary>
29+
@classmethod
30+
def _from_json(cls, json):
31+
json_ = json.copy()
32+
json_.pop("$report_type")
33+
return cls(**json_)
3734

38-
```
39-
{data}
40-
```
4135

42-
</details>
43-
"""
44-
)
45-
.rstrip()
46-
.format(summary=summary, data=data)
47-
)
36+
def parse_record(record):
37+
report_types = {
38+
"TestReport": TestReport,
39+
"CollectReport": CollectReport,
40+
"SessionStart": SessionStart,
41+
"SessionFinish": SessionFinish,
42+
}
43+
cls = report_types.get(record["$report_type"])
44+
if cls is None:
45+
raise ValueError(f"unknown report type: {record['$report_type']}")
4846

47+
return cls._from_json(record)
48+
49+
50+
@functools.singledispatch
51+
def format_summary(report):
52+
return f"{report.nodeid}: {report}"
53+
54+
55+
@format_summary.register
56+
def _(report: TestReport):
57+
message = report.longrepr.chain[0][1].message
58+
return f"{report.nodeid}: {message}"
59+
60+
61+
@format_summary.register
62+
def _(report: CollectReport):
63+
message = report.longrepr.split("\n")[-1].removeprefix("E").lstrip()
64+
return f"{report.nodeid}: {message}"
65+
66+
67+
def format_report(reports, py_version):
68+
newline = "\n"
69+
summaries = newline.join(format_summary(r) for r in reports)
70+
message = textwrap.dedent(
71+
"""\
72+
<details><summary>Python {py_version} Test Summary</summary>
73+
74+
```
75+
{summaries}
76+
```
77+
78+
</details>
79+
"""
80+
).format(summaries=summaries, py_version=py_version)
4981
return message
5082

5183

52-
print("Parsing logs ...")
53-
message = "\n\n".join(format_log_message(path) for path in filepaths)
84+
if __name__ == "__main__":
85+
parser = argparse.ArgumentParser()
86+
parser.add_argument("filepath", type=pathlib.Path)
87+
args = parser.parse_args()
88+
89+
py_version = args.filepath.stem.split("-")[1]
90+
91+
print("Parsing logs ...")
92+
93+
lines = args.filepath.read_text().splitlines()
94+
reports = [parse_record(json.loads(line)) for line in lines]
95+
96+
failed = [report for report in reports if report.outcome == "failed"]
97+
98+
message = format_report(failed, py_version=py_version)
5499

55-
output_file = pathlib.Path("pytest-logs.txt")
56-
print(f"Writing output file to: {output_file.absolute()}")
57-
output_file.write_text(message)
100+
output_file = pathlib.Path("pytest-logs.txt")
101+
print(f"Writing output file to: {output_file.absolute()}")
102+
output_file.write_text(message)

.github/workflows/upstream-dev-ci.yaml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ jobs:
6262
environment-name: xarray-tests
6363
extra-specs: |
6464
python=${{ matrix.python-version }}
65+
pytest-reportlog
6566
- name: Install upstream versions
6667
run: |
6768
bash ci/install-upstream-wheels.sh
@@ -80,10 +81,11 @@ jobs:
8081
if: success()
8182
id: status
8283
run: |
83-
set -euo pipefail
84-
python -m pytest --timeout=60 -rf | tee output-${{ matrix.python-version }}-log || (
84+
python -m pytest --timeout=60 -rf \
85+
--report-log output-${{ matrix.python-version }}-log.jsonl \
86+
|| (
8587
echo '::set-output name=ARTIFACTS_AVAILABLE::true' && false
86-
)
88+
)
8789
- name: Upload artifacts
8890
if: |
8991
failure()
@@ -92,8 +94,8 @@ jobs:
9294
&& github.repository == 'pydata/xarray'
9395
uses: actions/upload-artifact@v3
9496
with:
95-
name: output-${{ matrix.python-version }}-log
96-
path: output-${{ matrix.python-version }}-log
97+
name: output-${{ matrix.python-version }}-log.jsonl
98+
path: output-${{ matrix.python-version }}-log.jsonl
9799
retention-days: 5
98100

99101
report:
@@ -119,10 +121,14 @@ jobs:
119121
run: |
120122
rsync -a /tmp/workspace/logs/output-*/ ./logs
121123
ls -R ./logs
124+
- name: install dependencies
125+
run: |
126+
python -m pip install pytest
122127
- name: Parse logs
123128
run: |
124129
shopt -s globstar
125-
python .github/workflows/parse_logs.py logs/**/*-log
130+
python .github/workflows/parse_logs.py logs/**/*-log*
131+
cat pytest-logs.txt
126132
- name: Report failures
127133
uses: actions/github-script@v6
128134
with:

0 commit comments

Comments
 (0)