5_๐_Code_Coverage.py
1 """ 2 333 Method Analytics Dashboard - Code Coverage Page 3 """ 4 5 import logging 6 import os 7 import streamlit as st 8 import streamlit.components.v1 as components 9 from streamlit_autorefresh import st_autorefresh 10 from dashboard.utils import file_readers 11 from dashboard.components import charts 12 from dashboard.utils.logging_config import configure_app_logging, log_exception 13 from dashboard.utils.coverage_server import get_coverage_server 14 15 # Configure logging for this page 16 logger = configure_app_logging("dashboard.coverage") 17 18 # Page configuration 19 st.set_page_config(page_title="Code Coverage", page_icon="๐", layout="wide") 20 21 # Auto-refresh 22 st_autorefresh(interval=config.REFRESH_INTERVAL * 1000, key="coverage_refresh") 23 24 st.title("๐ Code Coverage & Quality") 25 26 # === Code Coverage === 27 st.subheader("๐ Code Coverage") 28 29 try: 30 coverage_data = file_readers.get_coverage_data() 31 32 if coverage_data and "total" in coverage_data: 33 total_cov = coverage_data["total"] 34 35 # Helper function to format percentage safely 36 def format_pct(pct_value): 37 if isinstance(pct_value, (int, float)): 38 return f"{pct_value:.1f}%" 39 return str(pct_value) 40 41 # Coverage metrics 42 cov_cols = st.columns(4) 43 with cov_cols[0]: 44 st.metric("Lines", format_pct(total_cov["lines"]["pct"])) 45 with cov_cols[1]: 46 st.metric("Statements", format_pct(total_cov["statements"]["pct"])) 47 with cov_cols[2]: 48 st.metric("Functions", format_pct(total_cov["functions"]["pct"])) 49 with cov_cols[3]: 50 st.metric("Branches", format_pct(total_cov["branches"]["pct"])) 51 52 # Gauge chart for overall coverage (only if numeric) 53 overall_pct = total_cov["lines"]["pct"] 54 if isinstance(overall_pct, (int, float)): 55 fig = charts.create_gauge_chart( 56 overall_pct, 57 "Overall Code Coverage", 58 100, 59 {"good": 80, "warning": 70, "critical": 0}, 60 ) 61 st.plotly_chart(fig, use_container_width=True) 62 63 # Coverage alert 64 if overall_pct < 70: 65 st.warning(f"โ ๏ธ Coverage is {overall_pct:.1f}% (target: 70%+)") 66 elif overall_pct >= 80: 67 st.success(f"โ Excellent coverage: {overall_pct:.1f}%") 68 else: 69 st.info(f"Coverage: {overall_pct:.1f}% (target: 80%+)") 70 else: 71 st.info( 72 f"๐ Coverage: {overall_pct} - Run `npm test` to generate coverage data" 73 ) 74 75 st.markdown("---") 76 77 # === Detailed Istanbul Report Breakdown === 78 st.subheader("๐ Detailed Istanbul Coverage Report") 79 80 # Prominent link to full HTML report 81 # Use the actual project directory, not the SyncThing symlink 82 cov_path = "/home/jason/code/333Method/coverage/index.html" 83 84 # Check if file exists and show file path 85 if os.path.exists(cov_path): 86 file_mtime = os.path.getmtime(cov_path) 87 from datetime import datetime 88 last_updated = datetime.fromtimestamp(file_mtime).strftime("%Y-%m-%d %H:%M:%S") 89 90 # Start coverage server (singleton, only starts once) 91 try: 92 coverage_server = get_coverage_server() 93 if not coverage_server.is_running(): 94 coverage_server.start("/home/jason/code/333Method/coverage") 95 coverage_url = coverage_server.get_url("index.html") 96 except Exception as e: 97 st.error(f"Failed to start coverage server: {e}") 98 coverage_url = f"file://{cov_path}" 99 100 # Show expandable iframe with the full HTML report (expanded by default) 101 with st.expander("๐ **View Interactive HTML Coverage Report**", expanded=True): 102 st.caption(f"๐ Last Updated: {last_updated} | ๐ Path: `{cov_path}`") 103 104 # Use iframe to display the actual coverage report via HTTP 105 st.markdown( 106 f'<iframe src="{coverage_url}" width="100%" height="800" frameborder="0" style="border: 1px solid #ddd; border-radius: 4px;"></iframe>', 107 unsafe_allow_html=True 108 ) 109 110 st.caption(f"๐ก **Serving from:** {coverage_url} | Alternative: `open coverage/index.html`") 111 else: 112 st.warning(f""" 113 โ ๏ธ **Coverage HTML report not found** 114 115 Expected path: `{cov_path}` 116 117 Run `npm test` to generate the interactive HTML coverage report. 118 """) 119 120 st.write("") 121 122 # Extract per-file data (only if coverage is numeric) 123 file_coverage = [] 124 125 # Get project root to strip from file paths 126 # Use the actual project directory, not the SyncThing symlink 127 project_root = "/home/jason/code/333Method" 128 129 for file_path, cov in coverage_data.items(): 130 if file_path != "total" and isinstance( 131 cov["lines"]["pct"], (int, float) 132 ): 133 # Strip project root from file path for cleaner display 134 display_path = file_path 135 if file_path.startswith(project_root): 136 display_path = file_path[len(project_root):].lstrip("/") 137 138 file_coverage.append( 139 { 140 "file": display_path, 141 "lines_pct": cov["lines"]["pct"], 142 "lines_covered": cov["lines"]["covered"], 143 "lines_total": cov["lines"]["total"], 144 "statements_pct": cov["statements"]["pct"], 145 "functions_pct": cov["functions"]["pct"], 146 "branches_pct": cov["branches"]["pct"], 147 } 148 ) 149 150 if file_coverage: 151 import pandas as pd 152 153 df = pd.DataFrame(file_coverage) 154 df = df.sort_values("lines_pct", ascending=True) # Worst coverage first 155 156 # Add coverage status column with both emoji and text for better filtering 157 def get_status(x): 158 if x < 50: 159 return "๐ด Critical" 160 elif x < 70: 161 return "๐ Low" 162 elif x < 80: 163 return "๐ก Medium" 164 else: 165 return "๐ข Good" 166 167 df["status"] = df["lines_pct"].apply(get_status) 168 169 # Display summary by status 170 status_counts = df["status"].value_counts() 171 status_cols = st.columns(4) 172 with status_cols[0]: 173 st.metric("๐ด Critical (<50%)", status_counts.get("๐ด Critical", 0)) 174 with status_cols[1]: 175 st.metric("๐ Low (50-69%)", status_counts.get("๐ Low", 0)) 176 with status_cols[2]: 177 st.metric("๐ก Medium (70-79%)", status_counts.get("๐ก Medium", 0)) 178 with status_cols[3]: 179 st.metric("๐ข Good (โฅ80%)", status_counts.get("๐ข Good", 0)) 180 181 st.write("") 182 183 # Collapsible per-file coverage table 184 with st.expander("๐ **Per-File Coverage Details**", expanded=False): 185 # Helper function to style only the status column with background colors 186 def highlight_status_column(s): 187 # Only color the status column 188 if s.name == "status": 189 return s.map({ 190 "๐ด Critical": "background-color: #ffcccc; font-weight: bold", # Light red 191 "๐ Low": "background-color: #ffe5cc; font-weight: bold", # Light orange 192 "๐ก Medium": "background-color: #ffffcc; font-weight: bold", # Light yellow 193 "๐ข Good": "background-color: #ccffcc; font-weight: bold", # Light green 194 }) 195 else: 196 return [""] * len(s) 197 198 # Apply styling and display 199 styled_df = df.style.apply(highlight_status_column, axis=0) 200 201 st.dataframe( 202 styled_df, 203 use_container_width=True, 204 hide_index=True, 205 column_config={ 206 "file": st.column_config.TextColumn("File", width="large"), 207 "status": st.column_config.TextColumn("Status", width="small"), 208 "lines_pct": st.column_config.NumberColumn( 209 "Lines %", format="%.1f%%" 210 ), 211 "lines_covered": st.column_config.NumberColumn("Covered"), 212 "lines_total": st.column_config.NumberColumn("Total"), 213 "statements_pct": st.column_config.NumberColumn( 214 "Statements %", format="%.1f%%" 215 ), 216 "functions_pct": st.column_config.NumberColumn( 217 "Functions %", format="%.1f%%" 218 ), 219 "branches_pct": st.column_config.NumberColumn( 220 "Branches %", format="%.1f%%" 221 ), 222 }, 223 ) 224 else: 225 st.warning("No per-file coverage data found") 226 else: 227 st.info("No coverage data available. Run `npm test` to generate coverage.") 228 229 except Exception as e: 230 log_exception(logger, f"Error loading coverage data: {e}") 231 st.error(f"Error loading coverage data: {e}") 232 233 st.markdown("---") 234 235 # === E2E Integration Tests === 236 st.subheader("๐งช E2E Integration Tests") 237 238 try: 239 test_results = file_readers.get_test_results() 240 test_list = file_readers.get_test_list() 241 242 if test_results: 243 test_cols = st.columns(4) 244 with test_cols[0]: 245 st.metric("Total Tests", test_results.get("total", "N/A")) 246 with test_cols[1]: 247 st.metric("Passed", test_results.get("pass", "N/A")) 248 with test_cols[2]: 249 st.metric("Failed", test_results.get("fail", "N/A")) 250 with test_cols[3]: 251 duration_ms = test_results.get("duration_ms", 0) 252 st.metric( 253 "Duration", f"{duration_ms/1000:.1f}s" if duration_ms else "N/A" 254 ) 255 256 # Test status 257 if test_results.get("fail", 0) > 0: 258 st.error(f"โ {test_results['fail']} tests failed") 259 else: 260 st.success("โ All tests passing") 261 262 # Display individual test results in expandable section 263 if test_list: 264 st.write("") 265 with st.expander( 266 f"๐ Individual Test Results ({len(test_list)} tests)", 267 expanded=False, 268 ): 269 # Group tests by suite 270 suites = {} 271 for test in test_list: 272 suite = test["suite"] or "Other" 273 if suite not in suites: 274 suites[suite] = [] 275 suites[suite].append(test) 276 277 # Display tests by suite 278 for suite_name, suite_tests in suites.items(): 279 st.write(f"**{suite_name}**") 280 for test in suite_tests: 281 status_icon = "โ " if test["status"] == "pass" else "โ" 282 duration = ( 283 f" ({test['duration_ms']:.1f}ms)" 284 if test["duration_ms"] 285 else "" 286 ) 287 st.text(f" {status_icon} {test['name']}{duration}") 288 st.write("") 289 else: 290 st.warning( 291 f"โ ๏ธ Test list is empty or None. Debug: test_results={bool(test_results)}, test_list={bool(test_list)}" 292 ) 293 else: 294 st.info( 295 "No E2E test results available. Run `npm run test:integration` to generate." 296 ) 297 298 except Exception as e: 299 log_exception(logger, f"Error loading test results: {e}") 300 st.error(f"Error loading test results: {e}")