Some checks failed
CI / lint-test (push) Successful in 5m50s
CI / skinny-install (aco) (push) Successful in 44s
CI / skinny-install (api) (push) Successful in 27s
CI / skinny-install (bcda) (push) Successful in 26s
CI / skinny-install (bib) (push) Successful in 24s
CI / skinny-install (bls) (push) Successful in 23s
CI / skinny-install (ccw) (push) Successful in 23s
CI / skinny-install (cli) (push) Successful in 42s
CI / skinny-install (cms) (push) Successful in 22s
CI / skinny-install (conf) (push) Successful in 24s
CI / skinny-install (pfs) (push) Successful in 27s
CI / skinny-install (rex) (push) Successful in 21s
Infra CI / notebooks (push) Successful in 8s
Infra CI / zotero (push) Failing after 6s
Infra CI / docs (push) Successful in 6s
Infra CI / api (push) Successful in 11s
Infra CI / mc (push) Successful in 6s
Package Supply Chain / pkg-supply-chain (push) Failing after 27s
Deploy / build-scan-report (push) Successful in 3m36s
168 lines
5.0 KiB
Python
168 lines
5.0 KiB
Python
"""Tests for conf — centralised configuration loader."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from conf import ROOT, cfg, context, path, reload, secret
|
|
|
|
|
|
class TestCfg:
|
|
def test_root_is_repo_root(self) -> None:
|
|
assert (ROOT / "stack.toml").exists()
|
|
assert (ROOT / "pyproject.toml").exists()
|
|
|
|
def test_db_section_exists(self) -> None:
|
|
assert "db" in cfg
|
|
|
|
def test_db_aco_value(self) -> None:
|
|
assert cfg.db.aco == "data/aco.duckdb"
|
|
|
|
def test_db_bib_value(self) -> None:
|
|
assert cfg.db.bib == "data/bib.sqlite"
|
|
|
|
def test_db_zotero_value(self) -> None:
|
|
assert cfg.db.zotero == "data/zotero/data/zotero.sqlite"
|
|
|
|
def test_storage_bcda(self) -> None:
|
|
assert cfg.storage.bcda == "data/bcda"
|
|
|
|
def test_bcda_timeout_is_float(self) -> None:
|
|
assert isinstance(cfg.bcda.timeout, float)
|
|
|
|
def test_bcda_max_retries_is_int(self) -> None:
|
|
assert isinstance(cfg.bcda.max_retries, int)
|
|
|
|
def test_lake_trino_port_is_int(self) -> None:
|
|
assert isinstance(cfg.lake.trino.port, int)
|
|
|
|
def test_dict_access(self) -> None:
|
|
val = cfg["db"]["aco"]
|
|
assert val == "data/aco.duckdb"
|
|
|
|
def test_to_dict(self) -> None:
|
|
d = cfg.db.to_dict()
|
|
assert isinstance(d, dict)
|
|
assert "aco" in d
|
|
|
|
def test_contains(self) -> None:
|
|
assert "db" in cfg
|
|
assert "nonexistent_key" not in cfg
|
|
|
|
def test_repr(self) -> None:
|
|
r = repr(cfg.db)
|
|
assert "aco" in r
|
|
|
|
def test_missing_attr_raises(self) -> None:
|
|
import pytest
|
|
|
|
with pytest.raises(AttributeError):
|
|
cfg.no_such_section
|
|
|
|
|
|
class TestPath:
|
|
def test_path_returns_absolute(self) -> None:
|
|
p = path("db.aco")
|
|
assert isinstance(p, Path)
|
|
assert p.is_absolute()
|
|
|
|
def test_path_resolves_relative_to_root(self) -> None:
|
|
p = path("db.aco")
|
|
assert p == ROOT / "data/aco.duckdb"
|
|
|
|
def test_path_nested_key(self) -> None:
|
|
p = path("db.bib")
|
|
assert p == ROOT / "data/bib.sqlite"
|
|
|
|
|
|
class TestReload:
|
|
def test_reload_does_not_raise(self) -> None:
|
|
reload()
|
|
assert cfg.db.aco == "data/aco.duckdb"
|
|
|
|
|
|
class TestContext:
|
|
def test_default_context_is_local(self) -> None:
|
|
ctx = context()
|
|
assert ctx.db_backend == "duckdb"
|
|
assert ctx.storage_backend == "local"
|
|
|
|
def test_env_var_overrides_active(self, monkeypatch) -> None:
|
|
monkeypatch.setenv("STACK_CONTEXT", "lake")
|
|
reload()
|
|
ctx = context()
|
|
assert ctx.db_backend == "iceberg"
|
|
assert ctx.storage_backend == "s3"
|
|
|
|
def test_databricks_context(self, monkeypatch) -> None:
|
|
monkeypatch.setenv("STACK_CONTEXT", "databricks")
|
|
ctx = context()
|
|
assert ctx.db_backend == "databricks"
|
|
assert ctx.storage_backend == "dbfs"
|
|
|
|
def test_trino_context(self, monkeypatch) -> None:
|
|
monkeypatch.setenv("STACK_CONTEXT", "trino")
|
|
ctx = context()
|
|
assert ctx.db_backend == "trino"
|
|
assert ctx.storage_backend == "s3"
|
|
|
|
def test_unknown_context_raises(self, monkeypatch) -> None:
|
|
import pytest
|
|
|
|
monkeypatch.setenv("STACK_CONTEXT", "nonexistent")
|
|
with pytest.raises(KeyError):
|
|
context()
|
|
|
|
|
|
class TestSecret:
|
|
def test_env_var_takes_precedence(self, monkeypatch) -> None:
|
|
monkeypatch.setenv("STACK_API_SECRET", "from-env")
|
|
assert secret("api.secret", "STACK_API_SECRET") == "from-env"
|
|
|
|
def test_falls_back_to_config(self, monkeypatch) -> None:
|
|
monkeypatch.delenv("STACK_API_SECRET", raising=False)
|
|
# config value is empty string by default
|
|
assert secret("api.secret", "STACK_API_SECRET") == ""
|
|
|
|
def test_reads_non_empty_config(self, monkeypatch) -> None:
|
|
import conf
|
|
|
|
monkeypatch.delenv("_CONF_TEST_SECRET_", raising=False)
|
|
monkeypatch.setitem(conf.cfg._data["api"], "secret", "from-toml")
|
|
result = secret("api.secret", "_CONF_TEST_SECRET_")
|
|
assert result == "from-toml"
|
|
|
|
|
|
class TestApiConfig:
|
|
def test_api_host(self) -> None:
|
|
assert cfg.api.host == "0.0.0.0"
|
|
|
|
def test_api_port(self) -> None:
|
|
assert cfg.api.port == 8000
|
|
|
|
def test_api_workers(self) -> None:
|
|
assert cfg.api.workers == 1
|
|
|
|
|
|
class TestFindRootError:
|
|
def test_raises_when_no_stack_toml(self, tmp_path: Path) -> None:
|
|
import pytest
|
|
|
|
# Monkeypatch __file__ won't work easily, but we can test
|
|
# the function directly with a path that has no stack.toml
|
|
import conf
|
|
from conf import _find_root
|
|
|
|
original = conf.__file__
|
|
try:
|
|
# Point to a deep tmp path with no stack.toml
|
|
deep = tmp_path / "a" / "b" / "c" / "d" / "e"
|
|
deep.mkdir(parents=True)
|
|
fake_init = deep / "__init__.py"
|
|
fake_init.write_text("")
|
|
conf.__file__ = str(fake_init)
|
|
with pytest.raises(FileNotFoundError, match="stack.toml"):
|
|
_find_root()
|
|
finally:
|
|
conf.__file__ = original
|