/ dashboard / pages / 5_๐Ÿ“ˆ_Code_Coverage.py
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}")