50c2fe2a1e
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%.
110 lines
3.7 KiB
Python
110 lines
3.7 KiB
Python
"""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")
|