wohnbot/tests/test_errorrate_runner.py
2025-12-16 13:51:25 +01:00

151 lines
6.1 KiB
Python

#!/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"<b>Total attempts:</b> {total_attempts}\n<b>Successes:</b> {total_success}\n<b>Failures:</b> {total_fail}\n<b>Overall error rate:</b> {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()