|
1 | 1 | import os |
2 | 2 | import random |
| 3 | +import re |
3 | 4 | import unittest |
4 | 5 | from distutils.util import strtobool |
| 6 | +from pathlib import Path |
5 | 7 | from typing import Union |
6 | 8 |
|
7 | 9 | import torch |
@@ -92,3 +94,157 @@ def load_image(image: Union[str, PIL.Image.Image]) -> PIL.Image.Image: |
92 | 94 | image = PIL.ImageOps.exif_transpose(image) |
93 | 95 | image = image.convert("RGB") |
94 | 96 | return image |
| 97 | + |
| 98 | + |
| 99 | +# --- pytest conf functions --- # |
| 100 | + |
| 101 | +# to avoid multiple invocation from tests/conftest.py and examples/conftest.py - make sure it's called only once |
| 102 | +pytest_opt_registered = {} |
| 103 | + |
| 104 | + |
| 105 | +def pytest_addoption_shared(parser): |
| 106 | + """ |
| 107 | + This function is to be called from `conftest.py` via `pytest_addoption` wrapper that has to be defined there. |
| 108 | +
|
| 109 | + It allows loading both `conftest.py` files at once without causing a failure due to adding the same `pytest` |
| 110 | + option. |
| 111 | +
|
| 112 | + """ |
| 113 | + option = "--make-reports" |
| 114 | + if option not in pytest_opt_registered: |
| 115 | + parser.addoption( |
| 116 | + option, |
| 117 | + action="store", |
| 118 | + default=False, |
| 119 | + help="generate report files. The value of this option is used as a prefix to report names", |
| 120 | + ) |
| 121 | + pytest_opt_registered[option] = 1 |
| 122 | + |
| 123 | + |
| 124 | +def pytest_terminal_summary_main(tr, id): |
| 125 | + """ |
| 126 | + Generate multiple reports at the end of test suite run - each report goes into a dedicated file in the current |
| 127 | + directory. The report files are prefixed with the test suite name. |
| 128 | +
|
| 129 | + This function emulates --duration and -rA pytest arguments. |
| 130 | +
|
| 131 | + This function is to be called from `conftest.py` via `pytest_terminal_summary` wrapper that has to be defined |
| 132 | + there. |
| 133 | +
|
| 134 | + Args: |
| 135 | + - tr: `terminalreporter` passed from `conftest.py` |
| 136 | + - id: unique id like `tests` or `examples` that will be incorporated into the final reports filenames - this is |
| 137 | + needed as some jobs have multiple runs of pytest, so we can't have them overwrite each other. |
| 138 | +
|
| 139 | + NB: this functions taps into a private _pytest API and while unlikely, it could break should |
| 140 | + pytest do internal changes - also it calls default internal methods of terminalreporter which |
| 141 | + can be hijacked by various `pytest-` plugins and interfere. |
| 142 | +
|
| 143 | + """ |
| 144 | + from _pytest.config import create_terminal_writer |
| 145 | + |
| 146 | + if not len(id): |
| 147 | + id = "tests" |
| 148 | + |
| 149 | + config = tr.config |
| 150 | + orig_writer = config.get_terminal_writer() |
| 151 | + orig_tbstyle = config.option.tbstyle |
| 152 | + orig_reportchars = tr.reportchars |
| 153 | + |
| 154 | + dir = "reports" |
| 155 | + Path(dir).mkdir(parents=True, exist_ok=True) |
| 156 | + report_files = { |
| 157 | + k: f"{dir}/{id}_{k}.txt" |
| 158 | + for k in [ |
| 159 | + "durations", |
| 160 | + "errors", |
| 161 | + "failures_long", |
| 162 | + "failures_short", |
| 163 | + "failures_line", |
| 164 | + "passes", |
| 165 | + "stats", |
| 166 | + "summary_short", |
| 167 | + "warnings", |
| 168 | + ] |
| 169 | + } |
| 170 | + |
| 171 | + # custom durations report |
| 172 | + # note: there is no need to call pytest --durations=XX to get this separate report |
| 173 | + # adapted from https://github.com/pytest-dev/pytest/blob/897f151e/src/_pytest/runner.py#L66 |
| 174 | + dlist = [] |
| 175 | + for replist in tr.stats.values(): |
| 176 | + for rep in replist: |
| 177 | + if hasattr(rep, "duration"): |
| 178 | + dlist.append(rep) |
| 179 | + if dlist: |
| 180 | + dlist.sort(key=lambda x: x.duration, reverse=True) |
| 181 | + with open(report_files["durations"], "w") as f: |
| 182 | + durations_min = 0.05 # sec |
| 183 | + f.write("slowest durations\n") |
| 184 | + for i, rep in enumerate(dlist): |
| 185 | + if rep.duration < durations_min: |
| 186 | + f.write(f"{len(dlist)-i} durations < {durations_min} secs were omitted") |
| 187 | + break |
| 188 | + f.write(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}\n") |
| 189 | + |
| 190 | + def summary_failures_short(tr): |
| 191 | + # expecting that the reports were --tb=long (default) so we chop them off here to the last frame |
| 192 | + reports = tr.getreports("failed") |
| 193 | + if not reports: |
| 194 | + return |
| 195 | + tr.write_sep("=", "FAILURES SHORT STACK") |
| 196 | + for rep in reports: |
| 197 | + msg = tr._getfailureheadline(rep) |
| 198 | + tr.write_sep("_", msg, red=True, bold=True) |
| 199 | + # chop off the optional leading extra frames, leaving only the last one |
| 200 | + longrepr = re.sub(r".*_ _ _ (_ ){10,}_ _ ", "", rep.longreprtext, 0, re.M | re.S) |
| 201 | + tr._tw.line(longrepr) |
| 202 | + # note: not printing out any rep.sections to keep the report short |
| 203 | + |
| 204 | + # use ready-made report funcs, we are just hijacking the filehandle to log to a dedicated file each |
| 205 | + # adapted from https://github.com/pytest-dev/pytest/blob/897f151e/src/_pytest/terminal.py#L814 |
| 206 | + # note: some pytest plugins may interfere by hijacking the default `terminalreporter` (e.g. |
| 207 | + # pytest-instafail does that) |
| 208 | + |
| 209 | + # report failures with line/short/long styles |
| 210 | + config.option.tbstyle = "auto" # full tb |
| 211 | + with open(report_files["failures_long"], "w") as f: |
| 212 | + tr._tw = create_terminal_writer(config, f) |
| 213 | + tr.summary_failures() |
| 214 | + |
| 215 | + # config.option.tbstyle = "short" # short tb |
| 216 | + with open(report_files["failures_short"], "w") as f: |
| 217 | + tr._tw = create_terminal_writer(config, f) |
| 218 | + summary_failures_short(tr) |
| 219 | + |
| 220 | + config.option.tbstyle = "line" # one line per error |
| 221 | + with open(report_files["failures_line"], "w") as f: |
| 222 | + tr._tw = create_terminal_writer(config, f) |
| 223 | + tr.summary_failures() |
| 224 | + |
| 225 | + with open(report_files["errors"], "w") as f: |
| 226 | + tr._tw = create_terminal_writer(config, f) |
| 227 | + tr.summary_errors() |
| 228 | + |
| 229 | + with open(report_files["warnings"], "w") as f: |
| 230 | + tr._tw = create_terminal_writer(config, f) |
| 231 | + tr.summary_warnings() # normal warnings |
| 232 | + tr.summary_warnings() # final warnings |
| 233 | + |
| 234 | + tr.reportchars = "wPpsxXEf" # emulate -rA (used in summary_passes() and short_test_summary()) |
| 235 | + with open(report_files["passes"], "w") as f: |
| 236 | + tr._tw = create_terminal_writer(config, f) |
| 237 | + tr.summary_passes() |
| 238 | + |
| 239 | + with open(report_files["summary_short"], "w") as f: |
| 240 | + tr._tw = create_terminal_writer(config, f) |
| 241 | + tr.short_test_summary() |
| 242 | + |
| 243 | + with open(report_files["stats"], "w") as f: |
| 244 | + tr._tw = create_terminal_writer(config, f) |
| 245 | + tr.summary_stats() |
| 246 | + |
| 247 | + # restore: |
| 248 | + tr._tw = orig_writer |
| 249 | + tr.reportchars = orig_reportchars |
| 250 | + config.option.tbstyle = orig_tbstyle |
0 commit comments