@@ -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+
336343def _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+
368439def 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