|
| 1 | +import os |
| 2 | +import sys |
| 3 | +import json |
| 4 | +import re |
| 5 | + |
| 6 | +def artifact_count(count, icon): |
| 7 | + """ |
| 8 | + Formats a count and an icon for display. If count is 0, |
| 9 | + it returns an empty string for the count and replaces the icon with :new_moon:. |
| 10 | + """ |
| 11 | + if count == 0: |
| 12 | + count = "" |
| 13 | + icon = ":new_moon:" |
| 14 | + |
| 15 | + return f"<code>{count:>3}</code>{icon}" |
| 16 | + |
| 17 | +def example_name(sketch_name): |
| 18 | + match = re.search(r'libraries/([^/]+)/(.*)', sketch_name) |
| 19 | + if match: |
| 20 | + lib = match.group(1) |
| 21 | + sketch = match.group(2) |
| 22 | + return f"<code>{lib}</code> <code>{sketch}</code>" |
| 23 | + else: |
| 24 | + return f"<code>{sketch_name}</code>" |
| 25 | + |
| 26 | +TEST_MATRIX = {} |
| 27 | +BOARD_SUMMARY = {} |
| 28 | + |
| 29 | +def log_test(name, board, icon, issues, job_link=None): |
| 30 | + """ |
| 31 | + Logs individual test results into the TEST_MATRIX dictionary. |
| 32 | + """ |
| 33 | + |
| 34 | + if not board in BOARD_SUMMARY: |
| 35 | + BOARD_SUMMARY[board] = { |
| 36 | + 'tests': 0, |
| 37 | + 'errors': 0, |
| 38 | + 'warnings': 0, |
| 39 | + 'job_link': job_link, |
| 40 | + 'icon': ':white_check_mark:' |
| 41 | + } |
| 42 | + |
| 43 | + board_info = BOARD_SUMMARY[board] |
| 44 | + |
| 45 | + if isinstance(issues, str): |
| 46 | + issues = [ issues ] |
| 47 | + |
| 48 | + board_info['tests'] += 1 |
| 49 | + if icon == ":fire:": |
| 50 | + board_info['icon'] = ":fire:" |
| 51 | + elif icon == ":red_circle:": |
| 52 | + board_info['icon'] = ":x:" |
| 53 | + board_info['errors'] += 1 |
| 54 | + elif icon == ":yellow_circle:": |
| 55 | + board_info['warnings'] += 1 |
| 56 | + # Only set to warning if not already an error |
| 57 | + if not board_info['icon']: |
| 58 | + board_info['icon'] = ":white_check_mark::grey_exclamation:" |
| 59 | + |
| 60 | + if name not in TEST_MATRIX: |
| 61 | + TEST_MATRIX[name] = {} |
| 62 | + |
| 63 | + TEST_MATRIX[name][board] = { |
| 64 | + 'icon': icon, |
| 65 | + 'issues': issues |
| 66 | + } |
| 67 | + |
| 68 | +# --- Main Logic --- |
| 69 | + |
| 70 | +def process_build_reports(): |
| 71 | + """ |
| 72 | + The main script logic, converting the Bash loop structures. |
| 73 | + """ |
| 74 | + # 1. Environment Variable Checks |
| 75 | + ALL_BOARD_DATA_STR = os.environ.get('ALL_BOARD_DATA') |
| 76 | + GITHUB_REPOSITORY = os.environ.get('GITHUB_REPOSITORY') |
| 77 | + GITHUB_RUN_ID = os.environ.get('GITHUB_RUN_ID') |
| 78 | + |
| 79 | + if not ALL_BOARD_DATA_STR or not GITHUB_REPOSITORY or not GITHUB_RUN_ID: |
| 80 | + print("Not in a Github CI run, cannot proceed.") |
| 81 | + sys.exit(0) |
| 82 | + |
| 83 | + ALL_BOARD_DATA = json.loads(ALL_BOARD_DATA_STR) |
| 84 | + |
| 85 | + for board_data in ALL_BOARD_DATA: |
| 86 | + # Extract common fields |
| 87 | + board = board_data.get('board') |
| 88 | + variant = board_data.get('variant') |
| 89 | + subarch = board_data.get('subarch') |
| 90 | + |
| 91 | + # Filename preparation |
| 92 | + REPORT_FILE = f"arduino-{subarch}-{board}.json" |
| 93 | + |
| 94 | + # 5. Report File Check |
| 95 | + if not os.path.exists(REPORT_FILE): |
| 96 | + log_test('CI test', board, ":fire:", "Report file not found.") |
| 97 | + continue # Skip to the next board |
| 98 | + |
| 99 | + # 6. Process Report File |
| 100 | + try: |
| 101 | + with open(REPORT_FILE, 'r') as f: |
| 102 | + report_data = json.load(f) |
| 103 | + except Exception as e: |
| 104 | + log_test('CI test', board, ":fire:", f"Error reading report file: {e}") |
| 105 | + continue # Skip to the next board |
| 106 | + |
| 107 | + # Extract data from the report file |
| 108 | + job_id = report_data.get('job_id') |
| 109 | + job_link = f"https://github.com/{GITHUB_REPOSITORY}/actions/runs/{GITHUB_RUN_ID}/job/{job_id}#step:5" if job_id else None |
| 110 | + |
| 111 | + reports = report_data.get('boards', [{}])[0].get('sketches', []) |
| 112 | + if not reports: |
| 113 | + log_test('CI test', board, ":fire:", "Test report is empty, check full log.", job_link) |
| 114 | + continue # Skip to the next board |
| 115 | + |
| 116 | + # 7. Sketch Loop: Iterate through individual sketch reports |
| 117 | + for report in reports: |
| 118 | + SKETCH_NAME = report.get('name', 'unknown_sketch') |
| 119 | + compilation_success = report.get('compilation_success', False) |
| 120 | + issues = report.get('issues', []) |
| 121 | + |
| 122 | + # Replace long absolute paths with '...' for brevity. |
| 123 | + sketch_issues = [ re.sub(r'(/.+?)((/[^/]+){3}):', r'...\2:', issue) for issue in issues ] |
| 124 | + |
| 125 | + # Logic to update counters and DETAILS string |
| 126 | + if not compilation_success: |
| 127 | + test_icon = ":red_circle:" |
| 128 | + elif len(sketch_issues): # Implies warnings/non-critical issues |
| 129 | + test_icon = ":yellow_circle:" |
| 130 | + else: |
| 131 | + test_icon = ":green_circle:" |
| 132 | + |
| 133 | + log_test(SKETCH_NAME, board, test_icon, sketch_issues, job_link) |
| 134 | + |
| 135 | + if not BOARD_SUMMARY[board]['icon']: |
| 136 | + # If no errors, set to green |
| 137 | + BOARD_SUMMARY[board]['icon'] = ":white_check_mark:" |
| 138 | + |
| 139 | + artifacts = set(item['artifact'] for item in ALL_BOARD_DATA) |
| 140 | + |
| 141 | + # Print the recap table |
| 142 | + for artifact in sorted(list(artifacts), reverse=True): |
| 143 | + print(f"### `{artifact}` test results:") |
| 144 | + |
| 145 | + # 4. Inner Loop: Filter board data for the current artifact |
| 146 | + artifact_boards = [item for item in ALL_BOARD_DATA if item['artifact'] == artifact] |
| 147 | + |
| 148 | + print() |
| 149 | + |
| 150 | + # Print the test matrix |
| 151 | + |
| 152 | + header_row = "<tr><th colspan=2>Sketch / Board</th>" |
| 153 | + for board_data in artifact_boards: |
| 154 | + board = board_data['board'] |
| 155 | + icon = BOARD_SUMMARY[board]['icon'] |
| 156 | + header_col = f"<code>{board}</code>" |
| 157 | + if BOARD_SUMMARY[board]['job_link']: |
| 158 | + link = BOARD_SUMMARY[board]['job_link'] |
| 159 | + header_col = f"<a href='{link}'>{header_col}</a>" |
| 160 | + header_col += f"<br/>{icon}" |
| 161 | + header_row += f"<th>{header_col}</th>" |
| 162 | + header_row += "</tr>" |
| 163 | + |
| 164 | + print("<table>") |
| 165 | + print(header_row) |
| 166 | + |
| 167 | + for sketch_name, board_results in TEST_MATRIX.items(): |
| 168 | + sketch_icon = None |
| 169 | + row_data = "" |
| 170 | + for board_data in artifact_boards: |
| 171 | + board_name = board_data['board'] |
| 172 | + test_result = board_results.get(board_name, {}) |
| 173 | + icon = test_result['icon'] if test_result else ":new_moon:" |
| 174 | + if icon == ":fire:" or icon == ":red_circle:": |
| 175 | + sketch_icon = icon |
| 176 | + elif icon == ":yellow_circle:" and not sketch_icon: |
| 177 | + sketch_icon = icon |
| 178 | + issues = test_result['issues'] if test_result else "" |
| 179 | + sketch_id = sketch_name.replace('/', '_').replace(' ', '_').replace('-', '_') |
| 180 | + icon_link = f"<a href='#{sketch_id}_{board_name}'>{icon}</a>" if issues else icon |
| 181 | + row_data += f"<td align='center'>{icon_link}</a></td>" |
| 182 | + if not sketch_icon: |
| 183 | + sketch_icon = ":green_circle:" |
| 184 | + print(f"<tr><td>{sketch_icon}</td><td><code>{example_name(sketch_name)}</code></td>{row_data}</tr>") |
| 185 | + print("</table>\n") |
| 186 | + |
| 187 | + print(f"<details><summary><b>Error and warning logs for <code>{artifact}</code></b></summary>") |
| 188 | + print("<table>") |
| 189 | + print("<tr><th>Sketch</th><th>Build Details</th></tr>") |
| 190 | + for sketch_name, board_results in TEST_MATRIX.items(): |
| 191 | + row_open = False |
| 192 | + for board_data in artifact_boards: |
| 193 | + board_name = board_data['board'] |
| 194 | + if not board_name in board_results: |
| 195 | + continue |
| 196 | + |
| 197 | + test_result = board_results[board_name] |
| 198 | + icon = test_result['icon'] |
| 199 | + issues = test_result['issues'] |
| 200 | + if not issues: |
| 201 | + continue |
| 202 | + |
| 203 | + if not row_open: |
| 204 | + row_open = True |
| 205 | + print(f"<tr><td>{example_name(sketch_name)}</td>") |
| 206 | + print("<td>") |
| 207 | + |
| 208 | + job_link = BOARD_SUMMARY[board]['job_link'] |
| 209 | + if job_link: |
| 210 | + job_link = f" <a href='{job_link}'> :scroll:</a>" |
| 211 | + sketch_id = sketch_name.replace('/', '_').replace(' ', '_').replace('-', '_') |
| 212 | + print(f"<details id='{sketch_id}_{board_name}'><summary>{artifact_count(len(issues), icon)} <code>{board_name}</code>{job_link}</summary>") |
| 213 | + print("\n```\n" + "\n".join(issues) + "\n```") |
| 214 | + print("</details>") |
| 215 | + |
| 216 | + if row_open: |
| 217 | + print("</td></tr>") |
| 218 | + print("</table></details>\n") |
| 219 | + |
| 220 | +if __name__ == "__main__": |
| 221 | + process_build_reports() |
0 commit comments