Files
pigway-raspberry-pi-agent/tests/test_audit_regressions.py
T

133 lines
7.3 KiB
Python

"""Regression checks for configuration, alert truth and stale snapshot handling."""
import ast
import concurrent.futures
import json
import sys
import tempfile
import threading
import time
import unittest
from http.server import ThreadingHTTPServer
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
from urllib.error import HTTPError
from urllib.request import Request, urlopen
ROOT=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(ROOT/'app'))
import pigway_pi_control_api as api
import plugin_api
from storage_cache import StorageCache
class ConfigTests(unittest.TestCase):
def setUp(self):
self.directory=tempfile.TemporaryDirectory();self.addCleanup(self.directory.cleanup)
self.config=Path(self.directory.name)/'agent.conf'
self.config.write_text((ROOT/'config/pigway-pi-control.conf').read_text())
self.override=patch.object(api,'CFG',self.config);self.override.start();self.addCleanup(self.override.stop)
def test_invalid_values_and_relationships_leave_configuration_unchanged(self):
original=self.config.read_text()
for updates in [
{'timing':{'sensor_interval':'-1'}},{'timing':{'sensor_interval':'0'}},
{'alerts':{'cpu_high':'nan'}},{'alerts':{'memory_high':'inf'}},
{'alerts':{'cpu_high':'101'}},{'alerts':{'cpu_high':'50'}},
{'alerts':{'temp_high':'80'}},{'alerts':{'disk_high':'99'}},
{'alerts':{'wifi_weak':'-60'}},{'alerts':{'cpu_high_seconds':'1.5'}},
{'services':{'BAD':'not-a-unit'}},
]:
with self.subTest(updates=updates):
with self.assertRaises(ValueError):api.update_ini(updates)
self.assertEqual(self.config.read_text(),original)
api.update_ini({'alerts':{'cpu_high':'60','cpu_recover':'50'}})
self.assertEqual(api.load_cfg().getfloat('alerts','cpu_high'),60)
def test_literal_percent_in_identity_and_process_matches(self):
api.update_ini({'device':{'name':'Office 100%'},'processes':{'APP':'worker --ratio=100%'}})
self.assertEqual(api.machine_identity()['name'],'Office 100%')
self.assertEqual(api.load_cfg().get('processes','app'),'worker --ratio=100%')
def test_parallel_writes_preserve_every_edit_and_backup(self):
def save(index):return api.update_ini({'processes':{f'APP{index}':f'worker-{index}'}})
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool:
backups=list(pool.map(save,range(20)))
self.assertEqual(len(set(backups)),20)
cfg=api.load_cfg()
for index in range(20):self.assertEqual(cfg.get('processes',f'app{index}'),f'worker-{index}')
self.assertFalse(list(self.config.parent.glob('agent.conf.*tmp*')))
class StateTests(unittest.TestCase):
def test_weekly_root_cache_survives_restart_after_first_day(self):
with tempfile.TemporaryDirectory() as directory:
reads=[]
def reader(_):reads.append(1);return SimpleNamespace(total=100,used=20,free=80)
path=Path(directory)/'cache'
StorageCache(path,reader,lambda:1000).sample()
restored=StorageCache(path,reader,lambda:101000)
restored.sample();self.assertEqual(len(reads),1)
self.assertEqual(restored.next_update_at,605800)
StorageCache(path,reader,lambda:605800).sample();self.assertEqual(len(reads),2)
def test_publisher_stops_renewing_stale_data_and_recovers(self):
now=[0];calls=[]
publisher=plugin_api.Publisher({},'test',lambda *a,**k:None)
class Event:
def is_set(self):return now[0]>=8
def wait(self,_):
now[0]+=1
if now[0]==6:publisher.publish(state)
publisher.stop_event=Event()
state={'system':{},'network':{},'display':{'alerts':[]}}
def request(path,method,endpoint,payload=None):
if method=='PUT':calls.append((now[0],payload['revision']))
return {'api_version':1}
with patch.object(plugin_api.time,'monotonic',lambda:now[0]),patch.object(plugin_api,'request',request):
publisher.publish(state);publisher.run('test','unused')
self.assertEqual(calls,[(0,1),(1,2),(2,3),(6,4),(7,5)])
def test_status_api_rejects_stale_agent_then_accepts_fresh_data(self):
with tempfile.TemporaryDirectory() as directory:
path=Path(directory)/'status.json';path.write_text(json.dumps({'timestamp':time.time()-60}))
token=Path(directory)/'token';token.write_text('test-token')
with patch.object(api,'STATUS',path),patch.object(api,'TOKEN',token):
server=ThreadingHTTPServer(('127.0.0.1',0),api.Handler)
worker=threading.Thread(target=server.serve_forever);worker.start()
try:
req=Request(f'http://127.0.0.1:{server.server_port}/api/v1/status',headers={'Authorization':'Bearer test-token'})
with self.assertRaises(HTTPError) as caught:urlopen(req)
self.assertEqual(caught.exception.code,503)
caught.exception.close()
path.write_text(json.dumps({'timestamp':time.time()}))
with urlopen(req) as response:self.assertEqual(response.status,200)
finally:server.shutdown();worker.join();server.server_close()
def test_alert_severity_owner_changes_and_configured_disk_text(self):
tree=ast.parse((ROOT/'app/pigway_pi_control.py').read_text())
names={'AlertManager','alert_priority','update_active_alerts'}
nodes=[n for n in tree.body if isinstance(n,(ast.ClassDef,ast.FunctionDef)) and n.name in names]
logs=[]
env={'time':time,'log':lambda *args,**kwargs:logs.append((args,kwargs)),
'ALERT_PRIORITIES':{'CPU_HIGH':45,'DISK_CRITICAL':55,'DISK_HIGH':40,'TEMP_HIGH':50,'TEMP_CRITICAL':90},
'ALERT_CATEGORIES':{'CPU_HIGH':'CPU','DISK_CRITICAL':'STORAGE','DISK_HIGH':'STORAGE','TEMP_HIGH':'CPU','TEMP_CRITICAL':'CPU'}}
exec(compile(ast.Module(body=nodes,type_ignores=[]),'monitor-functions','exec'),env)
manager=env['AlertManager']()
manager.request('CPU_HIGH',1,'CPU');manager.request('DISK_CRITICAL',2,'DISK')
self.assertEqual(logs[-1][0][0],'CRITICAL')
manager.request('TEMP_HIGH',3,'TEMP')
self.assertEqual(manager.status_snapshot()['owner'],'TEMP_HIGH')
self.assertEqual(logs[-1][0][0],'EMERGENCY')
before=len(logs);manager.request('TEMP_HIGH',3,'TEMP');self.assertEqual(len(logs),before)
manager.request('TEMP_HIGH',1,'TEMP');self.assertEqual(logs[-1][0][1],'ALERT_CHANGED')
env.update(alert_manager=manager,cpu_hi=0,mem_hi=0,CPU_HIGH=90,CPU_RECOVER=75,CPU_HIGH_SEC=30,
MEM_HIGH=90,MEM_RECOVER=85,MEM_HIGH_SEC=30,TEMP_HIGH=68,TEMP_CRIT=75,
DISK_HIGH=70,DISK_CRIT=80,link_ok=True,net_ok=True,watches=[],watch_state=[],
net_kind='NET',health={},fs_readonly=False,ethernet_slow_active=False,
transition=lambda *a,**k:None,net_iface='',net_speed_mbps=None,ETHERNET_MIN_SPEED=1000)
env['update_active_alerts'](10,40,20,85)
self.assertEqual(manager.requests['DISK_CRITICAL']['l3'],'USED >= 80%')
env['update_active_alerts'](10,40,20,75)
self.assertEqual(manager.requests['DISK_HIGH']['l3'],'USED >= 70%')