Skip to content

Commit 2820960

Browse files
Copilotdev-ankit
andcommitted
Add markdown export with emoji indicators
Co-authored-by: dev-ankit <1901680+dev-ankit@users.noreply.github.com>
1 parent 0f9f2ea commit 2820960

3 files changed

Lines changed: 366 additions & 33 deletions

File tree

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Compare performance results between two Locust runs and show changes relative to
77
- Compare any two runs (base vs. current).
88
- Parses CSV `report.csv` for aggregated and per-endpoint metrics.
99
- Parses per-feature `.html` pages and compares the latest history sample.
10-
- Outputs human-readable tables or machine-friendly JSON.
10+
- Outputs human-readable tables, markdown with emoji indicators, or machine-friendly JSON.
1111

1212
## Requirements
1313

@@ -39,6 +39,12 @@ python3 compare_runs.py test_runs/HTML-Report-292 test_runs/HTML-Report-294 --js
3939
python3 compare_runs.py test_runs/HTML-Report-292 test_runs/HTML-Report-294 --color
4040
```
4141

42+
- Markdown output with emoji indicators (✅ better, ❌ worse, ➖ same):
43+
44+
```
45+
python3 compare_runs.py test_runs/HTML-Report-292 test_runs/HTML-Report-294 --markdown
46+
```
47+
4248
Exit code is `0` on success and `1` on error.
4349

4450
## What It Compares
@@ -61,6 +67,28 @@ If a metric is not available for an item, it is shown as `-`.
6167
<img width="598" height="255" alt="image" src="https://github.com/user-attachments/assets/f5394045-6d1e-498e-aa3f-624928ec70a7" />
6268

6369

70+
## Markdown Output Example
71+
72+
The `--markdown` flag produces markdown tables with emoji indicators for verdicts:
73+
74+
```markdown
75+
## Aggregated
76+
77+
| Metric | Base | Current | Diff | % Change | Verdict |
78+
| --- | --- | --- | --- | --- | --- |
79+
| Requests/s | 286.200 | 300 | +13.800 | +4.8% ||
80+
| Request Count | 1500 | 1800 | +300 | +20.0% ||
81+
| Failure Count | 7 | 4 | -3 | -42.9% ||
82+
| Average Response Time | 85.200 | 78.500 | -6.700 | -7.9% ||
83+
| 95% | 150 | 140 | -10 | -6.7% ||
84+
```
85+
86+
Verdict emojis:
87+
- ✅ Better performance
88+
- ❌ Worse performance
89+
- ➖ No change
90+
91+
6492
## JSON Schema
6593

6694
The `--json` output is a single JSON object containing keys for each compared item.

compare_runs.py

Lines changed: 150 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,13 @@ def print_section(title: str):
333333
print("-" * len(title))
334334

335335

336+
def print_section_markdown(title: str, level: int = 2):
337+
"""Print a markdown section header."""
338+
print("")
339+
print("#" * level + " " + title)
340+
print("")
341+
342+
336343
def _metric_direction(metric: str) -> str:
337344
"""Return 'higher', 'lower', or 'neutral' for a metric's desirable direction.
338345
@@ -365,6 +372,70 @@ def _verdict_for(metric: str, b: Optional[float], c: Optional[float]) -> Optiona
365372
return None
366373

367374

375+
def _verdict_to_emoji(verdict: Optional[str]) -> str:
376+
"""Convert verdict to emoji for markdown output."""
377+
if verdict == "better":
378+
return "✅"
379+
elif verdict == "worse":
380+
return "❌"
381+
elif verdict == "same":
382+
return "➖"
383+
return ""
384+
385+
386+
def render_comparison_markdown(
387+
base_row: Optional[Row],
388+
curr_row: Optional[Row],
389+
important_fields: List[str],
390+
*,
391+
show_verdict: bool = True,
392+
):
393+
"""Render comparison as markdown table with emoji indicators."""
394+
headers = [
395+
"Metric",
396+
"Base",
397+
"Current",
398+
"Diff",
399+
"% Change",
400+
]
401+
if show_verdict:
402+
headers.append("Verdict")
403+
rows: List[List[str]] = []
404+
405+
base_data = base_row.data if base_row else {}
406+
curr_data = curr_row.data if curr_row else {}
407+
408+
fields = important_fields[:]
409+
# Also include any extra percentile columns present in data
410+
extra_fields = [k for k in curr_data.keys() | base_data.keys() if k.endswith("%") and k not in fields]
411+
fields.extend(sorted(extra_fields))
412+
413+
for field in fields:
414+
b = base_data.get(field)
415+
c = curr_data.get(field)
416+
d = diff(b, c)
417+
p = pct_change(b, c)
418+
p_str = "-" if p is None else f"{p:+.1f}%"
419+
row = [
420+
field,
421+
format_number(b),
422+
format_number(c),
423+
("-" if d is None else (f"{d:+.3f}" if abs(d - round(d)) > 1e-9 else f"{int(d):+d}")),
424+
p_str,
425+
]
426+
if show_verdict:
427+
v = _verdict_for(field, b, c)
428+
emoji = _verdict_to_emoji(v)
429+
row.append(emoji)
430+
rows.append(row)
431+
432+
# Print markdown table
433+
print("| " + " | ".join(headers) + " |")
434+
print("| " + " | ".join(["---"] * len(headers)) + " |")
435+
for r in rows:
436+
print("| " + " | ".join(r) + " |")
437+
438+
368439
def render_comparison(
369440
base_row: Optional[Row],
370441
curr_row: Optional[Row],
@@ -437,6 +508,7 @@ def compare_reports(
437508
*,
438509
colorize: bool = False,
439510
show_verdict: bool = True,
511+
as_markdown: bool = False,
440512
) -> int:
441513
# Resolve paths (extract zip files if needed)
442514
base_path = _resolve_path(base_path)
@@ -517,45 +589,85 @@ def compare_reports(
517589
return 0
518590

519591
# Human readable output
520-
print_section("Aggregated")
521-
render_comparison(
522-
base_idx.get("__Aggregated__"),
523-
curr_idx.get("__Aggregated__"),
524-
important_fields,
525-
colorize=colorize,
526-
show_verdict=show_verdict,
527-
)
592+
if as_markdown:
593+
print("# Locust Performance Comparison")
594+
print("")
595+
print_section_markdown("Aggregated", 2)
596+
render_comparison_markdown(
597+
base_idx.get("__Aggregated__"),
598+
curr_idx.get("__Aggregated__"),
599+
important_fields,
600+
show_verdict=show_verdict,
601+
)
528602

529-
endpoint_keys = [k for k in all_keys if k != "__Aggregated__"]
530-
for ek in endpoint_keys:
531-
title = f"Endpoint: {ek}"
532-
print_section(title)
603+
endpoint_keys = [k for k in all_keys if k != "__Aggregated__"]
604+
for ek in endpoint_keys:
605+
title = f"Endpoint: {ek}"
606+
print_section_markdown(title, 3)
607+
render_comparison_markdown(
608+
base_idx.get(ek),
609+
curr_idx.get(ek),
610+
important_fields,
611+
show_verdict=show_verdict,
612+
)
613+
614+
# Render HTML features
615+
feature_keys = sorted(set(base_html_map.keys()) | set(curr_html_map.keys()))
616+
if feature_keys:
617+
print_section_markdown("HTML Features", 2)
618+
for fk in feature_keys:
619+
print_section_markdown(f"Feature: {fk}", 3)
620+
b_map = base_html_map.get(fk, {})
621+
c_map = curr_html_map.get(fk, {})
622+
ep_keys = sorted(set(b_map.keys()) | set(c_map.keys()))
623+
for ep in ep_keys:
624+
print_section_markdown(f"Endpoint: {ep}", 4)
625+
render_comparison_markdown(
626+
b_map.get(ep),
627+
c_map.get(ep),
628+
important_fields,
629+
show_verdict=show_verdict,
630+
)
631+
else:
632+
print_section("Aggregated")
533633
render_comparison(
534-
base_idx.get(ek),
535-
curr_idx.get(ek),
634+
base_idx.get("__Aggregated__"),
635+
curr_idx.get("__Aggregated__"),
536636
important_fields,
537637
colorize=colorize,
538638
show_verdict=show_verdict,
539639
)
540640

541-
# Render HTML features
542-
feature_keys = sorted(set(base_html_map.keys()) | set(curr_html_map.keys()))
543-
if feature_keys:
544-
print_section("HTML Features")
545-
for fk in feature_keys:
546-
print_section(f"Feature: {fk}")
547-
b_map = base_html_map.get(fk, {})
548-
c_map = curr_html_map.get(fk, {})
549-
ep_keys = sorted(set(b_map.keys()) | set(c_map.keys()))
550-
for ep in ep_keys:
551-
print_section(f"Endpoint: {ep}")
552-
render_comparison(
553-
b_map.get(ep),
554-
c_map.get(ep),
555-
important_fields,
556-
colorize=colorize,
557-
show_verdict=show_verdict,
558-
)
641+
endpoint_keys = [k for k in all_keys if k != "__Aggregated__"]
642+
for ek in endpoint_keys:
643+
title = f"Endpoint: {ek}"
644+
print_section(title)
645+
render_comparison(
646+
base_idx.get(ek),
647+
curr_idx.get(ek),
648+
important_fields,
649+
colorize=colorize,
650+
show_verdict=show_verdict,
651+
)
652+
653+
# Render HTML features
654+
feature_keys = sorted(set(base_html_map.keys()) | set(curr_html_map.keys()))
655+
if feature_keys:
656+
print_section("HTML Features")
657+
for fk in feature_keys:
658+
print_section(f"Feature: {fk}")
659+
b_map = base_html_map.get(fk, {})
660+
c_map = curr_html_map.get(fk, {})
661+
ep_keys = sorted(set(b_map.keys()) | set(c_map.keys()))
662+
for ep in ep_keys:
663+
print_section(f"Endpoint: {ep}")
664+
render_comparison(
665+
b_map.get(ep),
666+
c_map.get(ep),
667+
important_fields,
668+
colorize=colorize,
669+
show_verdict=show_verdict,
670+
)
559671

560672
return 0
561673

@@ -570,6 +682,11 @@ def main():
570682
parser.add_argument("base", type=Path, help="Base run directory or report.csv path")
571683
parser.add_argument("current", type=Path, help="Current run directory or report.csv path")
572684
parser.add_argument("--json", action="store_true", help="Output results as JSON")
685+
parser.add_argument(
686+
"--markdown",
687+
action="store_true",
688+
help="Output results as Markdown with emoji indicators (✅ better, ❌ worse, ➖ same)",
689+
)
573690
parser.add_argument(
574691
"--color",
575692
action="store_true",
@@ -590,6 +707,7 @@ def main():
590707
as_json=args.json,
591708
colorize=args.color,
592709
show_verdict=args.show_verdict,
710+
as_markdown=args.markdown,
593711
)
594712
except Exception as e:
595713
print(f"Error: {e}")

0 commit comments

Comments
 (0)