Add health reporting, container probe, and JNN channel support
Build and Publish TVJ EPG Image / build (push) Successful in 29s

This commit is contained in:
Lyncoln
2026-09-10 10:38:21 -04:00
parent 02a68f029a
commit 8529fca032
11 changed files with 349 additions and 13 deletions
+82
View File
@@ -0,0 +1,82 @@
import json
import tempfile
import unittest
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import patch
from epg.config import BUILTINS
from epg.engine import run
from epg.health import evaluate, record, state_path
from epg.providers import univtec
NOW = datetime(2026, 9, 10, 12, 30, tzinfo=timezone.utc)
def feed():
return [{'id': c.channel_id, 'epgObject': {'events': [
{'title': 'Sport', 'start': '2026-09-10T12:00:00Z', 'end': '2026-09-10T14:00:00Z'}
]}} for c in BUILTINS.values()]
class HealthTests(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.xml = Path(self.directory.name) / 'tvj.xml'
self.env = {'CHANNELS': 'tvj,tvjsn', 'OUTPUT_FILE': str(self.xml)}
def update(self):
with patch.object(univtec, 'fetch', return_value=feed()):
run(self.env)
report = json.loads(state_path(self.xml).read_text())
report['checked_at'] = NOW.isoformat()
record(self.xml, report)
def test_startup_and_success(self):
self.assertEqual(evaluate(self.env, NOW)['status'], 'unhealthy')
self.update()
health = evaluate(self.env, NOW)
self.assertEqual(health['status'], 'healthy')
self.assertTrue(all(health['outputs'].values()))
self.assertEqual(len(health['channels']), 2)
def test_failure_retains_usable_guide(self):
self.update()
before = self.xml.read_bytes()
with patch.object(univtec, 'fetch', side_effect=TimeoutError):
with self.assertRaises(TimeoutError):
run(self.env)
self.assertEqual(self.xml.read_bytes(), before)
health = evaluate(self.env, NOW)
self.assertEqual(health['status'], 'degraded')
self.assertEqual(health['providers']['univtec']['status'], 'unhealthy')
self.assertEqual(evaluate(self.env, NOW + timedelta(hours=2))['status'], 'unhealthy')
def test_missing_channel_and_corrupt_outputs(self):
self.update()
with patch.object(univtec, 'fetch', return_value=feed()[:1]):
with self.assertRaises(ValueError):
run(self.env)
health = evaluate(self.env, NOW)
self.assertEqual(health['status'], 'degraded')
self.assertEqual(health['channels']['TVJSN.jm@SD']['last_update']['status'], 'unhealthy')
self.xml.with_suffix('.m3u').write_text('#EXTM3U\n')
self.assertEqual(evaluate(self.env, NOW)['status'], 'unhealthy')
self.xml.write_text('broken xml')
self.assertFalse(evaluate(self.env, NOW)['outputs']['xmltv'])
def test_future_only_and_overdue(self):
self.update()
self.assertEqual(evaluate(self.env, NOW - timedelta(hours=1))['status'], 'unhealthy')
report = json.loads(state_path(self.xml).read_text())
report['checked_at'] = (NOW - timedelta(hours=7)).isoformat()
record(self.xml, report)
self.assertEqual(evaluate(self.env, NOW)['status'], 'degraded')
def test_health_requests_do_not_fetch(self):
self.update()
with patch.object(univtec, 'fetch') as fetch:
evaluate(self.env, NOW)
evaluate(self.env, NOW)
fetch.assert_not_called()