mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-16 09:25:48 -07:00
Assembly Management Rework - Stage 5 & 6 Complete (Stage 4 Pending)
This commit is contained in:
152
Data/Engine/tests/assemblies/test_cache.py
Normal file
152
Data/Engine/tests/assemblies/test_cache.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# ======================================================
|
||||
# Data\Engine\tests\assemblies\test_cache.py
|
||||
# Description: Validates AssemblyCache lifecycle behaviour, including dirty flags and background flushing.
|
||||
#
|
||||
# API Endpoints (if applicable): None
|
||||
# ======================================================
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from typing import Iterator, Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from Data.Engine.assembly_management.databases import AssemblyDatabaseManager
|
||||
from Data.Engine.assembly_management.models import AssemblyDomain
|
||||
from Data.Engine.assembly_management.payloads import PayloadManager
|
||||
from Data.Engine.assembly_management.bootstrap import AssemblyCache
|
||||
from Data.Engine.services.assemblies.service import AssemblyRuntimeService
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def assembly_runtime(tmp_path_factory: pytest.TempPathFactory) -> Iterator[Tuple[AssemblyRuntimeService, AssemblyCache, AssemblyDatabaseManager]]:
|
||||
staging_root = tmp_path_factory.mktemp("assemblies_staging")
|
||||
runtime_root = tmp_path_factory.mktemp("assemblies_runtime")
|
||||
|
||||
db_manager = AssemblyDatabaseManager(staging_root=staging_root, runtime_root=runtime_root, logger=logging.getLogger("test.assemblies.db"))
|
||||
db_manager.initialise()
|
||||
payload_manager = PayloadManager(
|
||||
staging_root=staging_root / "Payloads",
|
||||
runtime_root=runtime_root / "Payloads",
|
||||
logger=logging.getLogger("test.assemblies.payload"),
|
||||
)
|
||||
cache = AssemblyCache(
|
||||
database_manager=db_manager,
|
||||
payload_manager=payload_manager,
|
||||
flush_interval_seconds=5.0,
|
||||
logger=logging.getLogger("test.assemblies.cache"),
|
||||
)
|
||||
service = AssemblyRuntimeService(cache, logger=logging.getLogger("test.assemblies.runtime"))
|
||||
try:
|
||||
yield service, cache, db_manager
|
||||
finally:
|
||||
cache.shutdown(flush=True)
|
||||
|
||||
|
||||
def _script_payload(display_name: str = "Cache Test Script") -> dict:
|
||||
body = 'Write-Host "Hello from cache tests"'
|
||||
encoded = base64.b64encode(body.encode("utf-8")).decode("ascii")
|
||||
return {
|
||||
"domain": "user",
|
||||
"assembly_guid": None,
|
||||
"assembly_kind": "script",
|
||||
"display_name": display_name,
|
||||
"summary": "Cache test fixture payload.",
|
||||
"category": "script",
|
||||
"assembly_type": "powershell",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"sites": {"mode": "all", "values": []},
|
||||
"variables": [],
|
||||
"files": [],
|
||||
"timeout_seconds": 120,
|
||||
"script_encoding": "base64",
|
||||
},
|
||||
"tags": {},
|
||||
"payload": {
|
||||
"version": 1,
|
||||
"name": display_name,
|
||||
"description": "Cache test fixture payload.",
|
||||
"category": "script",
|
||||
"type": "powershell",
|
||||
"script": encoded,
|
||||
"timeout_seconds": 120,
|
||||
"sites": {"mode": "all", "values": []},
|
||||
"variables": [],
|
||||
"files": [],
|
||||
"script_encoding": "base64",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_cache_flush_marks_entries_clean(assembly_runtime) -> None:
|
||||
service, cache, db_manager = assembly_runtime
|
||||
record = service.create_assembly(_script_payload())
|
||||
guid = record["assembly_guid"]
|
||||
|
||||
snapshot = {entry["assembly_guid"]: entry for entry in cache.describe()}
|
||||
assert snapshot[guid]["is_dirty"] == "true"
|
||||
|
||||
cache.flush_now()
|
||||
cache.reload()
|
||||
snapshot = {entry["assembly_guid"]: entry for entry in cache.describe()}
|
||||
assert snapshot[guid]["is_dirty"] == "false"
|
||||
persisted = db_manager.load_all(AssemblyDomain.USER)
|
||||
assert any(item.assembly_guid == guid for item in persisted)
|
||||
|
||||
|
||||
def test_cache_worker_flushes_on_event(assembly_runtime) -> None:
|
||||
service, cache, _db_manager = assembly_runtime
|
||||
record = service.create_assembly(_script_payload(display_name="Worker Flush Script"))
|
||||
guid = record["assembly_guid"]
|
||||
|
||||
cache._flush_event.set() # Trigger worker loop without waiting for full interval.
|
||||
time.sleep(0.2)
|
||||
cache.reload()
|
||||
snapshot = {entry["assembly_guid"]: entry for entry in cache.describe()}
|
||||
assert snapshot[guid]["is_dirty"] == "false"
|
||||
|
||||
|
||||
def test_cache_flush_waits_for_locked_database(assembly_runtime) -> None:
|
||||
service, cache, db_manager = assembly_runtime
|
||||
if cache._worker.is_alive(): # type: ignore[attr-defined]
|
||||
cache._stop_event.set() # type: ignore[attr-defined]
|
||||
cache._flush_event.set() # type: ignore[attr-defined]
|
||||
cache._worker.join(timeout=1.0) # type: ignore[attr-defined]
|
||||
cache._stop_event.clear() # type: ignore[attr-defined]
|
||||
record = service.create_assembly(_script_payload(display_name="Concurrency Script"))
|
||||
guid = record["assembly_guid"]
|
||||
cache.flush_now()
|
||||
|
||||
update_payload = _script_payload(display_name="Concurrency Script")
|
||||
update_payload["assembly_guid"] = guid
|
||||
update_payload["summary"] = "Updated summary after lock."
|
||||
service.update_assembly(guid, update_payload)
|
||||
|
||||
conn = db_manager._open_connection(AssemblyDomain.USER) # type: ignore[attr-defined]
|
||||
cur = conn.cursor()
|
||||
cur.execute("BEGIN IMMEDIATE")
|
||||
|
||||
flush_thread = threading.Thread(target=cache.flush_now)
|
||||
flush_thread.start()
|
||||
|
||||
time.sleep(0.2)
|
||||
snapshot = {entry["assembly_guid"]: entry for entry in cache.describe()}
|
||||
assert snapshot[guid]["is_dirty"] == "true"
|
||||
conn.rollback()
|
||||
conn.close()
|
||||
|
||||
flush_thread.join(timeout=5.0)
|
||||
assert not flush_thread.is_alive(), "Initial flush attempt did not return after releasing database lock."
|
||||
cache.flush_now()
|
||||
cache.reload()
|
||||
snapshot = {entry["assembly_guid"]: entry for entry in cache.describe()}
|
||||
assert snapshot[guid]["is_dirty"] == "false"
|
||||
|
||||
records = db_manager.load_all(AssemblyDomain.USER)
|
||||
summaries = {entry.assembly_guid: entry.summary for entry in records}
|
||||
assert summaries[guid] == "Updated summary after lock."
|
||||
Reference in New Issue
Block a user