#!/usr/bin/env python3 """Simple test runner for the monitor's error-rate plot generator. Run from the repository root (where `monitor.py` and `data/` live): python3 tests/test_errorrate_runner.py This will call `TelegramBot._generate_error_rate_plot()` and print the result. """ import os import sys import logging logging.basicConfig(level=logging.INFO) import pandas as pd import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import json DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'data')) APPLICATIONS_FILE = os.path.join(DATA_DIR, 'applications.json') def generate_error_rate_plot(applications_file: str): if not os.path.exists(applications_file): print('No applications.json found at', applications_file) return None, '' try: with open(applications_file, 'r', encoding='utf-8') as f: apps = json.load(f) if not apps: return None, '' rows = [] for _id, rec in apps.items(): ts = rec.get('timestamp') try: dt = pd.to_datetime(ts) except Exception: dt = pd.NaT rows.append({'id': _id, 'company': rec.get('company'), 'success': bool(rec.get('success')), 'ts': dt}) df = pd.DataFrame(rows) df = df.dropna(subset=['ts']) if df.empty: return None, '' df['date'] = df['ts'].dt.floor('D') grouped = df.groupby('date').agg(total=('id','count'), successes=('success', lambda x: x.sum())) grouped['failures'] = grouped['total'] - grouped['successes'] grouped['error_rate'] = grouped['failures'] / grouped['total'] grouped = grouped.sort_index() # Plot import matplotlib.dates as mdates # Add a third subplot for error rate by company fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(12, 12), sharex=True) # Stacked bar: successes vs failures (all companies) grouped[['successes','failures']].plot(kind='bar', stacked=True, ax=ax1, color=['#2E8B57','#C44A4A']) ax1.set_ylabel('Count') ax1.set_title('Autopilot: Successes vs Failures (by day)') dates = pd.to_datetime(grouped.index).to_pydatetime() x = mdates.date2num(dates) width = 0.6 ax1.bar(x, grouped['successes'].values, width=width, color='#2E8B57', align='center') ax1.bar(x, grouped['failures'].values, bottom=grouped['successes'].values, width=width, color='#C44A4A', align='center') ax1.set_xticks(x) ax1.set_xlim(min(x) - 1, max(x) + 1) ax1.xaxis.set_major_locator(mdates.AutoDateLocator()) ax1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d')) # Line: overall error rate ax2.plot(x, grouped['error_rate'].values, marker='o', color='#3333AA', linewidth=2) ax2.set_ylim(-0.02, 1.02) ax2.set_ylabel('Error rate') ax2.set_xlabel('Date') ax2.set_title('Daily Error Rate (failures / total)') ax2.grid(True, alpha=0.3) ax2.set_xticks(x) ax2.set_xlim(min(x) - 1, max(x) + 1) ax2.xaxis.set_major_locator(mdates.AutoDateLocator()) ax2.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d')) # New: Error rate by company (line plot) # Group by date and company company_grouped = df.groupby(['date', 'company']).agg(total=('id','count'), successes=('success', lambda x: x.sum())) company_grouped['failures'] = company_grouped['total'] - company_grouped['successes'] company_grouped['error_rate'] = company_grouped['failures'] / company_grouped['total'] company_grouped = company_grouped.reset_index() # Pivot for plotting: index=date, columns=company, values=error_rate error_rate_pivot = company_grouped.pivot(index='date', columns='company', values='error_rate') # Plot each company as a line for company in error_rate_pivot.columns: y = error_rate_pivot[company].values ax3.plot(x, y, marker='o', label=str(company)) ax3.set_ylim(-0.02, 1.02) ax3.set_ylabel('Error rate') ax3.set_xlabel('Date') ax3.set_title('Daily Error Rate by Company') ax3.grid(True, alpha=0.3) ax3.set_xticks(x) ax3.set_xlim(min(x) - 1, max(x) + 1) ax3.xaxis.set_major_locator(mdates.AutoDateLocator()) ax3.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d')) ax3.legend(title='Company', loc='upper right', fontsize='small') fig.autofmt_xdate() plot_path = os.path.join(DATA_DIR, 'error_rate.png') tmp_path = os.path.join(DATA_DIR, 'error_rate.tmp.png') fig.savefig(tmp_path, format='png') plt.close(fig) try: # Atomic replace where possible os.replace(tmp_path, plot_path) except Exception: try: if os.path.exists(plot_path): os.remove(plot_path) os.rename(tmp_path, plot_path) except Exception as e: print('Failed to write plot file:', e) return None, '' total_attempts = int(grouped['total'].sum()) total_success = int(grouped['successes'].sum()) total_fail = int(grouped['failures'].sum()) overall_error = (total_fail / total_attempts) if total_attempts>0 else 0.0 summary = f"Total attempts: {total_attempts}\nSuccesses: {total_success}\nFailures: {total_fail}\nOverall error rate: {overall_error:.1%}" return plot_path, summary except Exception as e: print('Error generating plot:', e) return None, '' def main(): # Use the local implementation to avoid importing the full monitor (Playwright heavy) plot_path, summary = generate_error_rate_plot(APPLICATIONS_FILE) if plot_path: print("PLOT_PATH:", plot_path) print("EXISTS:", os.path.exists(plot_path)) print("SUMMARY:\n", summary) sys.exit(0) else: print("No plot generated (insufficient data or error)") sys.exit(3) if __name__ == '__main__': main()