import json
import os
import sys
import tempfile
import unittest
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import gradio as gr
from admin_stats import AdminStatistics, build_admin_statistics, make_report, render_table
from types import SimpleNamespace
from generation_stats import JST
class TestAdminStatistics(unittest.TestCase):
def setUp(self):
environment = patch.dict(os.environ, {"ADMIN_USERS": "yamiuchi"})
environment.start()
self.addCleanup(environment.stop)
self.data = {"version": 1, "timezone": "Asia/Tokyo", "counts": {
"2026-09-30": {"anima": {"old-model": 4}},
"2026-10-14": {"anima": {"a": 26}},
"2026-10-15": {"anima": {"a": 94}, "illustrious": {"b": 2}}}}
self.models = {"anima": ["a", "unused"], "illustrious": ["b"]}
def test_daily_monthly_zero_models_and_removed_models(self):
report = make_report(self.data, self.models, datetime(2026, 10, 15, tzinfo=JST))
self.assertIn(["Anima", "a", 26, 94], report["daily"])
self.assertIn(["Anima", "unused", 0, 0], report["daily"])
self.assertEqual(report["daily"][-1], ["表示中のモデル合計", "", 26, 96])
self.assertIn(["Anima", "a", 0, 120], report["monthly"])
self.assertNotIn(["Anima", "old-model", 4, 0], report["monthly"])
self.assertIn(["Anima", "unused", 0, 0], report["monthly"])
self.assertEqual(report["monthly"][-1], ["表示中のモデル合計", "", 0, 122])
all_models = make_report(self.data, self.models, datetime(2026, 10, 15, tzinfo=JST), show_past=True)
self.assertIn(["Anima", "old-model", 4, 0], all_models["monthly"])
self.assertEqual(all_models["monthly"][-1], ["表示中のモデル合計", "", 4, 122])
self.assertEqual([r[:2] for r in report["daily"]], [r[:2] for r in report["monthly"]])
self.assertEqual(report["monthly_headers"], ["モード", "モデル", "前月合計 2026-09", "当月合計 2026-10"])
def test_jst_month_boundary(self):
report = make_report(self.data, self.models, datetime(2026, 10, 31, 15, tzinfo=timezone.utc))
self.assertIn("2026-11-01", report["daily_headers"][-1])
self.assertEqual(report["monthly_headers"][-2:], ["前月合計 2026-10", "当月合計 2026-11"])
self.assertIn(["Anima", "a", 120, 0], report["monthly"])
def test_monthly_year_boundary_and_leap_day(self):
for dates, now, expected in (
({"2025-12-31": 7, "2026-01-01": 3}, datetime(2026, 1, 1, tzinfo=JST), (7, 3)),
({"2024-02-28": 7, "2024-02-29": 3, "2024-03-01": 2}, datetime(2024, 3, 1, tzinfo=JST), (10, 2)),
):
raw = {"version": 1, "timezone": "Asia/Tokyo", "counts": {
day: {"anima": {"a": count}} for day, count in dates.items()}}
report = make_report(raw, self.models, now)
self.assertIn(["Anima", "a", *expected], report["monthly"])
self.assertEqual(report["monthly"][-1], ["表示中のモデル合計", "", *expected])
def test_empty_and_older_history_only(self):
for counts in ({}, {"2026-08-31": {"anima": {"removed": 99}}}):
raw = {"version": 1, "timezone": "Asia/Tokyo", "counts": counts}
report = make_report(raw, self.models, datetime(2026, 10, 15, tzinfo=JST))
self.assertTrue(all(r[2:] == [0, 0] for r in report["monthly"]))
self.assertEqual([r[:2] for r in report["daily"]], [r[:2] for r in report["monthly"]])
def test_ui_refresh_updates_month_headers_and_numeric_columns(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "daily-counts.json"
raw = {"version": 1, "timezone": "Asia/Tokyo", "counts": {}}
path.write_text(json.dumps(raw), encoding="utf-8")
with gr.Blocks(analytics_enabled=False) as demo:
build_admin_statistics(demo, SimpleNamespace(path=path), self.models)
self.assertFalse(any(c["type"] == "dataframe" for c in demo.config["components"]))
checkbox = next(c for c in demo.config["components"] if c["type"] == "checkbox")
self.assertFalse(checkbox["props"]["value"])
result = next(iter(demo.fns.values())).fn()
expected = make_report(raw, self.models)
self.assertEqual(result[3], render_table(expected["monthly_headers"], expected["monthly"], "前月・当月のモデル別生成件数"))
def test_public_ui_without_login_and_safe_errors(self):
with gr.Blocks() as demo:
build_admin_statistics(demo, SimpleNamespace(path=None), self.models)
config = demo.get_config_file()
self.assertFalse(any(c["type"] == "loginbutton" for c in config["components"]))
callback = next(iter(demo.fns.values())).fn
with self.assertLogs("admin_stats", level="WARNING"):
result = callback()
self.assertTrue(result[0]["visible"])
self.assertNotIn("GENERATION_STATS_DIR", result[1])
self.assertEqual(result[2], "")
self.assertEqual(result[3], "")
def test_filtered_totals_and_no_renamed_model_merging(self):
raw = {"version": 1, "timezone": "Asia/Tokyo", "counts": {
"2026-10-15": {"anima": {"a": 3, "old-a": 7}},
"2026-10-14": {"anima": {"old-a": 5}}}}
now = datetime(2026, 10, 15, tzinfo=JST)
current = make_report(raw, self.models, now)
historical = make_report(raw, self.models, now, show_past=True)
self.assertEqual(current["daily"][-1][2:], [0, 3])
self.assertEqual(historical["daily"][-1][2:], [5, 10])
self.assertEqual(current["monthly"][-1][2:], [0, 3])
self.assertEqual(historical["monthly"][-1][2:], [0, 15])
self.assertIn(["Anima", "a", 0, 3], historical["daily"])
self.assertEqual(raw["counts"]["2026-10-15"]["anima"]["old-a"], 7)
def test_bucket_model_names_are_html_escaped(self):
html = render_table(["モード", "モデル", "昨日", "今日"],
[["Anima", '
', 0, 1],
["表示中のモデル合計", "", 0, 1]], "test")
self.assertNotIn('
', html)
def test_read_only_file_and_errors(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "daily-counts.json"
path.write_text(json.dumps(self.data), encoding="utf-8")
before = path.read_bytes(), path.stat().st_mtime_ns
report = AdminStatistics(path, self.models).fetch()
self.assertTrue(report["daily"])
self.assertEqual(before, (path.read_bytes(), path.stat().st_mtime_ns))
with self.assertRaises(ValueError):
AdminStatistics(None, self.models).fetch()
path.write_text("broken", encoding="utf-8")
with self.assertRaises(ValueError):
AdminStatistics(path, self.models).fetch()
self.assertEqual(path.read_text(), "broken")
def test_public_regardless_of_admin_variable(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "daily-counts.json"
path.write_text(json.dumps(self.data), encoding="utf-8")
for value in (None, "", " , , ", "yamiuchi", "other"):
with patch.dict(os.environ):
if value is None:
os.environ.pop("ADMIN_USERS", None)
else:
os.environ["ADMIN_USERS"] = value
self.assertTrue(AdminStatistics(path, self.models).fetch()["daily"])
def test_invalid_counts_rejected(self):
for invalid in (True, -1, "1", 10**100):
self.data["counts"]["2026-10-15"]["illustrious"]["b"] = invalid
with self.assertRaises(ValueError):
make_report(self.data, self.models, datetime(2026, 10, 15, tzinfo=JST))
if __name__ == "__main__":
unittest.main()