1+ #!/usr/bin/env python3
2+
3+ """
4+ Google Test to Markdown Converter
5+
6+ This script converts Google Test (gtest) JSON output into GitHub-flavored markdown format.
7+ It processes test results and generates a well-formatted markdown document that includes:
8+ - A timestamped header
9+ - Summary statistics (total, passed, and failed tests)
10+ - A detailed table of test results
11+ - Failure details for any failed tests
12+
13+ The script is designed to be used as a command-line tool, taking two arguments:
14+ 1. context: A string describing the test run context
15+ 2. gtest_json_file: Path to the JSON file containing gtest results
16+
17+ Example usage:
18+ python gtest_to_markdown.py "Unit Tests" test_results.json
19+
20+ The output is formatted markdown that can be directly used in GitHub issues, pull requests,
21+ or documentation.
22+ """
23+
24+ import json
25+ import os
26+ import sys
27+ from datetime import datetime
28+ from typing import Dict , List , Any
29+
30+ def format_duration (milliseconds : float ) -> str :
31+ """
32+ Convert milliseconds to a human-readable duration string.
33+
34+ Args:
35+ milliseconds (float): Duration in milliseconds
36+
37+ Returns:
38+ str: Human-readable duration string in the format:
39+ - "X.XXms" for durations < 1 second
40+ - "X.XXs" for durations < 1 minute
41+ - "X.XXm" for durations >= 1 minute
42+
43+ Example:
44+ >>> format_duration(500)
45+ '500.00ms'
46+ >>> format_duration(1500)
47+ '1.50s'
48+ """
49+ if milliseconds < 1000 :
50+ return f"{ milliseconds :.2f} ms"
51+ seconds = milliseconds / 1000
52+ if seconds < 60 :
53+ return f"{ seconds :.2f} s"
54+ minutes = seconds / 60
55+ return f"{ minutes :.2f} m"
56+
57+ def parse_duration (time_value : Any ) -> float :
58+ """
59+ Parse a duration value from gtest output into milliseconds.
60+
61+ Args:
62+ time_value (Any): The duration value from gtest, which could be:
63+ - A float (milliseconds)
64+ - A string ending in 's' (seconds)
65+ - Any other value that should be converted to float
66+
67+ Returns:
68+ float: Duration in milliseconds
69+
70+ Example:
71+ >>> parse_duration(500)
72+ 500.0
73+ >>> parse_duration("1.5s")
74+ 1500.0
75+ """
76+ try :
77+ if isinstance (time_value , str ) and time_value .endswith ('s' ):
78+ return float (time_value [:- 1 ]) * 1000 # Convert seconds to milliseconds
79+ return float (time_value )
80+ except (ValueError , TypeError ):
81+ return 0.0 # Return 0 for invalid values
82+
83+ def format_failure_message (failure : Dict [str , Any ]) -> str :
84+ """
85+ Format a test failure message from the gtest JSON output.
86+
87+ Args:
88+ failure (Dict[str, Any]): A dictionary containing failure information
89+ with optional keys:
90+ - failure: The failure message
91+ - type: The type of failure
92+
93+ Returns:
94+ str: The message with all newlines replaced with "<br/>"
95+
96+ Example:
97+ >>> failure = {"failure": "Expected 2\n but got 3"}
98+ >>> format_failure_message(failure)
99+ 'Expected 2<br/>but got 3'
100+ """
101+ if "failure" in failure :
102+ return "<pre>" + failure ["failure" ].replace ("\n " , "<br/>" ) + "</pre>"
103+ return ""
104+
105+ def convert_to_markdown (data : Dict [str , Any ], context : str ) -> str :
106+ """
107+ Convert gtest JSON data to GitHub-flavored markdown.
108+
109+ This function processes the gtest JSON output and generates a comprehensive
110+ markdown document that includes test results, statistics, and failure details.
111+
112+ Args:
113+ data (Dict[str, Any]): The parsed JSON data from gtest output
114+ context (str): A string describing the context of the test run
115+ (e.g., "Unit Tests", "Integration Tests")
116+
117+ Returns:
118+ str: A complete markdown document containing:
119+ - Header with timestamp
120+ - Summary statistics
121+ - Detailed test results table
122+ - Failure details for failed tests
123+
124+ The output markdown includes:
125+ - A table with columns: Test Suite, Test Case, Status, Duration
126+ - Emoji indicators (✅ for pass, ❌ for fail)
127+ - Formatted duration strings
128+ - Code blocks for failure messages
129+ """
130+ output = []
131+
132+ # Add header with timestamp
133+ timestamp = datetime .now ().strftime ("%Y-%m-%d %H:%M:%S" )
134+ output .append (f"# { context } Results ({ timestamp } )\n " )
135+
136+ # Add summary
137+ total_tests = data .get ("tests" , 0 )
138+ failed_tests = data .get ("failures" , 0 )
139+ disabled_tests = data .get ("disabled" , 0 )
140+ error_tests = data .get ("errors" , 0 )
141+ tests_duration = format_duration (parse_duration (data .get ("time" , 0 )))
142+ passed_tests = total_tests - failed_tests
143+
144+ output .append ("## Summary\n " )
145+ output .append (f"- Tests: { total_tests } " )
146+ output .append (f"- Passed: { passed_tests } " )
147+ output .append (f"- Failed: { failed_tests } " )
148+ output .append (f"- Disabled: { disabled_tests } " )
149+ output .append (f"- Errors: { error_tests } " )
150+ output .append (f"- Duration: { tests_duration } \n " )
151+
152+ # Add detailed results table
153+ output .append ("## Details\n " )
154+ output .append ("| Suite | Case | Status | Duration | Details |" )
155+ output .append ("|-------|------|--------|----------|---------|" )
156+
157+ for suite in data .get ("testsuites" , []):
158+ suite_name = suite .get ("name" , "Unknown Suite" )
159+ for test in suite .get ("testsuite" , []):
160+ test_name = test .get ("name" , "Unknown Test" )
161+ status = "❌ FAIL" if "failures" in test else "✅ PASS"
162+ duration = format_duration (parse_duration (test .get ("time" , 0 )))
163+ details = []
164+
165+ # Add failure details if the test failed
166+ if "failures" in test :
167+ for failure in test ["failures" ]:
168+ details .append (format_failure_message (failure ))
169+
170+ # Add the test result row
171+ output .append (f"| { suite_name } | { test_name } | { status } | { duration } | { '<br/>' .join (details )} " )
172+
173+ return "\n " .join (output )
174+
175+ def main ():
176+ """
177+ Main entry point for the script.
178+
179+ Processes command line arguments and converts gtest JSON output to markdown.
180+ The script expects two arguments:
181+ 1. context: A string describing the test run context
182+ 2. gtest_json_file: Path to the JSON file containing gtest results
183+
184+ The script will:
185+ - Read and parse the JSON file
186+ - Convert the data to markdown format
187+ - Print the markdown to stdout
188+
189+ Exits with status code 1 if:
190+ - Incorrect number of arguments
191+ - File not found
192+ - Invalid JSON
193+ - Any other error occurs
194+ """
195+ if len (sys .argv ) != 3 :
196+ print ("Usage: python gtest_to_markdown.py <context> <gtest_json_file>" )
197+ sys .exit (1 )
198+
199+ context = sys .argv [1 ]
200+ json_file = sys .argv [2 ]
201+
202+ try :
203+ with open (json_file , 'r' ) as f :
204+ data = json .load (f )
205+ markdown = convert_to_markdown (data , context )
206+ print (markdown )
207+ except FileNotFoundError :
208+ print (f"Error: File { json_file } not found" , file = sys .stderr )
209+ sys .exit (1 )
210+ except json .JSONDecodeError :
211+ print (f"Error: Invalid JSON in { json_file } " , file = sys .stderr )
212+ sys .exit (1 )
213+ except Exception as e :
214+ print (f"Error: { str (e )} " , file = sys .stderr )
215+ sys .exit (1 )
216+
217+ if __name__ == "__main__" :
218+ main ()
0 commit comments