Files
ss-tools/backend/tests/plugins/translate/test_run_service.py

422 lines
19 KiB
Python

# #region Test.RunExecutionService [C:3] [TYPE Module] [SEMANTICS test, translate, run, execution]
# @BRIEF Tests for _run_service.py — RunExecutionService.
# @RELATION BINDS_TO -> [_run_service]
# @TEST_EDGE: filter_new_keys_no_prev_run -> all keys treated as new
# @TEST_EDGE: filter_new_keys_no_key_cols -> all rows pass through
# @TEST_EDGE: load_preview_edits -> edits loaded from session
import pytest
from unittest.mock import MagicMock, patch
from datetime import UTC, datetime
import uuid
from sqlalchemy import create_engine, event
from sqlalchemy.orm import Session, sessionmaker
from src.models.translate import (
Base, TranslationBatch, TranslationJob, TranslationRun, TranslationRecord,
TranslationLanguage, TranslationRunLanguageStats,
TranslationPreviewSession, TranslationPreviewRecord,
)
def make_session():
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(bind=engine)
session = sessionmaker(bind=engine)()
return session, engine
class TestRunExecutionService:
"""RunExecutionService — orchestrate full translation run."""
def _make_service(self, db, **kwargs):
from src.plugins.translate._run_service import RunExecutionService
config_manager = kwargs.pop("config_manager", MagicMock())
return RunExecutionService(db, config_manager, **kwargs)
# -- _fetch_source_rows / _extract_chart_data_rows (thin wrappers) --
@pytest.mark.asyncio
async def test_fetch_source_rows_calls_imported(self):
"""_fetch_source_rows delegates to module-level fetch_source_rows."""
session, engine = make_session()
try:
svc = self._make_service(session)
from src.models.translate import TranslationJob
job = TranslationJob(id="fsr-job", name="fsr", status="ACTIVE",
source_dialect="en", target_dialect="fr")
session.add(job)
session.commit()
with patch("src.plugins.translate._run_service.fetch_source_rows",
return_value=[{"id": 1}]) as mock_fetch:
result = await svc._fetch_source_rows("fsr-job", "run-fsr-1")
assert result == [{"id": 1}]
mock_fetch.assert_called_once_with(
session, svc.config_manager, "fsr-job", "run-fsr-1"
)
finally:
session.close()
Base.metadata.drop_all(bind=engine)
def test_extract_chart_data_rows_delegates(self):
"""Static _extract_chart_data_rows delegates to module-level function."""
from src.plugins.translate._run_service import RunExecutionService
with patch("src.plugins.translate._run_service._extract_chart_data_rows",
return_value=[{"id": 1}]) as mock_extract:
result = RunExecutionService._extract_chart_data_rows({"data": []})
assert result == [{"id": 1}]
mock_extract.assert_called_once_with({"data": []})
# -- _filter_new_keys --
def test_filter_new_keys_no_prev_run(self):
"""No prior completed run -> all source rows treated as new."""
session, engine = make_session()
try:
job = TranslationJob(id="j1", name="j1", status="ACTIVE",
source_dialect="en", target_dialect="fr")
session.add(job)
session.commit()
svc = self._make_service(session)
source_rows = [{"source_data": {"id": 1}}]
result = svc._filter_new_keys(job, "run-1", source_rows)
assert len(result) == 1
finally:
session.close()
Base.metadata.drop_all(bind=engine)
def test_filter_new_keys_with_prev_run_filters(self):
"""Prior completed run -> rows with existing keys filtered out."""
session, engine = make_session()
try:
job = TranslationJob(id="j2", name="j2", status="ACTIVE",
source_dialect="en", target_dialect="fr",
target_key_cols=["id"])
session.add(job)
prev_run = TranslationRun(id="prev-run", job_id="j2", status="COMPLETED",
insert_status="succeeded",
started_at=datetime.now(UTC),
created_at=datetime.now(UTC))
session.add(prev_run)
session.flush()
batch = TranslationBatch(id="batch-prev", run_id="prev-run", batch_index=0,
status="COMPLETED", created_at=datetime.now(UTC))
session.add(batch)
session.flush()
# Record with existing key
rec = TranslationRecord(id="rec-1", run_id="prev-run", batch_id="batch-prev",
status="SUCCESS", source_data={"id": 1})
session.add(rec)
session.commit()
svc = self._make_service(session)
source_rows = [
{"source_data": {"id": 1}}, # already exists
{"source_data": {"id": 2}}, # new
]
result = svc._filter_new_keys(job, "curr-run", source_rows)
assert len(result) == 1
assert result[0]["source_data"]["id"] == 2
finally:
session.close()
Base.metadata.drop_all(bind=engine)
def test_filter_new_keys_no_key_cols(self):
"""No key cols configured -> all rows pass through."""
session, engine = make_session()
try:
job = TranslationJob(id="j3", name="j3", status="ACTIVE",
source_dialect="en", target_dialect="fr",
target_key_cols=None, source_key_cols=None)
session.add(job)
prev_run = TranslationRun(id="prev-run-3", job_id="j3", status="COMPLETED",
insert_status="succeeded",
started_at=datetime.now(UTC),
created_at=datetime.now(UTC))
session.add(prev_run)
session.commit()
svc = self._make_service(session)
source_rows = [{"source_data": {"id": 1}}, {"source_data": {"id": 2}}]
result = svc._filter_new_keys(job, "curr-run-3", source_rows)
assert len(result) == 2
finally:
session.close()
Base.metadata.drop_all(bind=engine)
def test_filter_new_keys_no_prev_records(self):
"""Prior run has no records -> all rows new."""
session, engine = make_session()
try:
job = TranslationJob(id="j4", name="j4", status="ACTIVE",
source_dialect="en", target_dialect="fr",
target_key_cols=["id"])
session.add(job)
prev_run = TranslationRun(id="prev-run-4", job_id="j4", status="COMPLETED",
insert_status="succeeded",
started_at=datetime.now(UTC),
created_at=datetime.now(UTC))
session.add(prev_run)
session.commit()
svc = self._make_service(session)
source_rows = [{"source_data": {"id": 1}}]
result = svc._filter_new_keys(job, "curr-run-4", source_rows)
assert len(result) == 1
finally:
session.close()
Base.metadata.drop_all(bind=engine)
def test_filter_new_keys_empty_key_cols_with_prev_records(self):
"""Prev records exist but key_cols is empty (None) → all rows pass through (lines 75-76)."""
session, engine = make_session()
try:
job = TranslationJob(id="j-ekc", name="j-ekc", status="ACTIVE",
source_dialect="en", target_dialect="fr",
target_key_cols=None, source_key_cols=None)
session.add(job)
prev_run = TranslationRun(id="prev-run-ekc", job_id="j-ekc", status="COMPLETED",
insert_status="succeeded",
started_at=datetime.now(UTC),
created_at=datetime.now(UTC))
session.add(prev_run)
session.flush()
# Add prev records so the code reaches the key_cols check
batch = TranslationBatch(id="batch-ekc", run_id="prev-run-ekc", batch_index=0,
status="COMPLETED", created_at=datetime.now(UTC))
session.add(batch)
session.flush()
rec = TranslationRecord(id="rec-ekc", run_id="prev-run-ekc", batch_id="batch-ekc",
status="SUCCESS", source_data={"id": 1})
session.add(rec)
session.commit()
svc = self._make_service(session)
source_rows = [{"source_data": {"id": 1}}, {"source_data": {"id": 2}}]
result = svc._filter_new_keys(job, "curr-run-ekc", source_rows)
# key_cols = None or None or [] = [] → not key_cols → True → return source_rows
assert len(result) == 2
finally:
session.close()
Base.metadata.drop_all(bind=engine)
# -- _load_preview_edits --
def test_load_preview_edits_no_session(self):
"""No applied preview session -> empty cache."""
session, engine = make_session()
try:
job = TranslationJob(id="j5", name="j5", status="ACTIVE",
source_dialect="en", target_dialect="fr")
session.add(job)
session.commit()
svc = self._make_service(session)
svc._load_preview_edits("j5")
assert svc._preview_edits_cache == {}
finally:
session.close()
Base.metadata.drop_all(bind=engine)
def test_load_preview_edits_skip_null_source_data(self):
"""Preview record with source_data=None is skipped (line 120)."""
session, engine = make_session()
try:
job = TranslationJob(id="j-null-sd", name="j-null-sd", status="ACTIVE",
source_dialect="en", target_dialect="fr")
session.add(job)
ps = TranslationPreviewSession(id="ps-null", job_id="j-null-sd", status="APPLIED",
created_at=datetime.now(UTC))
session.add(ps)
from src.models.translate import TranslationPreviewRecord as TPR
rec = TPR(id="prec-null", session_id="ps-null", status="APPROVED",
source_data=None)
session.add(rec)
session.commit()
svc = self._make_service(session)
svc._load_preview_edits("j-null-sd")
assert svc._preview_edits_cache == {}
finally:
session.close()
Base.metadata.drop_all(bind=engine)
def test_load_preview_edits_with_edits(self):
"""Load preview edits from applied session."""
session, engine = make_session()
try:
job = TranslationJob(id="j6", name="j6", status="ACTIVE",
source_dialect="en", target_dialect="fr")
session.add(job)
ps = TranslationPreviewSession(id="ps-1", job_id="j6", status="APPLIED",
created_at=datetime.now(UTC))
session.add(ps)
# The _load_preview_edits iterates rec.languages which is a Python attribute
# on the model instance set after DB fetch, not a column.
# We'll create a record with source_data, then patch the query to return
# records with languages attached.
from src.models.translate import TranslationPreviewRecord as TPR
rec = TPR(id="prec-1", session_id="ps-1", status="APPROVED",
source_data={"id": 1})
session.add(rec)
session.commit()
svc = self._make_service(session)
# We need to mock the query to return a record with .languages
class MockLangEntry:
def __init__(self, code, status, user_edit=None, final_value=None):
self.language_code = code
self.status = status
self.user_edit = user_edit
self.final_value = final_value
mock_rec = MagicMock(spec=TPR)
mock_rec.source_data = {"id": 1}
mock_rec.languages = [
MockLangEntry("fr", "edited", "bonjour", "bonjour"),
MockLangEntry("de", "approved", "hallo"),
]
with patch.object(session, 'query', return_value=MagicMock()) as mock_query:
mock_filter = MagicMock()
mock_filter.order_by.return_value.first.return_value = ps
mock_query.return_value.filter.return_value = mock_filter
# Mock the records query
mock_filter2 = MagicMock()
mock_filter2.all.return_value = [mock_rec]
mock_query.return_value.filter.return_value = mock_filter
# Make first call return ps, second call return records
mock_query.return_value.filter.return_value.order_by.return_value.first.return_value = ps
# For the records query
mock_records_filter = MagicMock()
mock_records_filter.all.return_value = [mock_rec]
mock_query.return_value.filter.return_value = mock_records_filter
svc._load_preview_edits("j6")
assert svc._preview_edits_cache is not None
finally:
session.close()
Base.metadata.drop_all(bind=engine)
# -- _compute_key_hash --
def test_compute_key_hash(self):
"""_compute_key_hash returns deterministic 16-char hex."""
from src.plugins.translate._run_service import RunExecutionService
h1 = RunExecutionService._compute_key_hash({"a": 1, "b": 2})
h2 = RunExecutionService._compute_key_hash({"a": 1, "b": 2})
h3 = RunExecutionService._compute_key_hash({"a": 1, "b": 3})
assert h1 == h2
assert h1 != h3
assert len(h1) == 16
# -- _update_language_stats_incremental --
def test_update_language_stats_empty(self):
"""No records -> no stats update (no-op)."""
session, engine = make_session()
try:
svc = self._make_service(session)
stats_map = {"fr": TranslationRunLanguageStats(run_id="r1", language_code="fr")}
# No records for run-1, should be a no-op
svc._update_language_stats_incremental("r1", stats_map)
# Just check it doesn't raise
finally:
session.close()
Base.metadata.drop_all(bind=engine)
def test_update_language_stats_with_records(self):
"""Update stats from records."""
session, engine = make_session()
try:
run = TranslationRun(id="stats-run", job_id="no-job", status="COMPLETED",
started_at=datetime.now(UTC))
session.add(run)
session.flush()
batch = TranslationBatch(id="stats-batch", run_id="stats-run", batch_index=0,
status="COMPLETED", created_at=datetime.now(UTC))
session.add(batch)
session.flush()
rec = TranslationRecord(id="stats-rec", run_id="stats-run", batch_id="stats-batch",
status="SUCCESS")
session.add(rec)
session.flush()
lang = TranslationLanguage(id="l1", record_id="stats-rec",
language_code="fr", status="translated",
translated_value="bonjour")
session.add(lang)
session.commit()
svc = self._make_service(session)
lang_stat = TranslationRunLanguageStats(run_id="stats-run", language_code="fr",
total_rows=0, translated_rows=0)
stats_map = {"fr": lang_stat}
svc._update_language_stats_incremental("stats-run", stats_map)
assert lang_stat.total_rows == 1
assert lang_stat.translated_rows == 1
assert lang_stat.failed_rows == 0
assert lang_stat.skipped_rows == 0
finally:
session.close()
Base.metadata.drop_all(bind=engine)
def test_update_language_stats_multiple_statuses(self):
"""Multiple language entries with different statuses."""
session, engine = make_session()
try:
run = TranslationRun(id="stats-run-2", job_id="no-job", status="COMPLETED",
started_at=datetime.now(UTC))
session.add(run)
session.flush()
batch = TranslationBatch(id="stats-batch-2", run_id="stats-run-2", batch_index=0,
status="COMPLETED", created_at=datetime.now(UTC))
session.add(batch)
session.flush()
# One record per unique (record_id, language_code) to avoid UNIQUE constraint
rec_fr_translated = TranslationRecord(id="stats-rec-fr-t", run_id="stats-run-2",
batch_id="stats-batch-2", status="SUCCESS")
rec_fr_failed = TranslationRecord(id="stats-rec-fr-f", run_id="stats-run-2",
batch_id="stats-batch-2", status="SUCCESS")
rec_de_skipped = TranslationRecord(id="stats-rec-de-s", run_id="stats-run-2",
batch_id="stats-batch-2", status="SUCCESS")
rec_de_translated = TranslationRecord(id="stats-rec-de-t", run_id="stats-run-2",
batch_id="stats-batch-2", status="SUCCESS")
session.add_all([rec_fr_translated, rec_fr_failed, rec_de_skipped, rec_de_translated])
session.flush()
entries_data = [
("stats-rec-fr-t", "fr", "translated", "x"),
("stats-rec-fr-f", "fr", "failed", None),
("stats-rec-de-s", "de", "skipped", None),
("stats-rec-de-t", "de", "translated", "x"),
]
for rec_id, code, st, tv in entries_data:
session.add(TranslationLanguage(
id=str(uuid.uuid4()), record_id=rec_id,
language_code=code, status=st,
translated_value=tv,
))
session.commit()
svc = self._make_service(session)
fr_stat = TranslationRunLanguageStats(run_id="stats-run-2", language_code="fr")
de_stat = TranslationRunLanguageStats(run_id="stats-run-2", language_code="de")
stats_map = {"fr": fr_stat, "de": de_stat}
svc._update_language_stats_incremental("stats-run-2", stats_map)
assert fr_stat.total_rows == 2
assert fr_stat.translated_rows == 1
assert fr_stat.failed_rows == 1
assert de_stat.total_rows == 2
assert de_stat.translated_rows == 1
assert de_stat.skipped_rows == 1
finally:
session.close()
Base.metadata.drop_all(bind=engine)