135 lines
4.9 KiB
Python
135 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tests für den Statserver (web/app.py), ohne Datenbank.
|
|
|
|
Geprüft werden die DB-freien Teile: Parsen der psql-Zeile, der
|
|
2-Stunden-Rhythmus für den exakten Bild-Count, die Wahl zwischen
|
|
exakter Zählung und Stichprobe sowie die Einbindung der neuen
|
|
Images-Anzeige in die HTML-Seite.
|
|
"""
|
|
|
|
import importlib.util
|
|
import os
|
|
import subprocess
|
|
import unittest
|
|
from datetime import datetime, timedelta
|
|
from unittest import mock
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
# web/app.py unter einem eindeutigen Modulnamen laden, damit es nicht mit
|
|
# priorityweb/app.py (gleicher Dateiname) kollidiert.
|
|
_spec = importlib.util.spec_from_file_location(
|
|
"jonassearch_stats_app", os.path.join(HERE, "app.py")
|
|
)
|
|
stats_app = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(stats_app)
|
|
|
|
|
|
class ParseStatsRowTest(unittest.TestCase):
|
|
|
|
def test_count_and_images(self):
|
|
self.assertEqual(
|
|
stats_app._parse_stats_row("981981|12345"), (981981, 12345)
|
|
)
|
|
|
|
def test_empty_sample_is_none(self):
|
|
self.assertEqual(stats_app._parse_stats_row("10|"), (10, None))
|
|
self.assertEqual(stats_app._parse_stats_row("10|\n"), (10, None))
|
|
|
|
|
|
class ImagesExactDueTest(unittest.TestCase):
|
|
|
|
NOW = datetime(2026, 10, 17, 12, 0, 0)
|
|
|
|
def _entry(self, age, exact):
|
|
return {
|
|
"timestamp": (self.NOW - age).isoformat(),
|
|
"count": 1,
|
|
"images": 2,
|
|
"images_exact": exact,
|
|
}
|
|
|
|
def test_no_data_means_due(self):
|
|
self.assertTrue(stats_app.images_exact_due([], now=self.NOW))
|
|
|
|
def test_recent_exact_is_not_due(self):
|
|
data = [self._entry(timedelta(hours=1), True)]
|
|
self.assertFalse(stats_app.images_exact_due(data, now=self.NOW))
|
|
|
|
def test_two_hours_old_is_due(self):
|
|
data = [self._entry(timedelta(hours=2), True)]
|
|
self.assertTrue(stats_app.images_exact_due(data, now=self.NOW))
|
|
|
|
def test_estimates_in_between_do_not_reset_schedule(self):
|
|
data = [
|
|
self._entry(timedelta(hours=1, minutes=30), True),
|
|
self._entry(timedelta(minutes=30), False),
|
|
]
|
|
self.assertFalse(stats_app.images_exact_due(data, now=self.NOW))
|
|
|
|
def test_old_entries_without_flag_are_ignored(self):
|
|
data = [{"timestamp": self.NOW.isoformat(), "count": 1}]
|
|
self.assertTrue(stats_app.images_exact_due(data, now=self.NOW))
|
|
|
|
def test_malformed_exact_timestamp_is_ignored(self):
|
|
data = [{"timestamp": "kaputt", "images_exact": True}]
|
|
self.assertTrue(stats_app.images_exact_due(data, now=self.NOW))
|
|
|
|
|
|
class GetStatsTest(unittest.TestCase):
|
|
|
|
@mock.patch.object(stats_app, "_psql_scalar")
|
|
def test_exact_query(self, psql):
|
|
psql.return_value = "100|250"
|
|
self.assertEqual(stats_app.get_stats(True), (100, 250, True))
|
|
self.assertEqual(psql.call_args[0][0], stats_app.EXACT_IMAGE_SQL)
|
|
self.assertNotIn("TABLESAMPLE", stats_app.EXACT_IMAGE_SQL)
|
|
|
|
@mock.patch.object(stats_app, "_psql_scalar")
|
|
def test_estimate_query(self, psql):
|
|
psql.return_value = "100|250"
|
|
self.assertEqual(stats_app.get_stats(False), (100, 250, False))
|
|
self.assertEqual(psql.call_args[0][0], stats_app.ESTIMATE_IMAGE_SQL)
|
|
self.assertIn("TABLESAMPLE SYSTEM (1)", stats_app.ESTIMATE_IMAGE_SQL)
|
|
|
|
@mock.patch.object(stats_app, "_psql_scalar")
|
|
def test_empty_sample_on_small_table_falls_back_to_exact(self, psql):
|
|
psql.side_effect = ["10|", "10|20"]
|
|
self.assertEqual(stats_app.get_stats(False), (10, 20, True))
|
|
self.assertEqual(psql.call_count, 2)
|
|
self.assertEqual(psql.call_args_list[1][0][0],
|
|
stats_app.EXACT_IMAGE_SQL)
|
|
|
|
@mock.patch.object(stats_app, "_psql_scalar")
|
|
def test_empty_sample_on_large_table_stays_unknown(self, psql):
|
|
psql.return_value = "200000|"
|
|
self.assertEqual(stats_app.get_stats(False), (200000, None, False))
|
|
|
|
@mock.patch.object(stats_app, "_psql_scalar")
|
|
def test_missing_pictures_column_falls_back_to_page_count(self, psql):
|
|
error = subprocess.CalledProcessError(
|
|
1, ["psql"], stderr='ERROR: column "pictures" does not exist'
|
|
)
|
|
psql.side_effect = [error, "10"]
|
|
self.assertEqual(stats_app.get_stats(False), (10, None, False))
|
|
self.assertEqual(psql.call_args_list[1][0][0],
|
|
"SELECT COUNT(*) FROM pages;")
|
|
|
|
|
|
class StatsPageTest(unittest.TestCase):
|
|
|
|
def test_page_contains_image_counter(self):
|
|
client = stats_app.app.test_client()
|
|
response = client.get("/")
|
|
self.assertEqual(response.status_code, 200)
|
|
html = response.get_data(as_text=True)
|
|
self.assertIn('id="current-images"', html)
|
|
self.assertIn('id="current-images-meta"', html)
|
|
self.assertIn("Estimated · exact every 2 h", html)
|
|
self.assertIn("imagesExact", html)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|