svcforge: reference implementation
ci / lint (push) Successful in 1m19s
ci / unit (push) Failing after 1m2s
ci / integration (push) Has been skipped
ci / types (push) Successful in 1m37s
ci / security (push) Failing after 38s
ci / dockerfile (push) Successful in 14s
ci / image (api) (push) Has been skipped
ci / image (reconciler) (push) Has been skipped
ci / image (worker) (push) Has been skipped
ci / bump (push) Has been skipped
ci / lint (push) Successful in 1m19s
ci / unit (push) Failing after 1m2s
ci / integration (push) Has been skipped
ci / types (push) Successful in 1m37s
ci / security (push) Failing after 38s
ci / dockerfile (push) Successful in 14s
ci / image (api) (push) Has been skipped
ci / image (reconciler) (push) Has been skipped
ci / image (worker) (push) Has been skipped
ci / bump (push) Has been skipped
Complete working build of the system learn-python/ teaches. 164 tests, mypy --strict clean, domain coverage 99%.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
"""Every Protocol, with both of its implementations, checked by mypy.
|
||||
|
||||
These functions have no assertions and cannot fail at runtime — the check happens under
|
||||
`mypy --strict`. If FakeProvisioner drifts from Provisioner (a renamed parameter, a changed
|
||||
return type), the type-check fails here rather than the fake quietly diverging from the real
|
||||
adapter and the worker's fast tests proving nothing about production.
|
||||
|
||||
The test bodies exist so pytest runs the imports too: a Protocol satisfied statically but
|
||||
broken at import time is still broken.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from svcforge_core.adapters.clock import Clock, SystemClock
|
||||
from svcforge_core.adapters.helm import HelmProvisioner, Provisioner
|
||||
from svcforge_core.adapters.notify import LogNotifier, Notifier, WebhookNotifier
|
||||
from svcforge_core.adapters.redis import (
|
||||
IdempotencyStore,
|
||||
IdempotencyStoreProto,
|
||||
InstanceCache,
|
||||
InstanceCacheProto,
|
||||
RateLimiter,
|
||||
RateLimiterProto,
|
||||
make_redis,
|
||||
)
|
||||
from svcforge_core.settings import Settings
|
||||
from tests.fakes import (
|
||||
FakeClock,
|
||||
FakeIdempotencyStore,
|
||||
FakeInstanceCache,
|
||||
FakeNotifier,
|
||||
FakeProvisioner,
|
||||
FakeRateLimiter,
|
||||
)
|
||||
|
||||
|
||||
def take(p: Provisioner) -> None:
|
||||
"""Accepts anything structurally a Provisioner. The whole assertion is the annotation."""
|
||||
|
||||
|
||||
def take_clock(c: Clock) -> None: ...
|
||||
|
||||
|
||||
def take_notifier(n: Notifier) -> None: ...
|
||||
|
||||
|
||||
def test_provisioner_implementations_conform() -> None:
|
||||
take(HelmProvisioner(kubeconfig=Path("/dev/null")))
|
||||
take(FakeProvisioner())
|
||||
print("ok")
|
||||
|
||||
|
||||
def test_clock_implementations_conform() -> None:
|
||||
from datetime import UTC, datetime
|
||||
|
||||
take_clock(SystemClock())
|
||||
take_clock(FakeClock(start=datetime(2026, 1, 1, tzinfo=UTC)))
|
||||
print("ok")
|
||||
|
||||
|
||||
def test_notifier_implementations_conform() -> None:
|
||||
take_notifier(LogNotifier())
|
||||
take_notifier(WebhookNotifier(url="https://example.invalid/hook"))
|
||||
take_notifier(FakeNotifier())
|
||||
print("ok")
|
||||
|
||||
|
||||
def take_limiter(rl: RateLimiterProto) -> None: ...
|
||||
|
||||
|
||||
def take_idempotency(store: IdempotencyStoreProto) -> None: ...
|
||||
|
||||
|
||||
def take_cache(c: InstanceCacheProto) -> None: ...
|
||||
|
||||
|
||||
def test_redis_implementations_conform() -> None:
|
||||
"""A client at a closed port is still a Redis. Nothing here connects — no commands billed.
|
||||
|
||||
That is not a trick to keep the test fast; it is the module's thesis restated as a
|
||||
fixture. Every one of these classes has to be constructible and callable with Redis
|
||||
unreachable, because that is the state they are designed for.
|
||||
"""
|
||||
from datetime import UTC, datetime
|
||||
|
||||
settings = Settings(
|
||||
# pydantic parses these strings into PostgresDsn/RedisDsn at runtime; the
|
||||
# annotation names the parsed type, so the ignores sit on the arguments.
|
||||
pg_dsn="postgresql://unused:unused@127.0.0.1:5432/unused", # type: ignore[arg-type]
|
||||
redis_dsn="redis://127.0.0.1:1/0", # type: ignore[arg-type]
|
||||
)
|
||||
r = make_redis(settings)
|
||||
assert r is not None
|
||||
|
||||
take_limiter(RateLimiter(r, limit=10, window_s=60))
|
||||
take_limiter(FakeRateLimiter(10, 60, FakeClock(start=datetime(2026, 1, 1, tzinfo=UTC))))
|
||||
take_idempotency(IdempotencyStore(r))
|
||||
take_idempotency(FakeIdempotencyStore())
|
||||
take_cache(InstanceCache(r))
|
||||
take_cache(FakeInstanceCache())
|
||||
|
||||
# redis_dsn=None EXPLICITLY. Omitting it does not mean "unset": pydantic-settings
|
||||
# reads SVCFORGE_REDIS_DSN from the environment, so on any machine that has the real
|
||||
# DSN exported this assertion sees a live Upstash client and fails — a green test that
|
||||
# depends on your shell being empty is not a test.
|
||||
assert make_redis(Settings(pg_dsn=settings.pg_dsn, redis_dsn=None)) is None
|
||||
print("ok")
|
||||
Reference in New Issue
Block a user