forgeqe-cli 2.2.1__py3-none-any.whl → 2.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- forgeqe_cli/__init__.py +1 -1
- forgeqe_cli/pytest_plugin.py +132 -0
- {forgeqe_cli-2.2.1.dist-info → forgeqe_cli-2.2.2.dist-info}/METADATA +1 -1
- forgeqe_cli-2.2.2.dist-info/RECORD +10 -0
- {forgeqe_cli-2.2.1.dist-info → forgeqe_cli-2.2.2.dist-info}/entry_points.txt +3 -0
- forgeqe_cli-2.2.1.dist-info/RECORD +0 -9
- {forgeqe_cli-2.2.1.dist-info → forgeqe_cli-2.2.2.dist-info}/WHEEL +0 -0
- {forgeqe_cli-2.2.1.dist-info → forgeqe_cli-2.2.2.dist-info}/licenses/LICENSE +0 -0
- {forgeqe_cli-2.2.1.dist-info → forgeqe_cli-2.2.2.dist-info}/top_level.txt +0 -0
forgeqe_cli/__init__.py
CHANGED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""ForgeQE plugin for pytest: an HTML report after every run, from the terminal, an IDE's test runner, or CI.
|
|
2
|
+
|
|
3
|
+
pytest loads it by itself from the forgeqe-cli wheel (the pytest11 entry point). It is on in a folder with
|
|
4
|
+
forgeqe.yaml or .forgeqe/, or with ``pytest --forgeqe-report`` or FORGEQE_REPORT=1; FORGEQE_REPORT=0 turns it off.
|
|
5
|
+
At the end of the run it writes the results, with each failure's message and traceback, to
|
|
6
|
+
.pytest_cache/forgeqe/results.json and runs ``forgeqe report`` on it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
import time
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _clean(text: str) -> str:
|
|
25
|
+
return ANSI.sub("", text or "")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def pytest_addoption(parser):
|
|
29
|
+
parser.getgroup("forgeqe").addoption(
|
|
30
|
+
"--forgeqe-report", action="store_true", default=False, help="write a ForgeQE HTML report after the run"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _active(config) -> bool:
|
|
35
|
+
flag = os.environ.get("FORGEQE_REPORT")
|
|
36
|
+
if flag == "0":
|
|
37
|
+
return False
|
|
38
|
+
if flag == "1" or config.getoption("forgeqe_report", False):
|
|
39
|
+
return True
|
|
40
|
+
root = Path(str(config.rootpath))
|
|
41
|
+
return (root / "forgeqe.yaml").exists() or (root / ".forgeqe").is_dir()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def pytest_configure(config):
|
|
45
|
+
# An xdist worker sends its results to the controller, which writes the one report.
|
|
46
|
+
if hasattr(config, "workerinput") or not _active(config):
|
|
47
|
+
return
|
|
48
|
+
config.pluginmanager.register(_Collector(config), "forgeqe-collector")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _message(report) -> str:
|
|
52
|
+
crash = getattr(getattr(report, "longrepr", None), "reprcrash", None)
|
|
53
|
+
text = crash.message if crash is not None else str(getattr(report, "longrepr", "") or "")
|
|
54
|
+
first = next((line.strip() for line in _clean(text).splitlines() if line.strip()), "")
|
|
55
|
+
return first[:300]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _Collector:
|
|
59
|
+
def __init__(self, config):
|
|
60
|
+
self.config = config
|
|
61
|
+
self.tests: dict[str, dict] = {}
|
|
62
|
+
self.started = datetime.now(timezone.utc)
|
|
63
|
+
self.t0 = time.monotonic()
|
|
64
|
+
self.line = ""
|
|
65
|
+
|
|
66
|
+
def pytest_runtest_logreport(self, report):
|
|
67
|
+
nodeid = report.nodeid
|
|
68
|
+
test = self.tests.setdefault(
|
|
69
|
+
nodeid, {"name": " › ".join(nodeid.split("::")), "status": "passed", "seconds": 0.0, "file": report.location[0]}
|
|
70
|
+
)
|
|
71
|
+
if report.outcome == "rerun":
|
|
72
|
+
# pytest-rerunfailures: a failure that is run again; passing later makes the test flaky, not failed.
|
|
73
|
+
test["rerun"] = True
|
|
74
|
+
test.setdefault("message", _message(report))
|
|
75
|
+
test.setdefault("details", _clean(report.longreprtext))
|
|
76
|
+
return
|
|
77
|
+
test["seconds"] = round(test["seconds"] + (report.duration or 0), 3)
|
|
78
|
+
if report.failed:
|
|
79
|
+
if test["status"] != "failed":
|
|
80
|
+
test["message"] = _message(report)
|
|
81
|
+
test["details"] = _clean(report.longreprtext)
|
|
82
|
+
test["status"] = "failed"
|
|
83
|
+
elif report.skipped and test["status"] == "passed" and report.when in ("setup", "call"):
|
|
84
|
+
test["status"] = "skipped"
|
|
85
|
+
|
|
86
|
+
def pytest_sessionfinish(self, session):
|
|
87
|
+
if not self.tests:
|
|
88
|
+
return
|
|
89
|
+
tests = []
|
|
90
|
+
for test in self.tests.values():
|
|
91
|
+
rerun = test.pop("rerun", False)
|
|
92
|
+
if test["status"] == "passed" and rerun:
|
|
93
|
+
test["flaky"] = True
|
|
94
|
+
elif test["status"] != "failed" and not test.get("flaky"):
|
|
95
|
+
test.pop("message", None)
|
|
96
|
+
test.pop("details", None)
|
|
97
|
+
tests.append({k: v for k, v in test.items() if v not in ("", None)})
|
|
98
|
+
doc = {
|
|
99
|
+
"format": "forgeqe-results",
|
|
100
|
+
"version": 1,
|
|
101
|
+
"framework": "pytest",
|
|
102
|
+
"startedAt": self.started.isoformat(),
|
|
103
|
+
"ms": round((time.monotonic() - self.t0) * 1000),
|
|
104
|
+
"command": " ".join(["pytest", *self.config.invocation_params.args])[:500],
|
|
105
|
+
"tests": tests,
|
|
106
|
+
}
|
|
107
|
+
cache = getattr(self.config, "cache", None)
|
|
108
|
+
folder = Path(cache.mkdir("forgeqe")) if cache is not None else Path(tempfile.mkdtemp(prefix="forgeqe-"))
|
|
109
|
+
path = folder / "results.json"
|
|
110
|
+
path.write_text(json.dumps(doc, indent=2) + "\n", encoding="utf-8")
|
|
111
|
+
self.line = _report(path, Path(str(self.config.rootpath)))
|
|
112
|
+
|
|
113
|
+
def pytest_terminal_summary(self, terminalreporter):
|
|
114
|
+
if self.line:
|
|
115
|
+
terminalreporter.write_line(self.line)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _report(path: Path, root: Path) -> str:
|
|
119
|
+
"""Run forgeqe report on the results; the line to show."""
|
|
120
|
+
# This wheel's own launcher, by the Python running pytest: no PATH lookup and no .cmd shim on Windows.
|
|
121
|
+
bin_ = os.environ.get("FORGEQE_BIN")
|
|
122
|
+
cmd = [bin_, "report", str(path)] if bin_ else [sys.executable, "-m", "forgeqe_cli", "report", str(path)]
|
|
123
|
+
try:
|
|
124
|
+
run = subprocess.run(cmd, cwd=str(root), capture_output=True, text=True, timeout=300)
|
|
125
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
126
|
+
return f"ForgeQE: no report ({exc.__class__.__name__}). Build it with: forgeqe report {path}"
|
|
127
|
+
# forgeqe report exits 1 when a test failed: that is a report, not an error.
|
|
128
|
+
line = next((l for l in _clean(run.stdout).splitlines() if l.startswith("Report")), "") if run.returncode <= 1 else ""
|
|
129
|
+
if line:
|
|
130
|
+
return "ForgeQE report: " + line[len("Report"):].strip()
|
|
131
|
+
why = (_clean(run.stderr).strip().splitlines() or ["it did not finish"])[-1]
|
|
132
|
+
return f"ForgeQE: no report ({why}). Build it with: forgeqe report {path}"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
forgeqe_cli/__init__.py,sha256=P8v4jSY_ROSVD98F0zQcv49wFe88He6hTop3NqlqJrg,200
|
|
2
|
+
forgeqe_cli/__main__.py,sha256=frkRQzq1Cd7HI3aiw0W0eJ-w1r8VkXOJrv0wIR-on9Y,46
|
|
3
|
+
forgeqe_cli/launcher.py,sha256=GKmbo1W4xpNz2I3smM0CXGbnAYRdwhrX8Pe48Gr4kgs,6413
|
|
4
|
+
forgeqe_cli/pytest_plugin.py,sha256=f_hH2JXv059oJwHBBdR-TZAhuFYqM2LMr6vAAcRpC5o,5570
|
|
5
|
+
forgeqe_cli-2.2.2.dist-info/licenses/LICENSE,sha256=Z_3_KODt79ZZl4nLelIVBSsVsL6AhEypl7HouMkKjYA,3079
|
|
6
|
+
forgeqe_cli-2.2.2.dist-info/METADATA,sha256=LB2sIwF4wFAVpWvZgpbo42iftHYyk4fXcQDbnDBkQ84,50190
|
|
7
|
+
forgeqe_cli-2.2.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
forgeqe_cli-2.2.2.dist-info/entry_points.txt,sha256=KphefM5TY2MOIbABNg2UJJQsEZUZgrNM4ZD3lrIphVg,139
|
|
9
|
+
forgeqe_cli-2.2.2.dist-info/top_level.txt,sha256=YrCSJdE_QY2GlHw_tgRYsQpZ1cSiIejC9swAUcRLpD8,12
|
|
10
|
+
forgeqe_cli-2.2.2.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
forgeqe_cli/__init__.py,sha256=RuUCEANfbsqtwHS91AF7Tut7cJrNq7Ui3h50bGu6aXk,200
|
|
2
|
-
forgeqe_cli/__main__.py,sha256=frkRQzq1Cd7HI3aiw0W0eJ-w1r8VkXOJrv0wIR-on9Y,46
|
|
3
|
-
forgeqe_cli/launcher.py,sha256=GKmbo1W4xpNz2I3smM0CXGbnAYRdwhrX8Pe48Gr4kgs,6413
|
|
4
|
-
forgeqe_cli-2.2.1.dist-info/licenses/LICENSE,sha256=Z_3_KODt79ZZl4nLelIVBSsVsL6AhEypl7HouMkKjYA,3079
|
|
5
|
-
forgeqe_cli-2.2.1.dist-info/METADATA,sha256=RmpSV-v_5NpO2Gnc-0fi5ur1IUjwAc6vfkWwhZvEmqo,50190
|
|
6
|
-
forgeqe_cli-2.2.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
-
forgeqe_cli-2.2.1.dist-info/entry_points.txt,sha256=1IHZSsCnhKZfoCBsdScd7pRKwRzQ5bruc1Nk_FA5SI4,91
|
|
8
|
-
forgeqe_cli-2.2.1.dist-info/top_level.txt,sha256=YrCSJdE_QY2GlHw_tgRYsQpZ1cSiIejC9swAUcRLpD8,12
|
|
9
|
-
forgeqe_cli-2.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|