344 lines
12 KiB
Python
344 lines
12 KiB
Python
"""Step definitions for config_resolution.feature."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from behave import given, then, use_step_matcher, when
|
|
from behave.runner import Context
|
|
|
|
from cleveragents.application.services.config_service import ConfigService
|
|
|
|
use_step_matcher("re")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _get_service(context: Context) -> ConfigService:
|
|
"""Return the test-scoped ConfigService instance."""
|
|
svc: ConfigService = context._config_service
|
|
return svc
|
|
|
|
|
|
def _cleanup(context: Context) -> None:
|
|
"""Clean up temp dir and env vars."""
|
|
tmpdir = getattr(context, "_config_tmpdir", None)
|
|
if tmpdir and os.path.isdir(tmpdir):
|
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
for var in getattr(context, "_env_vars_set", []):
|
|
os.environ.pop(var, None)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Background
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a clean config service test environment")
|
|
def step_clean_config_service_env(context: Context) -> None:
|
|
tmpdir = tempfile.mkdtemp()
|
|
context._config_tmpdir = tmpdir
|
|
tmp_path = Path(tmpdir)
|
|
context._config_service = ConfigService(
|
|
config_dir=tmp_path,
|
|
config_path=tmp_path / "config.toml",
|
|
)
|
|
context._env_vars_set = []
|
|
if not hasattr(context, "_cleanup_handlers"):
|
|
context._cleanup_handlers = []
|
|
context._cleanup_handlers.append(lambda: _cleanup(context))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given(
|
|
r'I have written global config key "(?P<key>[^"]+)" with value "(?P<value>[^"]+)"'
|
|
)
|
|
def step_write_global_config(context: Context, key: str, value: str) -> None:
|
|
svc = _get_service(context)
|
|
data = svc.read_config()
|
|
data[key] = value
|
|
svc.write_config(data)
|
|
|
|
|
|
@given(
|
|
r'I have written project "(?P<project>[^"]+)" config key "(?P<key>[^"]+)" with value "(?P<value>[^"]+)"'
|
|
)
|
|
def step_write_project_config(
|
|
context: Context, project: str, key: str, value: str
|
|
) -> None:
|
|
svc = _get_service(context)
|
|
data = svc.read_config()
|
|
if "project" not in data:
|
|
data["project"] = {}
|
|
proj_data: dict[str, Any] = data["project"]
|
|
if project not in proj_data:
|
|
proj_data[project] = {}
|
|
proj_data[project][key] = value
|
|
svc.write_config(data)
|
|
|
|
|
|
@given(r'I write config data with key "(?P<key>[^"]+)" and value "(?P<value>[^"]+)"')
|
|
def step_write_config_data(context: Context, key: str, value: str) -> None:
|
|
svc = _get_service(context)
|
|
svc.set_value(key, value)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when(r'I resolve config key "(?P<key>[^"]+)" with CLI value "(?P<cli_value>[^"]+)"')
|
|
def step_resolve_key_cli(context: Context, key: str, cli_value: str) -> None:
|
|
svc = _get_service(context)
|
|
context.resolved = svc.resolve(key, cli_value=cli_value)
|
|
|
|
|
|
@when(r'I resolve config key "(?P<key>[^"]+)" for project "(?P<project>[^"]+)"')
|
|
def step_resolve_key_project(context: Context, key: str, project: str) -> None:
|
|
svc = _get_service(context)
|
|
context.resolved = svc.resolve(key, project_name=project)
|
|
|
|
|
|
@when(r'I resolve config key "(?P<key>[^"]+)" with verbose mode')
|
|
def step_resolve_key_verbose(context: Context, key: str) -> None:
|
|
svc = _get_service(context)
|
|
context.resolved = svc.resolve(key, verbose=True)
|
|
|
|
|
|
@when(r'I resolve config key "(?P<key>[^"]+)"')
|
|
def step_resolve_key(context: Context, key: str) -> None:
|
|
svc = _get_service(context)
|
|
context.resolved = svc.resolve(key)
|
|
|
|
|
|
@when(r'I look up the env var for key "(?P<key>[^"]+)"')
|
|
def step_lookup_env_var(context: Context, key: str) -> None:
|
|
svc = _get_service(context)
|
|
context.env_var_name = svc.env_var_for_key(key)
|
|
|
|
|
|
@when(r'I attempt to resolve unknown key "(?P<key>[^"]+)"')
|
|
def step_attempt_resolve_unknown(context: Context, key: str) -> None:
|
|
svc = _get_service(context)
|
|
try:
|
|
svc.resolve(key)
|
|
context.raised_error = None
|
|
except ValueError as exc:
|
|
context.raised_error = exc
|
|
|
|
|
|
@when(r'I attempt to validate unknown key "(?P<key>[^"]+)"')
|
|
def step_attempt_validate_unknown(context: Context, key: str) -> None:
|
|
try:
|
|
ConfigService.validate_key(key)
|
|
context.raised_error = None
|
|
except ValueError as exc:
|
|
context.raised_error = exc
|
|
|
|
|
|
@when(
|
|
r'I attempt to validate type for key "(?P<key>[^"]+)" with value "(?P<value>[^"]+)"'
|
|
)
|
|
def step_attempt_validate_type(context: Context, key: str, value: str) -> None:
|
|
try:
|
|
ConfigService.validate_type(key, value)
|
|
context.raised_error = None
|
|
except (TypeError, ValueError) as exc:
|
|
context.raised_error = exc
|
|
|
|
|
|
@when("I read the config file")
|
|
def step_read_config_file(context: Context) -> None:
|
|
svc = _get_service(context)
|
|
context.config_data = svc.read_config()
|
|
|
|
|
|
@when("I write config data to a new directory")
|
|
def step_write_to_new_dir(context: Context) -> None:
|
|
base = Path(context._config_tmpdir) / "nested" / "subdir"
|
|
context._new_config_dir = base
|
|
svc = ConfigService(config_dir=base, config_path=base / "config.toml")
|
|
svc.write_config({"test_key": "test_value"})
|
|
|
|
|
|
@when("I resolve all keys")
|
|
def step_resolve_all(context: Context) -> None:
|
|
svc = _get_service(context)
|
|
context.all_resolved = svc.resolve_all()
|
|
|
|
|
|
@when(r'I set config value "(?P<key>[^"]+)" to "(?P<value>[^"]+)"')
|
|
def step_set_config_value(context: Context, key: str, value: str) -> None:
|
|
svc = _get_service(context)
|
|
svc.set_value(key, value)
|
|
|
|
|
|
@when(r'I get entry for key "(?P<key>[^"]+)"')
|
|
def step_get_entry(context: Context, key: str) -> None:
|
|
context.entry = ConfigService.get_entry(key)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then(r'the resolved value should be "(?P<value>[^"]+)"')
|
|
def step_resolved_value(context: Context, value: str) -> None:
|
|
assert str(context.resolved.value) == value, (
|
|
f"Expected '{value}', got '{context.resolved.value}'"
|
|
)
|
|
|
|
|
|
@then(r'the resolved source should be "(?P<source>[^"]+)"')
|
|
def step_resolved_source(context: Context, source: str) -> None:
|
|
assert context.resolved.source.value == source, (
|
|
f"Expected source '{source}', got '{context.resolved.source.value}'"
|
|
)
|
|
|
|
|
|
@then(r'the env var name should be "(?P<expected>[^"]+)"')
|
|
def step_env_var_name(context: Context, expected: str) -> None:
|
|
assert context.env_var_name == expected, (
|
|
f"Expected '{expected}', got '{context.env_var_name}'"
|
|
)
|
|
|
|
|
|
@then(r'the config ValueError message should contain "(?P<fragment>[^"]+)"')
|
|
def step_config_value_error_raised(context: Context, fragment: str) -> None:
|
|
assert context.raised_error is not None, "Expected ValueError but none was raised"
|
|
assert isinstance(context.raised_error, ValueError), (
|
|
f"Expected ValueError, got {type(context.raised_error).__name__}"
|
|
)
|
|
assert fragment in str(context.raised_error), (
|
|
f"Expected '{fragment}' in message: {context.raised_error}"
|
|
)
|
|
|
|
|
|
@then(r'the config TypeError message should contain "(?P<fragment>[^"]+)"')
|
|
def step_config_type_error_raised(context: Context, fragment: str) -> None:
|
|
assert context.raised_error is not None, "Expected TypeError but none was raised"
|
|
assert isinstance(context.raised_error, TypeError), (
|
|
f"Expected TypeError, got {type(context.raised_error).__name__}"
|
|
)
|
|
assert fragment in str(context.raised_error), (
|
|
f"Expected '{fragment}' in message: {context.raised_error}"
|
|
)
|
|
|
|
|
|
@then(r"the resolved value should be integer (?P<value>\d+)")
|
|
def step_resolved_int(context: Context, value: str) -> None:
|
|
expected = int(value)
|
|
assert context.resolved.value == expected, (
|
|
f"Expected {expected}, got {context.resolved.value}"
|
|
)
|
|
assert isinstance(context.resolved.value, int), (
|
|
f"Expected int, got {type(context.resolved.value).__name__}"
|
|
)
|
|
|
|
|
|
@then("the resolved value should be boolean true")
|
|
def step_resolved_bool_true(context: Context) -> None:
|
|
assert context.resolved.value is True, (
|
|
f"Expected True, got {context.resolved.value}"
|
|
)
|
|
|
|
|
|
@then("the resolved value should be boolean false")
|
|
def step_resolved_bool_false(context: Context) -> None:
|
|
assert context.resolved.value is False, (
|
|
f"Expected False, got {context.resolved.value}"
|
|
)
|
|
|
|
|
|
@then(r"the resolved value should be float (?P<value>[\d.]+)")
|
|
def step_resolved_float(context: Context, value: str) -> None:
|
|
expected = float(value)
|
|
assert abs(context.resolved.value - expected) < 0.001, (
|
|
f"Expected {expected}, got {context.resolved.value}"
|
|
)
|
|
|
|
|
|
@then(r"the resolution chain should have (?P<count>\d+) entries")
|
|
def step_chain_length(context: Context, count: str) -> None:
|
|
expected = int(count)
|
|
assert len(context.resolved.chain) == expected, (
|
|
f"Expected {expected} entries, got {len(context.resolved.chain)}"
|
|
)
|
|
|
|
|
|
@then(r'the chain should include source "(?P<source>[^"]+)"')
|
|
def step_chain_includes_source(context: Context, source: str) -> None:
|
|
sources = [entry["source"] for entry in context.resolved.chain]
|
|
assert source in sources, f"Expected source '{source}' in chain: {sources}"
|
|
|
|
|
|
@then(r'the registry should contain key "(?P<key>[^"]+)"')
|
|
def step_registry_contains(context: Context, key: str) -> None:
|
|
registry = ConfigService.registry()
|
|
assert key in registry, f"Key '{key}' not found in registry"
|
|
|
|
|
|
@then(
|
|
r'the config data should contain key "(?P<key>[^"]+)" with value "(?P<value>[^"]+)"'
|
|
)
|
|
def step_config_data_contains(context: Context, key: str, value: str) -> None:
|
|
data = context.config_data
|
|
assert key in data, f"Key '{key}' not in config data: {data}"
|
|
assert str(data[key]) == value, (
|
|
f"Expected '{value}' for key '{key}', got '{data[key]}'"
|
|
)
|
|
|
|
|
|
@then("the config directory should exist")
|
|
def step_config_dir_exists(context: Context) -> None:
|
|
assert context._new_config_dir.exists(), (
|
|
f"Config dir {context._new_config_dir} does not exist"
|
|
)
|
|
|
|
|
|
@then("the result should contain all registered keys")
|
|
def step_result_has_all_keys(context: Context) -> None:
|
|
registered = ConfigService.registered_keys()
|
|
for key in registered:
|
|
assert key in context.all_resolved, (
|
|
f"Key '{key}' missing from resolve_all result"
|
|
)
|
|
|
|
|
|
@then(r'the entry should have section "(?P<section>[^"]+)"')
|
|
def step_entry_section(context: Context, section: str) -> None:
|
|
assert context.entry is not None, "Entry should not be None"
|
|
assert context.entry.section == section, (
|
|
f"Expected section '{section}', got '{context.entry.section}'"
|
|
)
|
|
|
|
|
|
@then(r'the entry should have python type "(?P<type_name>[^"]+)"')
|
|
def step_entry_type(context: Context, type_name: str) -> None:
|
|
assert context.entry is not None, "Entry should not be None"
|
|
assert context.entry.python_type.__name__ == type_name, (
|
|
f"Expected type '{type_name}', got '{context.entry.python_type.__name__}'"
|
|
)
|
|
|
|
|
|
@then("the entry should be None")
|
|
def step_entry_is_none(context: Context) -> None:
|
|
assert context.entry is None, f"Expected None, got {context.entry}"
|
|
|
|
|
|
# Restore default step matcher for other step files
|
|
use_step_matcher("parse")
|