cmvr_edge_ai/tests/unit/test_config.py
2026-07-20 16:59:37 +08:00

146 lines
4.1 KiB
Python

from __future__ import annotations
import pytest
from pydantic import ValidationError
from cmvr_edge_ai.config import (
MissingEnvironmentVariable,
load_config_data,
load_config_text,
)
def test_load_config_expands_environment_at_every_level() -> None:
config = load_config_text(
"""
api_version: cmvr.edge.ai/v1
runtime:
thread_workers: ${EDGE_THREADS:-2}
endpoints:
robot:
transport: GRPC
target: env://CMVR_TARGET
platform:
transport: http
base_url: ${PLATFORM_URL:-http://127.0.0.1:8080}
pipelines:
detection:
nodes:
source:
uses: core.sequence_source@1
with:
items: [env://SAMPLE_ITEM]
""",
environ={
"CMVR_TARGET": "192.168.0.10:50052",
"EDGE_THREADS": "3",
"SAMPLE_ITEM": "frame-1",
},
)
assert config.runtime.thread_workers == 3
assert config.endpoints["robot"].transport == "grpc"
assert config.endpoints["robot"].target == "192.168.0.10:50052"
assert config.endpoints["platform"].base_url == "http://127.0.0.1:8080"
assert config.pipelines["detection"].nodes["source"].params["items"] == ["frame-1"]
def test_load_config_rejects_missing_required_environment_variable() -> None:
with pytest.raises(MissingEnvironmentVariable, match="CMVR_TARGET"):
load_config_text(
"""
api_version: cmvr.edge.ai/v1
endpoints:
robot:
transport: grpc
target: ${CMVR_TARGET}
pipelines:
smoke:
nodes:
source:
uses: core.sequence_source@1
""",
environ={},
)
def test_config_rejects_edges_to_unknown_nodes() -> None:
with pytest.raises(ValidationError, match="references unknown node"):
load_config_data(
{
"api_version": "cmvr.edge.ai/v1",
"pipelines": {
"invalid": {
"nodes": {
"source": {"uses": "test.source@1"},
},
"edges": [{"from": "source.output", "to": "missing.input"}],
}
},
}
)
def test_config_rejects_cycles_before_runtime_construction() -> None:
with pytest.raises(ValidationError, match="must be acyclic"):
load_config_data(
{
"api_version": "cmvr.edge.ai/v1",
"pipelines": {
"cyclic": {
"nodes": {
"first": {"uses": "test.operator@1"},
"second": {"uses": "test.operator@1"},
},
"edges": [
{"from": "first.output", "to": "second.input"},
{"from": "second.output", "to": "first.input"},
],
}
},
}
)
def test_config_models_forbid_unknown_fields() -> None:
with pytest.raises(ValidationError, match="extra_forbidden"):
load_config_data(
{
"api_version": "cmvr.edge.ai/v1",
"unexpected": True,
"pipelines": {
"smoke": {"nodes": {"source": {"uses": "test.source@1"}}}
},
}
)
@pytest.mark.parametrize(
("endpoint", "message"),
[
({"transport": "grpc"}, "gRPC endpoint requires target"),
({"transport": "http"}, "HTTP endpoint requires base_url"),
(
{
"transport": "http",
"base_url": "https://platform.example",
"tls": False,
},
"HTTP endpoint must not set tls",
),
],
)
def test_builtin_endpoints_reject_incomplete_or_ambiguous_addresses(
endpoint: dict[str, object], message: str
) -> None:
with pytest.raises(ValidationError, match=message):
load_config_data(
{
"api_version": "cmvr.edge.ai/v1",
"endpoints": {"external": endpoint},
"pipelines": {
"smoke": {"nodes": {"source": {"uses": "test.source@1"}}}
},
}
)