104 lines
4.0 KiB
Python
104 lines
4.0 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from scimesh.worker.config import WorkerConfig
|
|
from scimesh.worker.coordinator import CoordinatorTransientError
|
|
from scimesh.worker.daemon import WorkerDaemon
|
|
from scimesh.worker.models import ClaimedTask, InputArtifact, ProducedArtifact, RunResult
|
|
|
|
|
|
class FakeCoordinator:
|
|
def __init__(self, task: ClaimedTask | None) -> None:
|
|
self.task, self.submissions = task, []
|
|
|
|
def claim(self, worker_id: str, capabilities: tuple[str, ...]) -> ClaimedTask | None:
|
|
task, self.task = self.task, None
|
|
return task
|
|
|
|
def submit(self, task: ClaimedTask, payload: dict) -> None:
|
|
self.submissions.append(payload)
|
|
|
|
|
|
class FakeArtifacts:
|
|
def __init__(self, content: bytes) -> None:
|
|
self.content, self.uploaded = content, []
|
|
|
|
def download(self, uri: str, destination: Path) -> None:
|
|
destination.write_bytes(self.content)
|
|
|
|
def upload(self, task: ClaimedTask, artifact: ProducedArtifact) -> str:
|
|
self.uploaded.append(artifact.path)
|
|
return f"https://example.test/results/{artifact.path.name}"
|
|
|
|
|
|
class FakeRunner:
|
|
def __init__(self) -> None:
|
|
self.calls = 0
|
|
|
|
def run(self, task: ClaimedTask, task_dir: Path) -> RunResult:
|
|
self.calls += 1
|
|
output = task_dir / "result.csv"
|
|
output.write_text("id,score\na,1\n", encoding="utf-8")
|
|
return RunResult((ProducedArtifact(output, "text/csv"),), {"processed_rows": 1})
|
|
|
|
|
|
def make_task(content: bytes, checksum: str | None = None) -> ClaimedTask:
|
|
return ClaimedTask("task-1", 1, "2026-07-30T00:00:00Z", "similarity-search", InputArtifact("https://example.test/input", checksum or hashlib.sha256(content).hexdigest()), {"query_id": "CHEMBL1"})
|
|
|
|
|
|
def daemon(tmp_path: Path, task: ClaimedTask | None, content: bytes):
|
|
coordinator, artifacts, runner = FakeCoordinator(task), FakeArtifacts(content), FakeRunner()
|
|
config = WorkerConfig("https://example.test", "worker-1", tmp_path / "work")
|
|
return WorkerDaemon(config, coordinator, artifacts, runner), coordinator, artifacts, runner, config
|
|
|
|
|
|
def test_claims_runs_uploads_and_submits_csv(tmp_path: Path) -> None:
|
|
content = b"input fixture"
|
|
worker, coordinator, artifacts, runner, _ = daemon(tmp_path, make_task(content), content)
|
|
assert worker.run_once() is True
|
|
assert runner.calls == 1
|
|
assert len(artifacts.uploaded) == 1
|
|
assert coordinator.submissions[0]["status"] == "completed"
|
|
assert coordinator.submissions[0]["result"]["content_type"] == "text/csv"
|
|
|
|
|
|
def test_no_task_does_not_create_directory(tmp_path: Path) -> None:
|
|
worker, _, _, runner, config = daemon(tmp_path, None, b"")
|
|
assert worker.run_once() is False
|
|
assert runner.calls == 0
|
|
assert not config.work_dir.exists()
|
|
|
|
|
|
def test_bad_checksum_reports_failure_without_running(tmp_path: Path) -> None:
|
|
worker, coordinator, _, runner, _ = daemon(tmp_path, make_task(b"actual", "not-the-hash"), b"actual")
|
|
assert worker.run_once() is True
|
|
assert runner.calls == 0
|
|
assert coordinator.submissions[0]["status"] == "failed"
|
|
assert coordinator.submissions[0]["error_code"] == "ValueError"
|
|
|
|
|
|
def test_transient_claim_error_is_propagated_for_bounded_backoff(tmp_path: Path) -> None:
|
|
class UnavailableCoordinator(FakeCoordinator):
|
|
def claim(self, worker_id: str, capabilities: tuple[str, ...]) -> ClaimedTask | None:
|
|
raise CoordinatorTransientError("temporary outage")
|
|
|
|
worker, _, _, _, _ = daemon(tmp_path, None, b"")
|
|
worker.coordinator = UnavailableCoordinator(None)
|
|
with pytest.raises(CoordinatorTransientError):
|
|
worker.run_once()
|
|
|
|
|
|
def test_task_directories_are_retained_until_cleanup_is_enabled(tmp_path: Path) -> None:
|
|
content = b"input fixture"
|
|
worker, _, _, _, config = daemon(tmp_path, make_task(content), content)
|
|
worker.run_once()
|
|
task_dir = config.work_dir / "task-1" / "1"
|
|
assert task_dir.is_dir()
|
|
worker.config = WorkerConfig(**{**config.__dict__, "cleanup_after_seconds": 0})
|
|
worker._cleanup_expired_directories()
|
|
assert not task_dir.exists()
|