Files
site/WESP_REL/tests/test_sync_dual_instance.py
T
2026-07-17 12:57:18 +03:00

237 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Sync с двумя независимыми терминалами: pull → apply → confirm на обоих."""
from __future__ import annotations
import time
import unittest
import uuid
from datetime import datetime
from sqlalchemy import select
from app import db
from app.models import (
FeedDispenser,
FeedingPeriod,
Ingredient,
PeriodRecipe,
Recipe,
SyncQueue,
)
from app.services.recipe_update_service import update_recipe_from_payload
from app.services.sync_manager import enqueue_sync_queue_task
from tests.helpers.mill_recipe_fixtures import (
create_recipe_with_children,
ingredient_payload_from_row,
recipe_update_payload,
)
from tests.helpers.sync_dual_harness import SyncDualInstanceHarness
class SyncDualInstanceTests(unittest.TestCase):
def setUp(self) -> None:
self.harness = SyncDualInstanceHarness()
self.harness.start()
def tearDown(self) -> None:
self.harness.stop()
def test_recipe_create_reaches_both_terminals(self) -> None:
with self.harness.server_ctx():
recipe_id, _, _ = create_recipe_with_children(name="Dual create")
self.harness.drain_sync()
for term in ("a", "b"):
row = self.harness.recipe_on_terminal(term, recipe_id)
self.assertIsNotNone(row, msg=f"terminal {term}")
self.assertEqual(row.name, "Dual create")
def test_ingredient_delete_reaches_both_terminals(self) -> None:
with self.harness.server_ctx():
recipe_id, ing_ids, _ = create_recipe_with_children(with_groups=False)
recipe = db.session.get(Recipe, recipe_id)
remove_id, keep_id = ing_ids[0], ing_ids[1]
keep_row = db.session.get(Ingredient, keep_id)
update_recipe_from_payload(
recipe_id,
recipe_update_payload(
recipe,
deleted_ingredient_ids=[remove_id],
ingredients=[ingredient_payload_from_row(keep_row, order=1)],
groups=[],
),
)
db.session.commit()
self.harness.drain_sync()
for term in ("a", "b"):
active = self.harness.active_ingredient_ids(term, recipe_id)
self.assertEqual(active, [keep_id], msg=f"terminal {term}")
with self.harness.terminal_ctx(term):
deleted = db.session.get(Ingredient, remove_id)
self.assertTrue(deleted.is_deleted)
def test_recipe_update_reaches_both_terminals(self) -> None:
with self.harness.server_ctx():
recipe_id, _, _ = create_recipe_with_children(name="Before update")
recipe = db.session.get(Recipe, recipe_id)
update_recipe_from_payload(
recipe_id,
recipe_update_payload(
recipe,
ingredients=[],
groups=[],
)
| {"name": "After dual sync"},
)
db.session.commit()
self.harness.drain_sync()
for term in ("a", "b"):
row = self.harness.recipe_on_terminal(term, recipe_id)
self.assertEqual(row.name, "After dual sync", msg=f"terminal {term}")
def test_mill_global_delete_reaches_both_terminals(self) -> None:
with self.harness.server_ctx():
recipe_id, ing_ids, _ = create_recipe_with_children(with_groups=False)
client = self.harness.server_test_client()
resp = client.delete(f"/api/recipes/{recipe_id}")
self.assertEqual(resp.status_code, 200)
self.harness.drain_sync()
for term in ("a", "b"):
with self.harness.terminal_ctx(term):
recipe = db.session.get(Recipe, recipe_id)
self.assertIsNotNone(recipe)
self.assertTrue(recipe.is_deleted)
for ing_id in ing_ids:
ing = db.session.get(Ingredient, ing_id)
self.assertTrue(ing.is_deleted, msg=f"ing {ing_id} term {term}")
def test_period_recipes_reorder_reaches_both_terminals(self) -> None:
disp_id = "dual-disp-1"
period_id = "dual-period-1"
r1, r2 = "dual-r1", "dual-r2"
with self.harness.server_ctx():
db.session.add_all(
[
FeedDispenser(
id=disp_id,
name="D",
farm="F",
operator="O",
type="dispenser",
content_hash="",
),
FeedingPeriod(id=period_id, name="P", dispenser_id=disp_id),
Recipe(id=r1, name="R1", heads_per_trip=1, mixing_time=0, content_hash=""),
Recipe(id=r2, name="R2", heads_per_trip=1, mixing_time=0, content_hash=""),
]
)
db.session.flush()
now = datetime.now()
for rid, ord_ in ((r1, 1), (r2, 2)):
db.session.add(
PeriodRecipe(
period_id=period_id,
recipe_id=rid,
order=ord_,
created_at=now,
created_by="system",
updated_by="system",
)
)
db.session.commit()
for rid in (r1, r2):
enqueue_sync_queue_task("recipe", rid, "create", priority=2, target_node_id=None)
enqueue_sync_queue_task(
"period_recipes", f"{period_id}:{r1}", "create", priority=2, target_node_id=None
)
enqueue_sync_queue_task(
"period_recipes", f"{period_id}:{r2}", "create", priority=2, target_node_id=None
)
db.session.commit()
self.harness.drain_sync()
client = self.harness.server_test_client()
move = client.put(
f"/api/recipes/{r1}/move",
json={"period_id": period_id, "from_index": 0, "to_index": 1},
)
self.assertEqual(move.status_code, 200, move.get_json())
time.sleep(0.25)
self.harness.drain_sync()
for term in ("a", "b"):
order = self.harness.period_recipe_order(term, period_id)
self.assertEqual(order, [r2, r1], msg=f"terminal {term}")
def test_confirm_waits_for_both_terminals(self) -> None:
with self.harness.server_ctx():
recipe_id, _, _ = create_recipe_with_children(name="Confirm gate")
task = self.harness.latest_task("recipe", recipe_id, "create")
self.assertIsNotNone(task)
task_id = task.id
self.harness.sync_terminal("a")
self.assertEqual(self.harness.task_status(task_id), "processing")
self.harness.sync_terminal("b")
self.assertEqual(self.harness.task_status(task_id), "completed")
def test_late_join_terminal_catches_up(self) -> None:
with self.harness.server_ctx():
recipe_id, _, _ = create_recipe_with_children(name="Late join")
self.harness.sync_terminal("a")
self.harness.sync_terminal("a")
row_b_before = self.harness.recipe_on_terminal("b", recipe_id)
self.assertIsNone(row_b_before)
self.harness.sync_terminal("b")
self.harness.sync_terminal("b")
row_b_after = self.harness.recipe_on_terminal("b", recipe_id)
self.assertIsNotNone(row_b_after)
self.assertEqual(row_b_after.name, "Late join")
def test_offline_terminal_catches_up_after_changes(self) -> None:
with self.harness.server_ctx():
recipe_id, ing_ids, _ = create_recipe_with_children(with_groups=False)
recipe = db.session.get(Recipe, recipe_id)
self.harness.sync_both(rounds=2)
with self.harness.server_ctx():
remove_id = ing_ids[0]
keep_row = db.session.get(Ingredient, ing_ids[1])
update_recipe_from_payload(
recipe_id,
recipe_update_payload(
recipe,
deleted_ingredient_ids=[remove_id],
ingredients=[ingredient_payload_from_row(keep_row, order=1)],
groups=[],
),
)
db.session.commit()
self.harness.drain_sync()
for term in ("a", "b"):
self.assertEqual(
len(self.harness.active_ingredient_ids(term, recipe_id)),
1,
msg=f"terminal {term}",
)
if __name__ == "__main__":
unittest.main()