324 lines
14 KiB
Python
324 lines
14 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)
|
|
|
|
# -- _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)
|
|
|
|
# -- _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_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 language to avoid unique constraint on (record_id, language_code)
|
|
rec_fr = TranslationRecord(id="stats-rec-fr", run_id="stats-run-2",
|
|
batch_id="stats-batch-2", status="SUCCESS")
|
|
rec_de = TranslationRecord(id="stats-rec-de", run_id="stats-run-2",
|
|
batch_id="stats-batch-2", status="SUCCESS")
|
|
session.add(rec_fr)
|
|
session.add(rec_de)
|
|
session.flush()
|
|
|
|
for code, st in [("fr", "translated"), ("fr", "failed"), ("de", "skipped"), ("de", "translated")]:
|
|
rec_id = "stats-rec-fr" if code == "fr" else "stats-rec-de"
|
|
session.add(TranslationLanguage(
|
|
id=str(uuid.uuid4()), record_id=rec_id,
|
|
language_code=code, status=st,
|
|
translated_value="x" if st in ("translated",) else None,
|
|
))
|
|
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)
|