correct test №1
This commit is contained in:
parent
4fb1fe532b
commit
23c0c61c5c
File diff suppressed because one or more lines are too long
BIN
testing/charts/report.png
Normal file
BIN
testing/charts/report.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 236 KiB |
@ -1,245 +0,0 @@
|
|||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from collections import Counter, defaultdict
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
import matplotlib
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import matplotlib.patches as mpatches
|
|
||||||
|
|
||||||
matplotlib.use("Agg")
|
|
||||||
|
|
||||||
RESULTS_FILE = Path(__file__).parent.parent / "results.json"
|
|
||||||
CHARTS_DIR = Path(__file__).parent.parent / "charts"
|
|
||||||
|
|
||||||
STATUS_COLORS = {
|
|
||||||
"passed": "#3ddc84",
|
|
||||||
"failed": "#ff5c5c",
|
|
||||||
"skipped": "#e0c84a",
|
|
||||||
"error": "#ff8a8a",
|
|
||||||
}
|
|
||||||
|
|
||||||
BG_COLOR = "#1a1a1a"
|
|
||||||
PANEL_COLOR = "#242424"
|
|
||||||
TEXT_COLOR = "#e0e0e0"
|
|
||||||
GRID_COLOR = "#333333"
|
|
||||||
FONT_MONO = "monospace"
|
|
||||||
|
|
||||||
|
|
||||||
def _style_ax(ax, title: str):
|
|
||||||
ax.set_facecolor(PANEL_COLOR)
|
|
||||||
ax.set_title(title, color=TEXT_COLOR, fontsize=11, fontweight="bold", pad=10)
|
|
||||||
ax.tick_params(colors=TEXT_COLOR, labelsize=8)
|
|
||||||
ax.spines[:].set_color(GRID_COLOR)
|
|
||||||
ax.yaxis.grid(True, color=GRID_COLOR, linewidth=0.5, linestyle="--")
|
|
||||||
ax.set_axisbelow(True)
|
|
||||||
for label in ax.get_xticklabels() + ax.get_yticklabels():
|
|
||||||
label.set_color(TEXT_COLOR)
|
|
||||||
label.set_fontfamily(FONT_MONO)
|
|
||||||
|
|
||||||
|
|
||||||
def load_data() -> tuple[pd.DataFrame, dict]:
|
|
||||||
if not RESULTS_FILE.exists():
|
|
||||||
raise FileNotFoundError(f"results.json not found at {RESULTS_FILE}")
|
|
||||||
|
|
||||||
raw = json.loads(RESULTS_FILE.read_text(encoding="utf-8"))
|
|
||||||
tests = raw.get("tests", [])
|
|
||||||
|
|
||||||
rows = []
|
|
||||||
for t in tests:
|
|
||||||
node = t["nodeid"]
|
|
||||||
parts = node.split("::")
|
|
||||||
module = parts[0].replace("tests/", "").replace("/", ".").replace(".py", "")
|
|
||||||
cls = parts[1] if len(parts) >= 3 else "unknown"
|
|
||||||
name = parts[-1]
|
|
||||||
|
|
||||||
duration = t.get("call", {}).get("duration", 0.0) if t.get("call") else 0.0
|
|
||||||
|
|
||||||
rows.append({
|
|
||||||
"module": module,
|
|
||||||
"class": cls,
|
|
||||||
"name": name,
|
|
||||||
"outcome": t["outcome"],
|
|
||||||
"duration": duration,
|
|
||||||
})
|
|
||||||
|
|
||||||
df = pd.DataFrame(rows)
|
|
||||||
summary = raw.get("summary", {})
|
|
||||||
return df, summary
|
|
||||||
|
|
||||||
|
|
||||||
def chart_overall_status(df: pd.DataFrame, ax: plt.Axes):
|
|
||||||
counts = df["outcome"].value_counts()
|
|
||||||
colors = [STATUS_COLORS.get(k, "#888") for k in counts.index]
|
|
||||||
|
|
||||||
wedges, texts, pcts = ax.pie(
|
|
||||||
counts.values,
|
|
||||||
labels=counts.index,
|
|
||||||
colors=colors,
|
|
||||||
autopct="%1.1f%%",
|
|
||||||
startangle=90,
|
|
||||||
pctdistance=0.78,
|
|
||||||
wedgeprops={"edgecolor": BG_COLOR, "linewidth": 2},
|
|
||||||
)
|
|
||||||
for t in texts:
|
|
||||||
t.set_color(TEXT_COLOR)
|
|
||||||
t.set_fontsize(9)
|
|
||||||
t.set_fontfamily(FONT_MONO)
|
|
||||||
for p in pcts:
|
|
||||||
p.set_color("#111")
|
|
||||||
p.set_fontsize(8)
|
|
||||||
p.set_fontweight("bold")
|
|
||||||
|
|
||||||
ax.set_facecolor(PANEL_COLOR)
|
|
||||||
ax.set_title("Overall Results", color=TEXT_COLOR, fontsize=11, fontweight="bold", pad=10)
|
|
||||||
|
|
||||||
|
|
||||||
def chart_by_module(df: pd.DataFrame, ax: plt.Axes):
|
|
||||||
pivot = (
|
|
||||||
df.groupby(["module", "outcome"])
|
|
||||||
.size()
|
|
||||||
.unstack(fill_value=0)
|
|
||||||
.reindex(columns=["passed", "failed", "skipped", "error"], fill_value=0)
|
|
||||||
)
|
|
||||||
|
|
||||||
x = np.arange(len(pivot))
|
|
||||||
width = 0.2
|
|
||||||
offset = -(len(pivot.columns) - 1) / 2 * width
|
|
||||||
|
|
||||||
for i, col in enumerate(pivot.columns):
|
|
||||||
bars = ax.bar(
|
|
||||||
x + offset + i * width,
|
|
||||||
pivot[col],
|
|
||||||
width,
|
|
||||||
label=col,
|
|
||||||
color=STATUS_COLORS.get(col, "#888"),
|
|
||||||
edgecolor=BG_COLOR,
|
|
||||||
linewidth=0.8,
|
|
||||||
)
|
|
||||||
|
|
||||||
ax.set_xticks(x)
|
|
||||||
ax.set_xticklabels(pivot.index, rotation=25, ha="right", fontsize=7, fontfamily=FONT_MONO)
|
|
||||||
ax.set_ylabel("Tests", color=TEXT_COLOR, fontsize=9)
|
|
||||||
ax.legend(
|
|
||||||
fontsize=7,
|
|
||||||
labelcolor=TEXT_COLOR,
|
|
||||||
facecolor=PANEL_COLOR,
|
|
||||||
edgecolor=GRID_COLOR,
|
|
||||||
)
|
|
||||||
_style_ax(ax, "Results by Module")
|
|
||||||
|
|
||||||
|
|
||||||
def chart_duration_histogram(df: pd.DataFrame, ax: plt.Axes):
|
|
||||||
durations = df.loc[df["outcome"] != "skipped", "duration"].values * 1000
|
|
||||||
|
|
||||||
if len(durations) == 0:
|
|
||||||
ax.text(0.5, 0.5, "No data", ha="center", va="center", color=TEXT_COLOR)
|
|
||||||
_style_ax(ax, "Test Duration (ms)")
|
|
||||||
return
|
|
||||||
|
|
||||||
mean_ms = float(np.mean(durations))
|
|
||||||
median_ms = float(np.median(durations))
|
|
||||||
p95_ms = float(np.percentile(durations, 95))
|
|
||||||
|
|
||||||
ax.hist(durations, bins=20, color="#5cb8ff", edgecolor=BG_COLOR, linewidth=0.6, alpha=0.85)
|
|
||||||
ax.axvline(mean_ms, color="#3ddc84", linewidth=1.5, linestyle="--", label=f"Mean {mean_ms:.1f} ms")
|
|
||||||
ax.axvline(median_ms, color="#e0c84a", linewidth=1.5, linestyle=":", label=f"Median {median_ms:.1f} ms")
|
|
||||||
ax.axvline(p95_ms, color="#ff5c5c", linewidth=1.5, linestyle="-.", label=f"P95 {p95_ms:.1f} ms")
|
|
||||||
|
|
||||||
ax.set_xlabel("ms", color=TEXT_COLOR, fontsize=9)
|
|
||||||
ax.set_ylabel("Tests", color=TEXT_COLOR, fontsize=9)
|
|
||||||
ax.legend(fontsize=7, labelcolor=TEXT_COLOR, facecolor=PANEL_COLOR, edgecolor=GRID_COLOR)
|
|
||||||
_style_ax(ax, "Test Duration (ms)")
|
|
||||||
|
|
||||||
|
|
||||||
def chart_slowest_tests(df: pd.DataFrame, ax: plt.Axes):
|
|
||||||
top = (
|
|
||||||
df[df["outcome"] != "skipped"]
|
|
||||||
.nlargest(10, "duration")
|
|
||||||
.copy()
|
|
||||||
)
|
|
||||||
top["label"] = top["class"] + "::" + top["name"]
|
|
||||||
top["duration"] = top["duration"] * 1000
|
|
||||||
|
|
||||||
colors = [STATUS_COLORS.get(o, "#888") for o in top["outcome"]]
|
|
||||||
bars = ax.barh(top["label"], top["duration"], color=colors, edgecolor=BG_COLOR, linewidth=0.6)
|
|
||||||
|
|
||||||
ax.set_xlabel("ms", color=TEXT_COLOR, fontsize=9)
|
|
||||||
ax.tick_params(axis="y", labelsize=7)
|
|
||||||
ax.invert_yaxis()
|
|
||||||
_style_ax(ax, "Top 10 Slowest Tests")
|
|
||||||
|
|
||||||
|
|
||||||
def chart_stats_table(df: pd.DataFrame, summary: dict, ax: plt.Axes):
|
|
||||||
ax.set_facecolor(PANEL_COLOR)
|
|
||||||
ax.axis("off")
|
|
||||||
|
|
||||||
total = len(df)
|
|
||||||
passed = summary.get("passed", 0)
|
|
||||||
failed = summary.get("failed", 0)
|
|
||||||
skipped = summary.get("skipped", 0)
|
|
||||||
duration = df["duration"].sum() * 1000
|
|
||||||
|
|
||||||
durations = df.loc[df["outcome"] != "skipped", "duration"].values * 1000
|
|
||||||
|
|
||||||
rows = [
|
|
||||||
["Total tests", str(total)],
|
|
||||||
["Passed", str(passed)],
|
|
||||||
["Failed", str(failed)],
|
|
||||||
["Skipped", str(skipped)],
|
|
||||||
["Pass rate", f"{passed / total * 100:.1f}%" if total else "—"],
|
|
||||||
["Total time", f"{duration:.0f} ms"],
|
|
||||||
["Mean duration", f"{np.mean(durations):.1f} ms" if len(durations) else "—"],
|
|
||||||
["Median", f"{np.median(durations):.1f} ms" if len(durations) else "—"],
|
|
||||||
["P95", f"{np.percentile(durations, 95):.1f} ms" if len(durations) else "—"],
|
|
||||||
]
|
|
||||||
|
|
||||||
table = ax.table(
|
|
||||||
cellText=rows,
|
|
||||||
colLabels=["Metric", "Value"],
|
|
||||||
cellLoc="left",
|
|
||||||
loc="center",
|
|
||||||
colWidths=[0.6, 0.4],
|
|
||||||
)
|
|
||||||
table.auto_set_font_size(False)
|
|
||||||
table.set_fontsize(9)
|
|
||||||
|
|
||||||
for (row, col), cell in table.get_celld().items():
|
|
||||||
cell.set_facecolor("#2e2e2e" if row % 2 == 0 else PANEL_COLOR)
|
|
||||||
cell.set_edgecolor(GRID_COLOR)
|
|
||||||
cell.set_text_props(color=TEXT_COLOR, fontfamily=FONT_MONO)
|
|
||||||
|
|
||||||
ax.set_title("Summary", color=TEXT_COLOR, fontsize=11, fontweight="bold", pad=10)
|
|
||||||
|
|
||||||
|
|
||||||
def generate(output_path: Path = None) -> Path:
|
|
||||||
CHARTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
output_path = output_path or CHARTS_DIR / "report.png"
|
|
||||||
|
|
||||||
df, summary = load_data()
|
|
||||||
|
|
||||||
fig = plt.figure(figsize=(18, 14), facecolor=BG_COLOR)
|
|
||||||
fig.suptitle(
|
|
||||||
"Legal AI Assistant — Test Report",
|
|
||||||
fontsize=16, fontweight="bold", color=TEXT_COLOR,
|
|
||||||
y=0.98, fontfamily=FONT_MONO,
|
|
||||||
)
|
|
||||||
|
|
||||||
gs = fig.add_gridspec(2, 3, hspace=0.45, wspace=0.35,
|
|
||||||
top=0.93, bottom=0.05, left=0.06, right=0.97)
|
|
||||||
|
|
||||||
chart_overall_status(df, fig.add_subplot(gs[0, 0]))
|
|
||||||
chart_by_module(df, fig.add_subplot(gs[0, 1:]))
|
|
||||||
chart_duration_histogram(df, fig.add_subplot(gs[1, 0]))
|
|
||||||
chart_slowest_tests(df, fig.add_subplot(gs[1, 1]))
|
|
||||||
chart_stats_table(df, summary, fig.add_subplot(gs[1, 2]))
|
|
||||||
|
|
||||||
fig.savefig(output_path, dpi=150, bbox_inches="tight", facecolor=BG_COLOR)
|
|
||||||
plt.close(fig)
|
|
||||||
return output_path
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
out = generate()
|
|
||||||
print(f"Chart saved: {out}")
|
|
||||||
File diff suppressed because one or more lines are too long
@ -14,12 +14,8 @@ def run_pytest() -> int:
|
|||||||
|
|
||||||
args = [
|
args = [
|
||||||
sys.executable, "-m", "pytest",
|
sys.executable, "-m", "pytest",
|
||||||
|
|
||||||
str(TESTS_DIR / "tests"),
|
str(TESTS_DIR / "tests"),
|
||||||
|
|
||||||
"--json-report",
|
|
||||||
f"--json-report-file={RESULTS_JSON}",
|
|
||||||
|
|
||||||
f"--html={REPORT_HTML}",
|
f"--html={REPORT_HTML}",
|
||||||
"--self-contained-html",
|
"--self-contained-html",
|
||||||
|
|
||||||
@ -29,32 +25,18 @@ def run_pytest() -> int:
|
|||||||
f"--cov-report=html:{CHARTS_DIR / 'coverage'}",
|
f"--cov-report=html:{CHARTS_DIR / 'coverage'}",
|
||||||
|
|
||||||
"--tb=no",
|
"--tb=no",
|
||||||
|
"-q",
|
||||||
]
|
]
|
||||||
|
|
||||||
return subprocess.run(args, cwd=str(ROOT), check=False).returncode
|
return subprocess.run(args, cwd=str(ROOT), check=False).returncode
|
||||||
|
|
||||||
|
|
||||||
def run_charts():
|
|
||||||
from testing.reports.charts import generate
|
|
||||||
return generate()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print("Start testing")
|
print("Start testing")
|
||||||
|
|
||||||
code = run_pytest()
|
code = run_pytest()
|
||||||
|
|
||||||
charts_path = None
|
charts_path = None
|
||||||
|
print(f"\npytest-html: {REPORT_HTML}")
|
||||||
if RESULTS_JSON.exists():
|
|
||||||
charts_path = run_charts()
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(f"pytest-html: {REPORT_HTML}")
|
|
||||||
print(f"pytest-cov: {CHARTS_DIR / 'coverage' / 'index.html'}")
|
print(f"pytest-cov: {CHARTS_DIR / 'coverage' / 'index.html'}")
|
||||||
print(f"charts: {charts_path or CHARTS_DIR / 'report.png'}")
|
|
||||||
|
|
||||||
print()
|
print("\nStop testing")
|
||||||
print("Stop testing")
|
|
||||||
|
|
||||||
sys.exit(code)
|
sys.exit(code)
|
||||||
Loading…
Reference in New Issue
Block a user