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()