# #region Test.Metrics [C:3] [TYPE Module] [SEMANTICS test, translate, metrics, job, statistics] # @BRIEF Tests for metrics.py — TranslationMetrics.get_job_metrics, get_all_metrics. # @RELATION BINDS_TO -> [TranslationMetrics] # @TEST_CONTRACT: get_job_metrics -> dict | aggregated job metrics # @TEST_CONTRACT: get_all_metrics -> list[dict] | aggregated metrics for all jobs # @TEST_EDGE: no_runs # @TEST_EDGE: per_language_aggregation # @TEST_EDGE: metric_snapshot_merge import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src")) import os os.environ.setdefault("SECRET_KEY", "test-secret-key-for-testing") os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:") os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:") from datetime import datetime, timezone from unittest.mock import MagicMock, patch import pytest from src.plugins.translate.metrics import TranslationMetrics class _MockDB: """Helper to create a DB mock with independent query mocks per call.""" @staticmethod def create(query_count: int = 8): """Create db mock where each db.query() call returns an independent mock.""" db = MagicMock() query_mocks = [MagicMock() for _ in range(query_count)] db.query.side_effect = query_mocks return db, query_mocks class TestGetJobMetrics: """TranslationMetrics.get_job_metrics — aggregated job metrics.""" def _make_lang_stat(self, code: str, **kwargs): ls = MagicMock() ls.language_code = code ls.token_count = 1000 ls.estimated_cost = 0.002 ls.translated_rows = 50 for k, v in kwargs.items(): setattr(ls, k, v) return ls def test_basic_metrics(self): """Returns dict with all required keys.""" db, q = _MockDB.create() # 0: run_counts — group_by().all() q[0].filter.return_value.group_by.return_value.all.return_value = [ ("COMPLETED", 5), ("FAILED", 2), ] # 1: record_stats — first() q[1].filter.return_value.first.return_value = (500, 480, 10, 10) # 2: avg_duration — filter(cond1, cond2, cond3).scalar() — single filter with 3 args q[2].filter.return_value.scalar.return_value = 120000.0 # 3: last_run — filter().order_by().first() last_run = MagicMock() last_run.created_at = datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc) q[3].filter.return_value.order_by.return_value.first.return_value = last_run # 4: next_schedule — filter().first() next_schedule = MagicMock() next_schedule.last_run_at = datetime(2026, 1, 20, 12, 0, 0, tzinfo=timezone.utc) q[4].filter.return_value.first.return_value = next_schedule # 5: latest_snapshot (1st call) — filter().order_by().first() q[5].filter.return_value.order_by.return_value.first.return_value = None # 6: lang_stats_rows — join().filter().all() lang_stats = [self._make_lang_stat("ru"), self._make_lang_stat("de")] q[6].join.return_value.filter.return_value.all.return_value = lang_stats # 7: latest_snapshot (2nd call, same query) — filter().order_by().first() q[7].filter.return_value.order_by.return_value.first.return_value = None metrics = TranslationMetrics(db) result = metrics.get_job_metrics("job-1") assert result["job_id"] == "job-1" assert result["total_runs"] == 7 assert result["successful_runs"] == 5 assert result["failed_runs"] == 2 assert result["cancelled_runs"] == 0 assert result["total_records"] == 500 assert result["successful_records"] == 480 assert result["failed_records"] == 10 assert result["skipped_records"] == 10 assert result["cumulative_tokens"] == 2000 assert result["cumulative_cost"] == 0.004 assert result["avg_duration_ms"] == 120000 assert result["last_run_at"] is not None assert result["next_scheduled_run"] is not None assert len(result["per_language_metrics"]) == 2 assert "ru" in result["per_language_metrics"] assert "de" in result["per_language_metrics"] def test_no_runs(self): """No runs returns zero metrics.""" db, q = _MockDB.create() q[0].filter.return_value.group_by.return_value.all.return_value = [] q[1].filter.return_value.first.return_value = (0, 0, 0, 0) q[2].filter.return_value.scalar.return_value = None q[3].filter.return_value.order_by.return_value.first.return_value = None q[4].filter.return_value.first.return_value = None q[5].filter.return_value.order_by.return_value.first.return_value = None q[6].join.return_value.filter.return_value.all.return_value = [] q[7].filter.return_value.order_by.return_value.first.return_value = None metrics = TranslationMetrics(db) result = metrics.get_job_metrics("job-1") assert result["total_runs"] == 0 assert result["total_records"] == 0 assert result["avg_duration_ms"] is None assert result["last_run_at"] is None assert result["next_scheduled_run"] is None assert result["per_language_metrics"] == {} def test_avg_duration_none_when_no_timed_runs(self): """No completed runs => avg_duration_ms is None.""" db, q = _MockDB.create() q[0].filter.return_value.group_by.return_value.all.return_value = [("PENDING", 1)] q[1].filter.return_value.first.return_value = (0, 0, 0, 0) q[2].filter.return_value.scalar.return_value = None q[3].filter.return_value.order_by.return_value.first.return_value = None q[4].filter.return_value.first.return_value = None q[5].filter.return_value.order_by.return_value.first.return_value = None q[6].join.return_value.filter.return_value.all.return_value = [] q[7].filter.return_value.order_by.return_value.first.return_value = None metrics = TranslationMetrics(db) result = metrics.get_job_metrics("job-1") assert result["avg_duration_ms"] is None def test_no_next_schedule(self): """No active schedule => next_scheduled_run is None.""" db, q = _MockDB.create() q[0].filter.return_value.group_by.return_value.all.return_value = [] q[1].filter.return_value.first.return_value = (0, 0, 0, 0) q[2].filter.return_value.scalar.return_value = None q[3].filter.return_value.order_by.return_value.first.return_value = None q[4].filter.return_value.first.return_value = None q[5].filter.return_value.order_by.return_value.first.return_value = None q[6].join.return_value.filter.return_value.all.return_value = [] q[7].filter.return_value.order_by.return_value.first.return_value = None metrics = TranslationMetrics(db) result = metrics.get_job_metrics("job-1") assert result["next_scheduled_run"] is None def test_per_language_with_snapshot_merge(self): """MetricSnapshot per_language_metrics merged into result.""" db, q = _MockDB.create() q[0].filter.return_value.group_by.return_value.all.return_value = [("COMPLETED", 3)] q[1].filter.return_value.first.return_value = (100, 95, 3, 2) q[2].filter.return_value.scalar.return_value = 50000.0 q[3].filter.return_value.order_by.return_value.first.return_value = None # no last_run q[4].filter.return_value.first.return_value = None # no next_schedule # 5: latest_snapshot (1st) snapshot1 = MagicMock() snapshot1.per_language_metrics = None q[5].filter.return_value.order_by.return_value.first.return_value = snapshot1 # 6: lang_stats_rows (empty) q[6].join.return_value.filter.return_value.all.return_value = [] # 7: latest_snapshot (2nd) with per_language_metrics snapshot2 = MagicMock() snapshot2.per_language_metrics = { "ru": {"cumulative_tokens": 5000, "cumulative_cost": 0.01, "runs": 10}, } q[7].filter.return_value.order_by.return_value.first.return_value = snapshot2 metrics = TranslationMetrics(db) result = metrics.get_job_metrics("job-1") assert "ru" in result["per_language_metrics"] assert result["cumulative_tokens"] == 5000 assert result["cumulative_cost"] == 0.01 def test_per_language_with_live_and_snapshot(self): """Live stats + snapshot metrics merge correctly.""" db, q = _MockDB.create() q[0].filter.return_value.group_by.return_value.all.return_value = [("COMPLETED", 3)] q[1].filter.return_value.first.return_value = (100, 95, 3, 2) q[2].filter.return_value.scalar.return_value = 50000.0 q[3].filter.return_value.order_by.return_value.first.return_value = None # no last_run q[4].filter.return_value.first.return_value = None # no next_schedule # 5: latest_snapshot (1st) q[5].filter.return_value.order_by.return_value.first.return_value = None # 6: lang_stats_rows — live data q[6].join.return_value.filter.return_value.all.return_value = [ self._make_lang_stat("ru", token_count=500, estimated_cost=0.001, translated_rows=25), self._make_lang_stat("fr", token_count=200, estimated_cost=0.0005, translated_rows=10), ] # 7: latest_snapshot (2nd) snapshot = MagicMock() snapshot.per_language_metrics = { "ru": {"cumulative_tokens": 5000, "cumulative_cost": 0.01, "runs": 10}, "de": {"cumulative_tokens": 3000, "cumulative_cost": 0.006, "runs": 5}, } q[7].filter.return_value.order_by.return_value.first.return_value = snapshot metrics = TranslationMetrics(db) result = metrics.get_job_metrics("job-1") assert "ru" in result["per_language_metrics"] assert "de" in result["per_language_metrics"] assert "fr" in result["per_language_metrics"] # ru: live 500 + snapshot 5000 = 5500 assert result["per_language_metrics"]["ru"]["tokens"] == 5500 assert result["per_language_metrics"]["ru"]["cost"] == 0.011 assert result["per_language_metrics"]["fr"]["tokens"] == 200 assert result["per_language_metrics"]["de"]["tokens"] == 3000 def test_no_last_run_returns_none(self): """No last run => last_run_at is None.""" db, q = _MockDB.create() q[0].filter.return_value.group_by.return_value.all.return_value = [] q[1].filter.return_value.first.return_value = (0, 0, 0, 0) q[2].filter.return_value.scalar.return_value = None q[3].filter.return_value.order_by.return_value.first.return_value = None q[4].filter.return_value.first.return_value = None q[5].filter.return_value.order_by.return_value.first.return_value = None q[6].join.return_value.filter.return_value.all.return_value = [] q[7].filter.return_value.order_by.return_value.first.return_value = None metrics = TranslationMetrics(db) result = metrics.get_job_metrics("job-1") assert result["last_run_at"] is None class TestGetAllMetrics: """TranslationMetrics.get_all_metrics — metrics for all jobs.""" def test_all_metrics(self): """Returns list of per-job metrics.""" db = MagicMock() db.query.return_value.distinct.return_value.all.return_value = [ ("job-1",), ("job-2",), ] with patch.object(TranslationMetrics, "get_job_metrics") as mock_get: mock_get.side_effect = [ {"job_id": "job-1", "total_runs": 5}, {"job_id": "job-2", "total_runs": 3}, ] metrics = TranslationMetrics(db) result = metrics.get_all_metrics() assert len(result) == 2 assert result[0]["job_id"] == "job-1" assert result[1]["job_id"] == "job-2" def test_empty_jobs(self): """No jobs returns empty list.""" db = MagicMock() db.query.return_value.distinct.return_value.all.return_value = [] metrics = TranslationMetrics(db) result = metrics.get_all_metrics() assert result == [] # #endregion Test.Metrics