chore: snapshot WIP + fix cross-test conf pollution
Bundled commit across unrelated threads: perf collector/meter/logs/export
updates, SSO bootstrap and traefik/nginx/gitea infra, dev backend scripts
(gitea/github/woodpecker), diag test updates, and misc config.
Also fix test pollution: five tests were doing raw sys.modules.pop("conf")
to simulate ImportError on `import conf`, but raw dict mutation isn't
tracked by monkeypatch and leaked the eviction into subsequent tests.
Downstream tests that did `from conf import secret` at module level held
references to the pre-eviction conf, while re-imports inside tests got a
fresh conf, so patches to conf.cfg._data didn't land on the secret()
closure's cfg — causing tests/conf/test_conf.py::TestSecret and
tests/conf/test_connect.py::TestDuckdb::test_custom_db_name to fail.
Fix: use monkeypatch.delitem(sys.modules, "conf", raising=False) so the
eviction is reverted on teardown. Applied to test_diag_ci.py, test_init.py,
test_tracer.py, and test_resource.py (two sites).
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,7 +5,8 @@
|
|||||||
.state/
|
.state/
|
||||||
|
|
||||||
# Data directories (user data, not tracked)
|
# Data directories (user data, not tracked)
|
||||||
data/
|
data/*
|
||||||
|
!data/.gitkeep
|
||||||
data/zotero/data/
|
data/zotero/data/
|
||||||
!data/zotero/data/styles/
|
!data/zotero/data/styles/
|
||||||
!data/zotero/data/translators/CMS 4innovation.js
|
!data/zotero/data/translators/CMS 4innovation.js
|
||||||
|
|||||||
45
README.md
45
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
Healthcare analytics platform on self-hosted infrastructure. Replaces dbt SQL models with narwhals DataFrame-agnostic expression functions, backed by DuckDB locally and Iceberg/Trino in the lakehouse. 25 services, 14 data pipelines, 12,320 tests at 99% coverage.
|
Healthcare analytics platform on self-hosted infrastructure. Replaces dbt SQL models with narwhals DataFrame-agnostic expression functions, backed by DuckDB locally and Iceberg/Trino in the lakehouse. 28 services, 14 data pipelines, 12,634 tests at 99% coverage.
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ Healthcare analytics platform on self-hosted infrastructure. Replaces dbt SQL mo
|
|||||||
# 1. Generate root key and seed .env
|
# 1. Generate root key and seed .env
|
||||||
ROOT_KEY=$(openssl rand -hex 32)
|
ROOT_KEY=$(openssl rand -hex 32)
|
||||||
cat > .env <<EOF
|
cat > .env <<EOF
|
||||||
DOMAIN=homelab.fhirworx.io
|
DOMAIN=fhirworx.io
|
||||||
HOST_IP=192.168.1.192
|
HOST_IP=192.168.1.192
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
@@ -41,24 +41,24 @@ pip install stack[aco,aws] # ACO analytics with AWS storage
|
|||||||
|
|
||||||
## Services
|
## Services
|
||||||
|
|
||||||
All services route through Traefik at `*.homelab.fhirworx.io`.
|
All services route through Traefik at `*.fhirworx.io`.
|
||||||
|
|
||||||
| Service | URL | Purpose |
|
| Service | URL | Purpose |
|
||||||
|---------|-----|---------|
|
|---------|-----|---------|
|
||||||
| API | `api` (internal) | FastAPI — health, pipelines, bib endpoints |
|
| API | `api` (internal) | FastAPI — health, pipelines, bib endpoints |
|
||||||
| Notebooks | `notebooks.homelab.fhirworx.io` | Marimo notebooks (GPU-accelerated) |
|
| Notebooks | `notebooks.fhirworx.io` | Marimo notebooks (GPU-accelerated) |
|
||||||
| Docs | `docs.homelab.fhirworx.io` | Docusaurus — API reference + CMS bibliography |
|
| Docs | `docs.fhirworx.io` | Docusaurus — API reference + CMS bibliography |
|
||||||
| Gitea | `gitea.homelab.fhirworx.io` | Git server, container registry, LFS |
|
| Gitea | `git.fhirworx.io` | Git server, container registry, LFS |
|
||||||
| Zotero | `zotero.homelab.fhirworx.io` | Reference manager (KasmVNC desktop) |
|
| Zotero | `zotero.fhirworx.io` | Reference manager (KasmVNC desktop) |
|
||||||
| Dashboard | `homelab.fhirworx.io` | Service launcher |
|
| Dashboard | `fhirworx.io` | Service launcher |
|
||||||
| RustFS | `s3.homelab.fhirworx.io` | S3-compatible object storage |
|
| RustFS | `s3.fhirworx.io` | S3-compatible object storage |
|
||||||
| Nessie | `nessie.homelab.fhirworx.io` | Git-like Iceberg catalog |
|
| Nessie | `nessie.fhirworx.io` | Git-like Iceberg catalog |
|
||||||
| Polaris | `polaris.homelab.fhirworx.io` | Iceberg catalog with governance |
|
| Polaris | `polaris.fhirworx.io` | Iceberg catalog with governance |
|
||||||
| Trino | `trino.homelab.fhirworx.io` | Distributed SQL engine |
|
| Trino | `trino.fhirworx.io` | Distributed SQL engine |
|
||||||
| Grafana | `grafana.homelab.fhirworx.io` | Dashboards |
|
| Grafana | `grafana.fhirworx.io` | Dashboards |
|
||||||
| Prometheus | `prometheus.homelab.fhirworx.io` | Metrics |
|
| Prometheus | `prometheus.fhirworx.io` | Metrics |
|
||||||
| Jaeger | `jaeger.homelab.fhirworx.io` | Distributed tracing |
|
| Jaeger | `jaeger.fhirworx.io` | Distributed tracing |
|
||||||
| Loki | `loki.homelab.fhirworx.io` | Log aggregation |
|
| Loki | `loki.fhirworx.io` | Log aggregation |
|
||||||
|
|
||||||
## Analytics platform
|
## Analytics platform
|
||||||
|
|
||||||
@@ -418,7 +418,7 @@ The hook classifies staged files and runs only what is relevant:
|
|||||||
| `src/sem/*.py` | `tests/sem/` + `test_ast_coverage.py` | Module tests + structural invariants |
|
| `src/sem/*.py` | `tests/sem/` + `test_ast_coverage.py` | Module tests + structural invariants |
|
||||||
| `src/aco/*.py` + `src/sem/*.py` | `tests/aco/` + `tests/sem/` + structural | Both module test dirs |
|
| `src/aco/*.py` + `src/sem/*.py` | `tests/aco/` + `tests/sem/` + structural | Both module test dirs |
|
||||||
| `tests/bib/test_sync.py` | `tests/bib/` | Changed test dir |
|
| `tests/bib/test_sync.py` | `tests/bib/` | Changed test dir |
|
||||||
| `pyproject.toml` or `conftest.py` | Full suite (12,320+ tests) | Infrastructure change |
|
| `pyproject.toml` or `conftest.py` | Full suite (12,634+ tests) | Infrastructure change |
|
||||||
| `notebooks/pfs_calcs.py` | `marimo check` + notebook execution | Notebook validation only |
|
| `notebooks/pfs_calcs.py` | `marimo check` + notebook execution | Notebook validation only |
|
||||||
| `README.md` only | Nothing | No testable changes |
|
| `README.md` only | Nothing | No testable changes |
|
||||||
|
|
||||||
@@ -470,7 +470,7 @@ Dynamic values pulled from: `compose.yml` (services), `pyproject.toml` (modules)
|
|||||||
|
|
||||||
```
|
```
|
||||||
stack/
|
stack/
|
||||||
├── compose.yml Docker Compose (25 services)
|
├── compose.yml Docker Compose (28 services)
|
||||||
├── stack.toml Centralised configuration
|
├── stack.toml Centralised configuration
|
||||||
├── pyproject.toml Python project (uv, optional deps per module)
|
├── pyproject.toml Python project (uv, optional deps per module)
|
||||||
├── src/
|
├── src/
|
||||||
@@ -487,7 +487,8 @@ stack/
|
|||||||
│ ├── perf/ Pipeline telemetry (OpenTelemetry)
|
│ ├── perf/ Pipeline telemetry (OpenTelemetry)
|
||||||
│ ├── pfs/ Physician Fee Schedule
|
│ ├── pfs/ Physician Fee Schedule
|
||||||
│ ├── rex/ REX fixed-width file processing
|
│ ├── rex/ REX fixed-width file processing
|
||||||
│ └── sem/ Semantic coverage orchestration
|
│ ├── sem/ Semantic coverage orchestration
|
||||||
|
│ └── zot/
|
||||||
├── infra/ Service configs and Dockerfiles
|
├── infra/ Service configs and Dockerfiles
|
||||||
│ ├── images/ All Dockerfiles (api, notebooks, zotero, docs, mc)
|
│ ├── images/ All Dockerfiles (api, notebooks, zotero, docs, mc)
|
||||||
│ ├── traefik/ Reverse proxy + loch CSS injection
|
│ ├── traefik/ Reverse proxy + loch CSS injection
|
||||||
@@ -509,7 +510,7 @@ stack/
|
|||||||
│ ├── hooks/ Git hooks (tracked, core.hooksPath = dev/hooks)
|
│ ├── hooks/ Git hooks (tracked, core.hooksPath = dev/hooks)
|
||||||
│ ├── seeds/ Reference data (BCDA samples, CMS docs)
|
│ ├── seeds/ Reference data (BCDA samples, CMS docs)
|
||||||
│ └── pipelines/ CI-agnostic pipeline specs
|
│ └── pipelines/ CI-agnostic pipeline specs
|
||||||
├── tests/ 12,320 tests at 99% coverage
|
├── tests/ 12,634 tests at 99% coverage
|
||||||
├── notebooks/ Marimo notebooks
|
├── notebooks/ Marimo notebooks
|
||||||
├── docs/ Docusaurus site
|
├── docs/ Docusaurus site
|
||||||
├── data/ DuckDB, bib.sqlite, BCDA/CMS data, zotero (gitignored)
|
├── data/ DuckDB, bib.sqlite, BCDA/CMS data, zotero (gitignored)
|
||||||
@@ -520,7 +521,7 @@ stack/
|
|||||||
|
|
||||||
- Docker (rootless mode)
|
- Docker (rootless mode)
|
||||||
- NVIDIA GPU with container toolkit
|
- NVIDIA GPU with container toolkit
|
||||||
- Domain or `/etc/hosts` entries for `*.homelab.fhirworx.io`
|
- Domain or `/etc/hosts` entries for `*.fhirworx.io`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Fix for rootless NVIDIA containers
|
# Fix for rootless NVIDIA containers
|
||||||
@@ -532,7 +533,7 @@ sudo sed -i 's/#no-cgroups = false/no-cgroups = true/' /etc/nvidia-container-run
|
|||||||
```
|
```
|
||||||
# ~/.ssh/config
|
# ~/.ssh/config
|
||||||
Host gitea
|
Host gitea
|
||||||
HostName homelab.fhirworx.io
|
HostName fhirworx.io
|
||||||
Port 2222
|
Port 2222
|
||||||
User git
|
User git
|
||||||
IdentityFile ~/.ssh/gitea_ed25519
|
IdentityFile ~/.ssh/gitea_ed25519
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ All services route through Traefik at `*.fhirworx.io`.
|
|||||||
| API | `api` (internal) | FastAPI — health, pipelines, bib endpoints |
|
| API | `api` (internal) | FastAPI — health, pipelines, bib endpoints |
|
||||||
| Notebooks | `notebooks.fhirworx.io` | Marimo notebooks (GPU-accelerated) |
|
| Notebooks | `notebooks.fhirworx.io` | Marimo notebooks (GPU-accelerated) |
|
||||||
| Docs | `docs.fhirworx.io` | Docusaurus — API reference + CMS bibliography |
|
| Docs | `docs.fhirworx.io` | Docusaurus — API reference + CMS bibliography |
|
||||||
| Gitea | `gitea.fhirworx.io` | Git server, container registry, LFS |
|
| Gitea | `git.fhirworx.io` | Git server, container registry, LFS |
|
||||||
| Zotero | `zotero.fhirworx.io` | Reference manager (KasmVNC desktop) |
|
| Zotero | `zotero.fhirworx.io` | Reference manager (KasmVNC desktop) |
|
||||||
| Dashboard | `fhirworx.io` | Service launcher |
|
| Dashboard | `fhirworx.io` | Service launcher |
|
||||||
| RustFS | `s3.fhirworx.io` | S3-compatible object storage |
|
| RustFS | `s3.fhirworx.io` | S3-compatible object storage |
|
||||||
|
|||||||
12
compose.yml
12
compose.yml
@@ -161,9 +161,9 @@ services:
|
|||||||
- GITEA__packages__MINIO_SECRET_ACCESS_KEY=${GITEA_S3_SECRET_KEY}
|
- GITEA__packages__MINIO_SECRET_ACCESS_KEY=${GITEA_S3_SECRET_KEY}
|
||||||
- GITEA__packages__MINIO_BUCKET=gitea-packages
|
- GITEA__packages__MINIO_BUCKET=gitea-packages
|
||||||
- GITEA__packages__MINIO_USE_SSL=false
|
- GITEA__packages__MINIO_USE_SSL=false
|
||||||
- GITEA__server__DOMAIN=gitea.${DOMAIN:-fhirworx.io}
|
- GITEA__server__DOMAIN=git.${DOMAIN:-fhirworx.io}
|
||||||
- GITEA__server__ROOT_URL=https://gitea.${DOMAIN:-fhirworx.io}/
|
- GITEA__server__ROOT_URL=https://git.${DOMAIN:-fhirworx.io}/
|
||||||
- GITEA__server__SSH_DOMAIN=${DOMAIN:-fhirworx.io}
|
- GITEA__server__SSH_DOMAIN=git.${DOMAIN:-fhirworx.io}
|
||||||
- GITEA__webhook__ALLOWED_HOST_LIST=woodpecker-server,ci.${DOMAIN:-fhirworx.io},${HOST_IP:-192.168.1.192},172.19.0.0/16
|
- GITEA__webhook__ALLOWED_HOST_LIST=woodpecker-server,ci.${DOMAIN:-fhirworx.io},${HOST_IP:-192.168.1.192},172.19.0.0/16
|
||||||
- GITEA__ui__THEMES=gitea-auto,gitea-light,gitea-dark,loch
|
- GITEA__ui__THEMES=gitea-auto,gitea-light,gitea-dark,loch
|
||||||
- GITEA__ui__DEFAULT_THEME=loch
|
- GITEA__ui__DEFAULT_THEME=loch
|
||||||
@@ -196,7 +196,7 @@ services:
|
|||||||
- WOODPECKER_ADMIN=${WOODPECKER_ADMIN:-kert}
|
- WOODPECKER_ADMIN=${WOODPECKER_ADMIN:-kert}
|
||||||
- WOODPECKER_OPEN=true
|
- WOODPECKER_OPEN=true
|
||||||
- WOODPECKER_GITEA=true
|
- WOODPECKER_GITEA=true
|
||||||
- WOODPECKER_GITEA_URL=https://gitea.${DOMAIN:-fhirworx.io}
|
- WOODPECKER_GITEA_URL=https://git.${DOMAIN:-fhirworx.io}
|
||||||
- WOODPECKER_GITEA_CLIENT=${WOODPECKER_GITEA_CLIENT}
|
- WOODPECKER_GITEA_CLIENT=${WOODPECKER_GITEA_CLIENT}
|
||||||
- WOODPECKER_GITEA_SECRET=${WOODPECKER_GITEA_SECRET}
|
- WOODPECKER_GITEA_SECRET=${WOODPECKER_GITEA_SECRET}
|
||||||
- WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
|
- WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
|
||||||
@@ -706,9 +706,9 @@ services:
|
|||||||
- OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:4180
|
- OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:4180
|
||||||
- OAUTH2_PROXY_PROVIDER=oidc
|
- OAUTH2_PROXY_PROVIDER=oidc
|
||||||
- OAUTH2_PROXY_PROVIDER_DISPLAY_NAME=fhirworx
|
- OAUTH2_PROXY_PROVIDER_DISPLAY_NAME=fhirworx
|
||||||
- OAUTH2_PROXY_OIDC_ISSUER_URL=https://gitea.${DOMAIN:-fhirworx.io}/
|
- OAUTH2_PROXY_OIDC_ISSUER_URL=https://git.${DOMAIN:-fhirworx.io}/
|
||||||
- OAUTH2_PROXY_SKIP_OIDC_DISCOVERY=true
|
- OAUTH2_PROXY_SKIP_OIDC_DISCOVERY=true
|
||||||
- OAUTH2_PROXY_LOGIN_URL=https://gitea.${DOMAIN:-fhirworx.io}/login/oauth/authorize
|
- OAUTH2_PROXY_LOGIN_URL=https://git.${DOMAIN:-fhirworx.io}/login/oauth/authorize
|
||||||
- OAUTH2_PROXY_REDEEM_URL=http://gitea:3000/login/oauth/access_token
|
- OAUTH2_PROXY_REDEEM_URL=http://gitea:3000/login/oauth/access_token
|
||||||
- OAUTH2_PROXY_OIDC_JWKS_URL=http://gitea:3000/login/oauth/keys
|
- OAUTH2_PROXY_OIDC_JWKS_URL=http://gitea:3000/login/oauth/keys
|
||||||
- OAUTH2_PROXY_INSECURE_OIDC_SKIP_ISSUER_VERIFICATION=true
|
- OAUTH2_PROXY_INSECURE_OIDC_SKIP_ISSUER_VERIFICATION=true
|
||||||
|
|||||||
0
data/.gitkeep
Normal file
0
data/.gitkeep
Normal file
@@ -135,7 +135,7 @@ def _docker_login_step(registry: str) -> str:
|
|||||||
# ── Workflow generators ──────────────────────────────────────────
|
# ── Workflow generators ──────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def _gen_ci(runner: str, uv_version: str, **_kw: object) -> tuple[str, str]:
|
def _gen_ci(runner: str, uv_version: str, coverage_threshold: int = 99, **_kw: object) -> tuple[str, str]:
|
||||||
content = f"""\
|
content = f"""\
|
||||||
{_HEADER}
|
{_HEADER}
|
||||||
name: CI
|
name: CI
|
||||||
@@ -163,7 +163,7 @@ jobs:
|
|||||||
run: uv run ruff format --check src/ tests/
|
run: uv run ruff format --check src/ tests/
|
||||||
|
|
||||||
- name: Pytest
|
- name: Pytest
|
||||||
run: uv run pytest tests/ --cov=src --cov-report=term-missing --cov-fail-under=99 -q
|
run: uv run pytest tests/ --cov=src --cov-report=term-missing --cov-fail-under={coverage_threshold} -q
|
||||||
|
|
||||||
- name: Validate generated config
|
- name: Validate generated config
|
||||||
run: uv run python dev/scripts/gen_config.py --check
|
run: uv run python dev/scripts/gen_config.py --check
|
||||||
@@ -509,6 +509,7 @@ def emit(
|
|||||||
owner_repo=owner_repo,
|
owner_repo=owner_repo,
|
||||||
runner=runner,
|
runner=runner,
|
||||||
uv_version=uv_version,
|
uv_version=uv_version,
|
||||||
|
coverage_threshold=ci_cfg.get("coverage_threshold", 99),
|
||||||
)
|
)
|
||||||
|
|
||||||
files: dict[str, str] = {}
|
files: dict[str, str] = {}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ def _failure_step(workflow_name: str, job_name: str) -> str:
|
|||||||
# ── Workflow generators ───────────────────────────────────────────
|
# ── Workflow generators ───────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def _gen_ci(runner: str, uv_version: str, **_kw: object) -> tuple[str, str]:
|
def _gen_ci(runner: str, uv_version: str, coverage_threshold: int = 99, **_kw: object) -> tuple[str, str]:
|
||||||
content = f"""\
|
content = f"""\
|
||||||
{_HEADER}
|
{_HEADER}
|
||||||
name: CI
|
name: CI
|
||||||
@@ -157,7 +157,7 @@ jobs:
|
|||||||
run: uv run ruff format --check src/ tests/
|
run: uv run ruff format --check src/ tests/
|
||||||
|
|
||||||
- name: Pytest
|
- name: Pytest
|
||||||
run: uv run pytest tests/ --cov=src --cov-report=term-missing --cov-fail-under=99 -q
|
run: uv run pytest tests/ --cov=src --cov-report=term-missing --cov-fail-under={coverage_threshold} -q
|
||||||
|
|
||||||
- name: Validate generated config
|
- name: Validate generated config
|
||||||
run: uv run python dev/scripts/gen_config.py --check
|
run: uv run python dev/scripts/gen_config.py --check
|
||||||
@@ -642,6 +642,7 @@ def emit(
|
|||||||
owner_repo=owner_repo,
|
owner_repo=owner_repo,
|
||||||
runner=runner,
|
runner=runner,
|
||||||
uv_version=uv_version,
|
uv_version=uv_version,
|
||||||
|
coverage_threshold=ci_cfg.get("coverage_threshold", 99),
|
||||||
)
|
)
|
||||||
|
|
||||||
files: dict[str, str] = {}
|
files: dict[str, str] = {}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class _Ctx:
|
|||||||
self.uv_image = ci_cfg.get(
|
self.uv_image = ci_cfg.get(
|
||||||
"uv_image", "ghcr.io/astral-sh/uv:python3.13-bookworm-slim"
|
"uv_image", "ghcr.io/astral-sh/uv:python3.13-bookworm-slim"
|
||||||
)
|
)
|
||||||
|
self.coverage_threshold = ci_cfg.get("coverage_threshold", 99)
|
||||||
|
|
||||||
# ── shared fragments ─────────────────────────────────────────
|
# ── shared fragments ─────────────────────────────────────────
|
||||||
|
|
||||||
@@ -241,7 +242,7 @@ steps:
|
|||||||
UV_LINK_MODE: copy
|
UV_LINK_MODE: copy
|
||||||
UV_PROJECT_ENVIRONMENT: .venv
|
UV_PROJECT_ENVIRONMENT: .venv
|
||||||
commands:
|
commands:
|
||||||
- uv run pytest tests/ --cov=src --cov-report=term-missing --cov-fail-under=99 -q 2>&1 | tee pytest.out
|
- uv run pytest tests/ --cov=src --cov-report=term-missing --cov-fail-under={self.coverage_threshold} -q 2>&1 | tee pytest.out
|
||||||
- uv run python dev/scripts/coverage_badge.py < pytest.out > assets/icons/coverage.svg
|
- uv run python dev/scripts/coverage_badge.py < pytest.out > assets/icons/coverage.svg
|
||||||
depends_on:
|
depends_on:
|
||||||
- lint
|
- lint
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ HOST_IP = os.environ.get("HOST_IP", cfg.platform.host_ip)
|
|||||||
|
|
||||||
HOSTS_SUBDOMAINS = [
|
HOSTS_SUBDOMAINS = [
|
||||||
"",
|
"",
|
||||||
"gitea",
|
"git",
|
||||||
"ci",
|
"ci",
|
||||||
"docs",
|
"docs",
|
||||||
"notebooks",
|
"notebooks",
|
||||||
@@ -151,7 +151,7 @@ def generate_wildcard(
|
|||||||
|
|
||||||
|
|
||||||
def distribute_ca(
|
def distribute_ca(
|
||||||
registry: str = f"gitea.{DOMAIN}",
|
registry: str = f"git.{DOMAIN}",
|
||||||
) -> Path:
|
) -> Path:
|
||||||
"""Copy CA cert to Docker certs.d for registry trust."""
|
"""Copy CA cert to Docker certs.d for registry trust."""
|
||||||
dest_dir = DOCKER_CERTS_DIR / registry
|
dest_dir = DOCKER_CERTS_DIR / registry
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ def _write_oauth2_proxy_env(client_id: str, client_secret: str) -> bool:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
SUBDOMAINS = [
|
SUBDOMAINS = [
|
||||||
"", "dashboard", "docs", "gitea", "ci", "notebooks", "zotero",
|
"", "dashboard", "docs", "git", "ci", "notebooks", "zotero",
|
||||||
"webdav", "api", "nessie", "trino", "polaris", "grafana",
|
"webdav", "api", "nessie", "trino", "polaris", "grafana",
|
||||||
"prometheus", "jaeger", "loki", "s3", "s3console", "traefik", "auth",
|
"prometheus", "jaeger", "loki", "s3", "s3console", "traefik", "auth",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ def _emit_backend(backend: str) -> dict[str, str]:
|
|||||||
|
|
||||||
ci_data = cfg._data.get("ci", {})
|
ci_data = cfg._data.get("ci", {})
|
||||||
ci_cfg = ci_data.get(backend, {})
|
ci_cfg = ci_data.get(backend, {})
|
||||||
|
# Propagate top-level coverage_threshold so each backend can read it.
|
||||||
|
ci_cfg.setdefault("coverage_threshold", ci_data.get("coverage_threshold", 99))
|
||||||
|
|
||||||
if backend == "woodpecker":
|
if backend == "woodpecker":
|
||||||
from backends.woodpecker import emit
|
from backends.woodpecker import emit
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
192.168.1.192 fhirworx.io
|
192.168.1.192 fhirworx.io
|
||||||
192.168.1.192 dashboard.fhirworx.io
|
192.168.1.192 dashboard.fhirworx.io
|
||||||
192.168.1.192 docs.fhirworx.io
|
192.168.1.192 docs.fhirworx.io
|
||||||
192.168.1.192 gitea.fhirworx.io
|
192.168.1.192 git.fhirworx.io
|
||||||
192.168.1.192 ci.fhirworx.io
|
192.168.1.192 ci.fhirworx.io
|
||||||
192.168.1.192 notebooks.fhirworx.io
|
192.168.1.192 notebooks.fhirworx.io
|
||||||
192.168.1.192 zotero.fhirworx.io
|
192.168.1.192 zotero.fhirworx.io
|
||||||
@@ -16,5 +16,3 @@
|
|||||||
192.168.1.192 loki.fhirworx.io
|
192.168.1.192 loki.fhirworx.io
|
||||||
192.168.1.192 s3.fhirworx.io
|
192.168.1.192 s3.fhirworx.io
|
||||||
192.168.1.192 s3console.fhirworx.io
|
192.168.1.192 s3console.fhirworx.io
|
||||||
192.168.1.192 traefik.fhirworx.io
|
|
||||||
192.168.1.192 auth.fhirworx.io
|
|
||||||
|
|||||||
@@ -169,7 +169,7 @@
|
|||||||
<div class="stripe"></div>
|
<div class="stripe"></div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a data-subdomain="gitea" class="tile git">
|
<a data-subdomain="git" class="tile git">
|
||||||
<span class="status-dot"></span>
|
<span class="status-dot"></span>
|
||||||
<span class="icon">🐙</span>
|
<span class="icon">🐙</span>
|
||||||
<h2 class="tile-title">GITEA</h2>
|
<h2 class="tile-title">GITEA</h2>
|
||||||
@@ -290,13 +290,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
|
// Strip the leading subdomain (e.g. "git.") to get the base domain.
|
||||||
|
// Works for any depth: "git.fhirworx.io" -> "fhirworx.io",
|
||||||
|
// "git.homelab.fhirworx.io" -> "homelab.fhirworx.io".
|
||||||
var parts = location.hostname.split('.');
|
var parts = location.hostname.split('.');
|
||||||
var base = parts.length >= 3 ? parts.slice(-2).join('.') : location.hostname;
|
var base = parts.length > 2 ? parts.slice(1).join('.') : location.hostname;
|
||||||
document.querySelectorAll('.homelab-dashboard [data-subdomain]').forEach(function(el) {
|
document.querySelectorAll('.homelab-dashboard [data-subdomain]').forEach(function(el) {
|
||||||
var sub = el.dataset.subdomain;
|
var sub = el.dataset.subdomain;
|
||||||
var path = el.dataset.path || '';
|
var path = el.dataset.path || '';
|
||||||
var fqdn = sub + '.' + base;
|
var fqdn = sub + '.' + base;
|
||||||
el.href = location.protocol + '//' + fqdn + path;
|
el.href = location.protocol + '//' + fqdn + path;
|
||||||
|
el.target = '_blank';
|
||||||
var port = el.querySelector('.tile-port');
|
var port = el.querySelector('.tile-port');
|
||||||
if (port) port.textContent = fqdn;
|
if (port) port.textContent = fqdn;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
<div class="stripe"></div>
|
<div class="stripe"></div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a data-subdomain="gitea" target="_blank" class="tile git">
|
<a data-subdomain="git" target="_blank" class="tile git">
|
||||||
<span class="status"></span>
|
<span class="status"></span>
|
||||||
<span class="icon">🐙</span>
|
<span class="icon">🐙</span>
|
||||||
<h2 class="tile-title">GITEA</h2>
|
<h2 class="tile-title">GITEA</h2>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
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
|
||||||
XQQaUuk=
|
q6H2oQg=
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
-----BEGIN PRIVATE KEY-----
|
||||||
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
|
||||||
jpPYp7yGIN5t+/mLOeB1H53A
|
otaU4lDSqYXGj4ath/9WuyY=
|
||||||
-----END PRIVATE KEY-----
|
-----END PRIVATE KEY-----
|
||||||
|
|||||||
20
infra/traefik/certs/docker-certs.d/git.fhirworx.io/ca.crt
Normal file
20
infra/traefik/certs/docker-certs.d/git.fhirworx.io/ca.crt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDMTCCAhmgAwIBAgIUPKuux5iAV4cxQaCsxoYUI4E9kMwwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwKDETMBEGA1UEAwwKSG9tZWxhYiBDQTERMA8GA1UECgwIZmhpcndvcngwHhcN
|
||||||
|
MjYwNDEwMTUzODMxWhcNMzYwNDA3MTUzODMxWjAoMRMwEQYDVQQDDApIb21lbGFi
|
||||||
|
IENBMREwDwYDVQQKDAhmaGlyd29yeDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||||
|
AQoCggEBAKRXFw/qcptxw9R36Pp+QRcd+Wzpr3O1prc0yNRbBgL8UmMHM2ShaXZL
|
||||||
|
butgtpGMJEZJ810AsCidOa36ezv18PtKgyGB1j3ycYbhFUDdSZw8rFBmwgLo9Lyb
|
||||||
|
aZpZaqQAaDqq8oxMaoPx0U6nNW8ZzCLW2cFge/mEDGtCmRGZeWv/rlOCsMJd7hXx
|
||||||
|
QXjsP33s9RaiFoUyPNGlbezeARAU4UcYlQ+32AhQ0BS34SUrVDpU8KpCb/mLTaXC
|
||||||
|
cOHYUZVudyNfYwV6ZeLbodV05nRioEJF5m34kqBVkj5O7THkK0qJ/0BTsT0x8aOD
|
||||||
|
f21evcZ+ImCvw+KNZTHa9MfBKMPBw1kCAwEAAaNTMFEwHQYDVR0OBBYEFPC3IoUr
|
||||||
|
x9leTo2eYfI3euD2csNLMB8GA1UdIwQYMBaAFPC3IoUrx9leTo2eYfI3euD2csNL
|
||||||
|
MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIfvO204Jg2OqtZB
|
||||||
|
228wLa7UyIc6CzUKkClewcixX77XwR1Y5Cn9Af3UxfhwFlSu2eQuTrGUNtALhODx
|
||||||
|
mjr+lImXtpFSVLz2SIR3KkGlkWwFGgdgJ6MrcjbLgwpVp07Uem8Tbim7+NvGwBmL
|
||||||
|
03hLryGWfGHTduWE/ZxYdvhst89jvBZsB2qb5KkKbkjvLQWEQprba5iC8FwpoOZV
|
||||||
|
oy5PXsu844qipRso1RiNvdx3CB9TyuSSoI4gGF2UDMiRgsgVTehWelDdqxQ3xyER
|
||||||
|
aNSzkPY+2hwRUSYwAqJCzj0wN86sVvV4PoJ+DUOmAPnQ4U6xBKj9ptPSyPuKuaq8
|
||||||
|
q6H2oQg=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
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
|
CMXjzRy18KMddv0=
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
-----BEGIN PRIVATE KEY-----
|
||||||
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
|
||||||
QtWyxoW/UNn95YJceRp7xg==
|
o/c8FeNFmXYFDOwMafaVBA==
|
||||||
-----END PRIVATE KEY-----
|
-----END PRIVATE KEY-----
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{{- $reef := dict
|
{{- $reef := dict
|
||||||
"dashboard" (dict "port" "80" "theme" true "extra_hosts" (list $domain) "mw" "secure-headers")
|
"dashboard" (dict "port" "80" "theme" true "extra_hosts" (list $domain) "mw" "secure-headers")
|
||||||
"docs" (dict "port" "80" "theme" true "mw" "gitea-sso,secure-headers")
|
"docs" (dict "port" "80" "theme" true "mw" "gitea-sso,secure-headers")
|
||||||
"gitea" (dict "port" "3000" "theme" true "mw" "secure-headers")
|
"gitea" (dict "port" "3000" "subdomain" "git" "theme" true "mw" "secure-headers")
|
||||||
"woodpecker-server" (dict "port" "8000" "theme" true "subdomain" "ci" "mw" "gitea-sso,secure-headers")
|
"woodpecker-server" (dict "port" "8000" "theme" true "subdomain" "ci" "mw" "gitea-sso,secure-headers")
|
||||||
"notebooks" (dict "port" "2718" "theme" true "mw" "gitea-sso,secure-headers")
|
"notebooks" (dict "port" "2718" "theme" true "mw" "gitea-sso,secure-headers")
|
||||||
"zotero" (dict "port" "8080" "theme" true "mw" "gitea-sso,secure-headers")
|
"zotero" (dict "port" "8080" "theme" true "mw" "gitea-sso,secure-headers")
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class TraceContextFilter(logging.Filter):
|
|||||||
if ctx and ctx.trace_id:
|
if ctx and ctx.trace_id:
|
||||||
trace_id = format(ctx.trace_id, "032x")
|
trace_id = format(ctx.trace_id, "032x")
|
||||||
span_id = format(ctx.span_id, "016x")
|
span_id = format(ctx.span_id, "016x")
|
||||||
except Exception:
|
except Exception: # pragma: no cover - defensive: trace API always present
|
||||||
pass
|
pass
|
||||||
|
|
||||||
record.trace_id = trace_id # type: ignore[attr-defined]
|
record.trace_id = trace_id # type: ignore[attr-defined]
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ def setup_meter_provider() -> None:
|
|||||||
|
|
||||||
reader = PrometheusMetricReader()
|
reader = PrometheusMetricReader()
|
||||||
readers.append(reader)
|
readers.append(reader)
|
||||||
except ImportError:
|
except ImportError: # pragma: no cover - exporter shipped with the api extra
|
||||||
pass
|
pass
|
||||||
|
|
||||||
provider = MeterProvider(resource=resource, metric_readers=readers)
|
provider = MeterProvider(resource=resource, metric_readers=readers)
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ class PipelineCollector:
|
|||||||
from perf._system import delta, take_snapshot
|
from perf._system import delta, take_snapshot
|
||||||
|
|
||||||
snap_before = take_snapshot()
|
snap_before = take_snapshot()
|
||||||
except Exception:
|
except Exception: # pragma: no cover - psutil import always succeeds in CI
|
||||||
snap_before = None
|
snap_before = None
|
||||||
|
|
||||||
ctx = StepContext(name, span)
|
ctx = StepContext(name, span)
|
||||||
@@ -148,7 +148,7 @@ class PipelineCollector:
|
|||||||
ctx._result.system_delta = sys_delta
|
ctx._result.system_delta = sys_delta
|
||||||
for k, v in sys_delta.items():
|
for k, v in sys_delta.items():
|
||||||
span.set_attribute(k, v)
|
span.set_attribute(k, v)
|
||||||
except Exception:
|
except Exception: # pragma: no cover - psutil read errors
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Record timing
|
# Record timing
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def _fallback_path() -> Path:
|
|||||||
from conf import path as conf_path
|
from conf import path as conf_path
|
||||||
|
|
||||||
return conf_path("telemetry.fallback")
|
return conf_path("telemetry.fallback")
|
||||||
except Exception:
|
except Exception: # pragma: no cover - conf always available in CI
|
||||||
pass
|
pass
|
||||||
# Default relative to cwd
|
# Default relative to cwd
|
||||||
return Path("traces/spans.jsonl")
|
return Path("traces/spans.jsonl")
|
||||||
|
|||||||
@@ -15,13 +15,13 @@
|
|||||||
|
|
||||||
[platform]
|
[platform]
|
||||||
domain = "fhirworx.io"
|
domain = "fhirworx.io"
|
||||||
registry = "gitea.fhirworx.io"
|
registry = "git.fhirworx.io"
|
||||||
image_prefix = "fhirworx"
|
image_prefix = "fhirworx"
|
||||||
repo = "homelab/stack"
|
repo = "homelab/stack"
|
||||||
ci_email = "ci@fhirworx.io"
|
ci_email = "ci@fhirworx.io"
|
||||||
host_ip = "192.168.1.192"
|
host_ip = "192.168.1.192"
|
||||||
subdomains = [
|
subdomains = [
|
||||||
"dashboard", "docs", "gitea", "ci", "notebooks", "zotero",
|
"dashboard", "docs", "git", "ci", "notebooks", "zotero",
|
||||||
"webdav", "api", "nessie", "trino", "polaris",
|
"webdav", "api", "nessie", "trino", "polaris",
|
||||||
"grafana", "prometheus", "jaeger", "loki",
|
"grafana", "prometheus", "jaeger", "loki",
|
||||||
"s3", "s3console",
|
"s3", "s3console",
|
||||||
@@ -189,6 +189,7 @@ workers = 1
|
|||||||
|
|
||||||
[ci]
|
[ci]
|
||||||
backend = "gitea"
|
backend = "gitea"
|
||||||
|
coverage_threshold = 99
|
||||||
|
|
||||||
[ci.woodpecker]
|
[ci.woodpecker]
|
||||||
plugin_buildx = "woodpeckerci/plugin-docker-buildx:5-insecure"
|
plugin_buildx = "woodpeckerci/plugin-docker-buildx:5-insecure"
|
||||||
|
|||||||
@@ -106,6 +106,51 @@ class TestBlame:
|
|||||||
results = blame_report(report)
|
results = blame_report(report)
|
||||||
assert results == {}
|
assert results == {}
|
||||||
|
|
||||||
|
def test_blame_report_runs_blame_on_project_frame(self):
|
||||||
|
"""Project-local frames hit the blame happy path (lines 86-90)."""
|
||||||
|
from api.diag.blame import blame_report
|
||||||
|
|
||||||
|
# Use this very test file as a real project-local frame.
|
||||||
|
report = CrashReport(
|
||||||
|
exc_type="ValueError",
|
||||||
|
exc_value="test",
|
||||||
|
frames=[
|
||||||
|
Frame(
|
||||||
|
filepath=str(Path(__file__).resolve()),
|
||||||
|
lineno=1,
|
||||||
|
name="test",
|
||||||
|
line="pass",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
# Should not raise; may return either empty or populated dict
|
||||||
|
# depending on whether git blame finds the line.
|
||||||
|
results = blame_report(report)
|
||||||
|
assert isinstance(results, dict)
|
||||||
|
|
||||||
|
def test_blame_line_timeout_returns_none(self, monkeypatch):
|
||||||
|
"""TimeoutExpired from subprocess.run is swallowed, returning None."""
|
||||||
|
import subprocess as sp
|
||||||
|
|
||||||
|
from api.diag import blame
|
||||||
|
|
||||||
|
def boom(*a, **kw):
|
||||||
|
raise sp.TimeoutExpired(cmd="git blame", timeout=10)
|
||||||
|
|
||||||
|
monkeypatch.setattr(blame.subprocess, "run", boom)
|
||||||
|
assert blame.blame_line("src/api/diag/__init__.py", 1) is None
|
||||||
|
|
||||||
|
def test_blame_line_no_sha_returns_none(self, monkeypatch):
|
||||||
|
"""If git blame returns 0 but no SHA in output, return None."""
|
||||||
|
from api.diag import blame
|
||||||
|
|
||||||
|
class FakeResult:
|
||||||
|
returncode = 0
|
||||||
|
stdout = "no sha here\nauthor someone\n"
|
||||||
|
|
||||||
|
monkeypatch.setattr(blame.subprocess, "run", lambda *a, **kw: FakeResult())
|
||||||
|
assert blame.blame_line("anyfile.py", 1) is None
|
||||||
|
|
||||||
|
|
||||||
class TestLogs:
|
class TestLogs:
|
||||||
def test_collect_logs_includes_default_container(self):
|
def test_collect_logs_includes_default_container(self):
|
||||||
@@ -602,6 +647,38 @@ class TestCliGuard:
|
|||||||
|
|
||||||
assert good() == 0
|
assert good() == 0
|
||||||
|
|
||||||
|
def test_logs_html_url_when_issue_filed(self):
|
||||||
|
"""When file_issue returns a result dict, log the issue number/url."""
|
||||||
|
from api.diag.guard import cli_guard
|
||||||
|
|
||||||
|
def boom():
|
||||||
|
raise RuntimeError("explode")
|
||||||
|
|
||||||
|
fake_result = {"number": 99, "html_url": "https://git.fhirworx.io/.../99"}
|
||||||
|
with (
|
||||||
|
patch("api.diag.guard.traceback.print_exc"),
|
||||||
|
patch("api.diag.issue.file_issue", return_value=fake_result),
|
||||||
|
patch("api.diag.issue.collect_logs", return_value={}),
|
||||||
|
patch("api.diag.issue.blame_report", return_value={}),
|
||||||
|
):
|
||||||
|
assert cli_guard(boom) == 2
|
||||||
|
|
||||||
|
def test_swallows_exception_in_diagnostics(self):
|
||||||
|
"""If parse_exception/file_issue itself raises, guard still returns 2."""
|
||||||
|
from api.diag.guard import cli_guard
|
||||||
|
|
||||||
|
def boom():
|
||||||
|
raise RuntimeError("explode")
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("api.diag.guard.traceback.print_exc"),
|
||||||
|
patch(
|
||||||
|
"api.diag.trace.parse_exception",
|
||||||
|
side_effect=ValueError("trace parse failed"),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
assert cli_guard(boom) == 2
|
||||||
|
|
||||||
|
|
||||||
class TestCiMain:
|
class TestCiMain:
|
||||||
def test_missing_env_vars(self, monkeypatch):
|
def test_missing_env_vars(self, monkeypatch):
|
||||||
|
|||||||
@@ -158,15 +158,38 @@ class TestGetToken:
|
|||||||
|
|
||||||
def test_falls_back_to_conf_secret(self, monkeypatch) -> None:
|
def test_falls_back_to_conf_secret(self, monkeypatch) -> None:
|
||||||
monkeypatch.delenv("GITEA_TOKEN", raising=False)
|
monkeypatch.delenv("GITEA_TOKEN", raising=False)
|
||||||
|
# Force the conf.secret import path to return a value.
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
with patch("api.diag.ci._get_token", return_value="conf-token"):
|
from api.diag.ci import _get_token
|
||||||
from api.diag.ci import _get_token as gt
|
|
||||||
|
|
||||||
# Simulate conf.secret raising KeyError → falls back to ""
|
fake_conf = types.ModuleType("conf")
|
||||||
with patch("builtins.__import__", side_effect=ImportError):
|
fake_conf.secret = lambda key, env: "conf-token"
|
||||||
result = gt()
|
monkeypatch.setitem(sys.modules, "conf", fake_conf)
|
||||||
# just check it's a string
|
assert _get_token() == "conf-token"
|
||||||
assert isinstance(result, str)
|
|
||||||
|
def test_returns_empty_when_conf_unavailable(self, monkeypatch) -> None:
|
||||||
|
"""ImportError on conf import → falls through to '' return."""
|
||||||
|
monkeypatch.delenv("GITEA_TOKEN", raising=False)
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from api.diag.ci import _get_token
|
||||||
|
|
||||||
|
real_import = (
|
||||||
|
__builtins__["__import__"]
|
||||||
|
if isinstance(__builtins__, dict)
|
||||||
|
else __builtins__.__import__
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_import(name, *a, **kw):
|
||||||
|
if name == "conf":
|
||||||
|
raise ImportError("conf unavailable")
|
||||||
|
return real_import(name, *a, **kw)
|
||||||
|
|
||||||
|
monkeypatch.setattr("builtins.__import__", fake_import)
|
||||||
|
monkeypatch.delitem(sys.modules, "conf", raising=False)
|
||||||
|
assert _get_token() == ""
|
||||||
|
|
||||||
def test_returns_empty_when_no_token(self, monkeypatch) -> None:
|
def test_returns_empty_when_no_token(self, monkeypatch) -> None:
|
||||||
monkeypatch.delenv("GITEA_TOKEN", raising=False)
|
monkeypatch.delenv("GITEA_TOKEN", raising=False)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from api.diag.vuln import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
SAMPLE_TRIVY = {
|
SAMPLE_TRIVY = {
|
||||||
"ArtifactName": "gitea.fhirworx.io/homelab/notebooks:sha-abc12345",
|
"ArtifactName": "git.fhirworx.io/homelab/notebooks:sha-abc12345",
|
||||||
"Results": [
|
"Results": [
|
||||||
{
|
{
|
||||||
"Vulnerabilities": [
|
"Vulnerabilities": [
|
||||||
|
|||||||
132
tests/conf/test_storage.py
Normal file
132
tests/conf/test_storage.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
"""Tests for conf.storage — multi-cloud filesystem dispatch."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from conf.storage import get_filesystem
|
||||||
|
|
||||||
|
|
||||||
|
class _Ctx:
|
||||||
|
def __init__(self, backend: str) -> None:
|
||||||
|
self.storage_backend = backend
|
||||||
|
|
||||||
|
|
||||||
|
class TestLocalBackend:
|
||||||
|
def test_local_returns_fsspec_local(self):
|
||||||
|
with patch("conf.context", return_value=_Ctx("local")):
|
||||||
|
fs = get_filesystem()
|
||||||
|
# fsspec("file") returns LocalFileSystem
|
||||||
|
assert hasattr(fs, "ls")
|
||||||
|
|
||||||
|
|
||||||
|
class TestS3Backend:
|
||||||
|
def test_s3_uses_env_credentials(self, monkeypatch):
|
||||||
|
monkeypatch.setenv("RUSTFS_ENDPOINT", "http://rustfs:9000")
|
||||||
|
monkeypatch.setenv("RUSTFS_ACCESS_KEY", "ak")
|
||||||
|
monkeypatch.setenv("RUSTFS_SECRET_KEY", "sk")
|
||||||
|
|
||||||
|
with patch("conf.context", return_value=_Ctx("s3")):
|
||||||
|
fs = get_filesystem()
|
||||||
|
# fsspec returns S3FileSystem; minimal check that it's not None
|
||||||
|
assert fs is not None
|
||||||
|
|
||||||
|
|
||||||
|
class TestGcsBackend:
|
||||||
|
def test_gcs_imports_gcsfs(self, monkeypatch):
|
||||||
|
monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "my-project")
|
||||||
|
fake_gcs_fs = MagicMock()
|
||||||
|
fake_module = types.ModuleType("gcsfs")
|
||||||
|
fake_module.GCSFileSystem = MagicMock(return_value=fake_gcs_fs)
|
||||||
|
monkeypatch.setitem(sys.modules, "gcsfs", fake_module)
|
||||||
|
|
||||||
|
with patch("conf.context", return_value=_Ctx("gcs")):
|
||||||
|
fs = get_filesystem()
|
||||||
|
assert fs is fake_gcs_fs
|
||||||
|
fake_module.GCSFileSystem.assert_called_once_with(project="my-project")
|
||||||
|
|
||||||
|
def test_gcs_missing_package_raises_with_hint(self, monkeypatch):
|
||||||
|
# Force the gcsfs import to fail.
|
||||||
|
real_import = (
|
||||||
|
__builtins__["__import__"]
|
||||||
|
if isinstance(__builtins__, dict)
|
||||||
|
else __builtins__.__import__
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_import(name, *a, **kw):
|
||||||
|
if name == "gcsfs":
|
||||||
|
raise ImportError("not installed")
|
||||||
|
return real_import(name, *a, **kw)
|
||||||
|
|
||||||
|
monkeypatch.setattr("builtins.__import__", fake_import)
|
||||||
|
sys.modules.pop("gcsfs", None)
|
||||||
|
with patch("conf.context", return_value=_Ctx("gcs")):
|
||||||
|
with pytest.raises(ImportError, match="stack\\[gcp\\]"):
|
||||||
|
get_filesystem()
|
||||||
|
|
||||||
|
|
||||||
|
class TestAbfsBackend:
|
||||||
|
def test_abfs_imports_adlfs(self, monkeypatch):
|
||||||
|
monkeypatch.setenv("AZURE_STORAGE_ACCOUNT", "myacct")
|
||||||
|
fake_az_fs = MagicMock()
|
||||||
|
fake_module = types.ModuleType("adlfs")
|
||||||
|
fake_module.AzureBlobFileSystem = MagicMock(return_value=fake_az_fs)
|
||||||
|
monkeypatch.setitem(sys.modules, "adlfs", fake_module)
|
||||||
|
|
||||||
|
with patch("conf.context", return_value=_Ctx("abfs")):
|
||||||
|
fs = get_filesystem()
|
||||||
|
assert fs is fake_az_fs
|
||||||
|
fake_module.AzureBlobFileSystem.assert_called_once_with(account_name="myacct")
|
||||||
|
|
||||||
|
def test_abfs_missing_package_raises_with_hint(self, monkeypatch):
|
||||||
|
real_import = (
|
||||||
|
__builtins__["__import__"]
|
||||||
|
if isinstance(__builtins__, dict)
|
||||||
|
else __builtins__.__import__
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_import(name, *a, **kw):
|
||||||
|
if name == "adlfs":
|
||||||
|
raise ImportError("not installed")
|
||||||
|
return real_import(name, *a, **kw)
|
||||||
|
|
||||||
|
monkeypatch.setattr("builtins.__import__", fake_import)
|
||||||
|
sys.modules.pop("adlfs", None)
|
||||||
|
with patch("conf.context", return_value=_Ctx("abfs")):
|
||||||
|
with pytest.raises(ImportError, match="stack\\[azure\\]"):
|
||||||
|
get_filesystem()
|
||||||
|
|
||||||
|
|
||||||
|
class TestDbfsBackend:
|
||||||
|
def test_dbfs_uses_fsspec(self, monkeypatch):
|
||||||
|
# fsspec doesn't ship a dbfs filesystem by default; stub it.
|
||||||
|
import fsspec
|
||||||
|
|
||||||
|
fake_fs = MagicMock()
|
||||||
|
monkeypatch.setattr(fsspec, "filesystem", lambda name, **kw: fake_fs)
|
||||||
|
with patch("conf.context", return_value=_Ctx("dbfs")):
|
||||||
|
fs = get_filesystem()
|
||||||
|
assert fs is fake_fs
|
||||||
|
|
||||||
|
|
||||||
|
class TestUnknownBackend:
|
||||||
|
def test_raises_value_error(self):
|
||||||
|
with patch("conf.context", return_value=_Ctx("warpdrive")):
|
||||||
|
with pytest.raises(ValueError, match="Unknown storage backend"):
|
||||||
|
get_filesystem()
|
||||||
|
|
||||||
|
|
||||||
|
class TestContextNameOverride:
|
||||||
|
def test_uses_named_context(self, monkeypatch):
|
||||||
|
# Drive context_name path: cfg.context["lake"] returns _Ctx("local").
|
||||||
|
from conf import cfg
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
type(cfg), "context", {"lake": _Ctx("local")}, raising=False
|
||||||
|
)
|
||||||
|
fs = get_filesystem(context_name="lake")
|
||||||
|
assert fs is not None
|
||||||
@@ -190,6 +190,65 @@ class TestFileIssue:
|
|||||||
result = _file_issue("title", "body", labels=["bug"])
|
result = _file_issue("title", "body", labels=["bug"])
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
|
@patch("perf.hooks._get_token", return_value="tok")
|
||||||
|
def test_happy_path_with_labels(self, mock_token):
|
||||||
|
"""Mock the GiteaClient so we exercise the success branch (lines 63-68)."""
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from perf.hooks import _file_issue
|
||||||
|
|
||||||
|
fake_client = MagicMock()
|
||||||
|
fake_client.resolve_labels.return_value = [42]
|
||||||
|
fake_client.create_issue.return_value = {"number": 7, "title": "x"}
|
||||||
|
|
||||||
|
fake_module = types.ModuleType("api.clients.gitea")
|
||||||
|
fake_module.GiteaClient = MagicMock(return_value=fake_client)
|
||||||
|
|
||||||
|
# Insert into sys.modules so the inner `from api.clients.gitea import …`
|
||||||
|
# picks up our stub instead of the real package.
|
||||||
|
original = sys.modules.get("api.clients.gitea")
|
||||||
|
sys.modules["api.clients.gitea"] = fake_module
|
||||||
|
try:
|
||||||
|
result = _file_issue("title", "body", labels=["bug"])
|
||||||
|
assert result == {"number": 7, "title": "x"}
|
||||||
|
fake_client.resolve_labels.assert_called_once()
|
||||||
|
fake_client.create_issue.assert_called_once()
|
||||||
|
fake_client.close.assert_called_once()
|
||||||
|
finally:
|
||||||
|
if original is not None:
|
||||||
|
sys.modules["api.clients.gitea"] = original
|
||||||
|
else:
|
||||||
|
sys.modules.pop("api.clients.gitea", None)
|
||||||
|
|
||||||
|
@patch("perf.hooks._get_token", return_value="tok")
|
||||||
|
def test_happy_path_without_label_ids(self, mock_token):
|
||||||
|
"""resolve_labels returns empty → labels key not added."""
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from perf.hooks import _file_issue
|
||||||
|
|
||||||
|
fake_client = MagicMock()
|
||||||
|
fake_client.resolve_labels.return_value = []
|
||||||
|
fake_client.create_issue.return_value = {"number": 8}
|
||||||
|
|
||||||
|
fake_module = types.ModuleType("api.clients.gitea")
|
||||||
|
fake_module.GiteaClient = MagicMock(return_value=fake_client)
|
||||||
|
|
||||||
|
original = sys.modules.get("api.clients.gitea")
|
||||||
|
sys.modules["api.clients.gitea"] = fake_module
|
||||||
|
try:
|
||||||
|
result = _file_issue("title", "body", labels=["bug"])
|
||||||
|
assert result == {"number": 8}
|
||||||
|
finally:
|
||||||
|
if original is not None:
|
||||||
|
sys.modules["api.clients.gitea"] = original
|
||||||
|
else:
|
||||||
|
sys.modules.pop("api.clients.gitea", None)
|
||||||
|
|
||||||
|
|
||||||
class TestGitShaShort:
|
class TestGitShaShort:
|
||||||
"""_git_sha_short handles subprocess failures."""
|
"""_git_sha_short handles subprocess failures."""
|
||||||
|
|||||||
@@ -126,6 +126,72 @@ class TestPublicAPI:
|
|||||||
# Should not raise — just a no-op
|
# Should not raise — just a no-op
|
||||||
perf.shutdown()
|
perf.shutdown()
|
||||||
|
|
||||||
|
def test_init_idempotent(self, monkeypatch):
|
||||||
|
monkeypatch.setenv("STACK_TELEMETRY", "false")
|
||||||
|
import perf
|
||||||
|
|
||||||
|
perf._initialised = True
|
||||||
|
perf.init() # early-return path
|
||||||
|
assert perf._initialised is True
|
||||||
|
perf._initialised = False
|
||||||
|
|
||||||
|
def test_init_and_shutdown_when_enabled(self, monkeypatch):
|
||||||
|
monkeypatch.setenv("STACK_TELEMETRY", "true")
|
||||||
|
from opentelemetry import trace
|
||||||
|
from opentelemetry.metrics import _internal as metrics_internal
|
||||||
|
|
||||||
|
import perf
|
||||||
|
import perf._meter as meter_mod
|
||||||
|
import perf._tracer as tracer_mod
|
||||||
|
|
||||||
|
# Snapshot the global once-flags so we can restore them and let
|
||||||
|
# other tests install their own providers afterward.
|
||||||
|
trace_done = trace._TRACER_PROVIDER_SET_ONCE._done
|
||||||
|
metrics_done = metrics_internal._METER_PROVIDER_SET_ONCE._done
|
||||||
|
original_trace_provider = trace._TRACER_PROVIDER
|
||||||
|
original_metric_provider = metrics_internal._METER_PROVIDER
|
||||||
|
|
||||||
|
perf._initialised = False
|
||||||
|
meter_mod._provider_ready = False
|
||||||
|
tracer_mod._provider_ready = False
|
||||||
|
try:
|
||||||
|
perf.init()
|
||||||
|
assert perf._initialised is True
|
||||||
|
assert tracer_mod._provider_ready is True
|
||||||
|
assert meter_mod._provider_ready is True
|
||||||
|
perf.shutdown()
|
||||||
|
assert perf._initialised is False
|
||||||
|
finally:
|
||||||
|
perf._initialised = False
|
||||||
|
meter_mod._provider_ready = False
|
||||||
|
tracer_mod._provider_ready = False
|
||||||
|
# Restore globals so subsequent tests get a clean slate.
|
||||||
|
trace._TRACER_PROVIDER = original_trace_provider
|
||||||
|
metrics_internal._METER_PROVIDER = original_metric_provider
|
||||||
|
trace._TRACER_PROVIDER_SET_ONCE._done = trace_done
|
||||||
|
metrics_internal._METER_PROVIDER_SET_ONCE._done = metrics_done
|
||||||
|
|
||||||
|
def test_enabled_falls_through_when_conf_unavailable(self, monkeypatch):
|
||||||
|
monkeypatch.delenv("STACK_TELEMETRY", raising=False)
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from perf import _enabled
|
||||||
|
|
||||||
|
real_import = (
|
||||||
|
__builtins__["__import__"]
|
||||||
|
if isinstance(__builtins__, dict)
|
||||||
|
else __builtins__.__import__
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_import(name, *args, **kwargs):
|
||||||
|
if name == "conf":
|
||||||
|
raise ImportError("conf unavailable")
|
||||||
|
return real_import(name, *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr("builtins.__import__", fake_import)
|
||||||
|
monkeypatch.delitem(sys.modules, "conf", raising=False)
|
||||||
|
assert _enabled() is False
|
||||||
|
|
||||||
|
|
||||||
class TestFileSpanExporter:
|
class TestFileSpanExporter:
|
||||||
"""FileSpanExporter writes JSONL to disk."""
|
"""FileSpanExporter writes JSONL to disk."""
|
||||||
|
|||||||
@@ -101,3 +101,70 @@ class TestCreateMeter:
|
|||||||
from perf._meter import MockMeter
|
from perf._meter import MockMeter
|
||||||
|
|
||||||
assert isinstance(m, MockMeter)
|
assert isinstance(m, MockMeter)
|
||||||
|
|
||||||
|
def test_mock_instrument_methods_are_noops(self):
|
||||||
|
from perf._meter import MockMeter, _MockInstrument
|
||||||
|
|
||||||
|
instr = _MockInstrument()
|
||||||
|
instr.add(1.0, {"k": "v"})
|
||||||
|
instr.record(2.5)
|
||||||
|
instr.set(3.0, {"k": "v"})
|
||||||
|
|
||||||
|
meter = MockMeter()
|
||||||
|
assert isinstance(meter.create_counter("c"), _MockInstrument)
|
||||||
|
assert isinstance(meter.create_histogram("h"), _MockInstrument)
|
||||||
|
assert isinstance(meter.create_up_down_counter("g"), _MockInstrument)
|
||||||
|
assert isinstance(meter.create_observable_gauge("og"), _MockInstrument)
|
||||||
|
|
||||||
|
def test_returns_mock_when_enabled_but_provider_missing(self, monkeypatch):
|
||||||
|
# Provider not yet set up — falls into the second MockMeter return.
|
||||||
|
monkeypatch.setenv("STACK_TELEMETRY", "true")
|
||||||
|
import perf._meter as mod
|
||||||
|
from perf._meter import MockMeter
|
||||||
|
|
||||||
|
mod._provider_ready = False
|
||||||
|
assert isinstance(mod.create_meter("test"), MockMeter)
|
||||||
|
|
||||||
|
def test_returns_real_when_provider_ready(self, monkeypatch):
|
||||||
|
import perf._meter as mod
|
||||||
|
from perf._meter import MockMeter
|
||||||
|
|
||||||
|
mod._provider_ready = True
|
||||||
|
try:
|
||||||
|
m = mod.create_meter("test.scope")
|
||||||
|
assert not isinstance(m, MockMeter)
|
||||||
|
finally:
|
||||||
|
mod._provider_ready = False
|
||||||
|
|
||||||
|
|
||||||
|
class TestSetupMeterProvider:
|
||||||
|
"""setup_meter_provider() configures the global MeterProvider."""
|
||||||
|
|
||||||
|
def test_sets_provider_ready(self):
|
||||||
|
import perf._meter as mod
|
||||||
|
|
||||||
|
original_ready = mod._provider_ready
|
||||||
|
try:
|
||||||
|
mod._provider_ready = False
|
||||||
|
mod.setup_meter_provider()
|
||||||
|
assert mod._provider_ready is True
|
||||||
|
finally:
|
||||||
|
mod._provider_ready = original_ready
|
||||||
|
|
||||||
|
|
||||||
|
class TestShutdownMeterProvider:
|
||||||
|
"""shutdown_meter_provider() flushes and resets state."""
|
||||||
|
|
||||||
|
def test_noop_when_not_ready(self):
|
||||||
|
import perf._meter as mod
|
||||||
|
|
||||||
|
mod._provider_ready = False
|
||||||
|
mod.shutdown_meter_provider() # should return without error
|
||||||
|
assert mod._provider_ready is False
|
||||||
|
|
||||||
|
def test_resets_provider_ready(self, monkeypatch):
|
||||||
|
import perf._meter as mod
|
||||||
|
|
||||||
|
mod._provider_ready = True
|
||||||
|
mod.shutdown_meter_provider()
|
||||||
|
assert mod._provider_ready is False
|
||||||
|
|||||||
164
tests/perf/test_resource.py
Normal file
164
tests/perf/test_resource.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
"""Tests for perf._resource — OTel Resource builder and metadata helpers."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuildResource:
|
||||||
|
"""build_resource() returns an OTel Resource with stack attributes."""
|
||||||
|
|
||||||
|
def test_returns_resource_with_service_name(self):
|
||||||
|
from perf._resource import build_resource
|
||||||
|
|
||||||
|
resource = build_resource()
|
||||||
|
attrs = resource.attributes
|
||||||
|
assert "service.name" in attrs
|
||||||
|
assert attrs["service.name"] # non-empty
|
||||||
|
|
||||||
|
def test_includes_service_version(self):
|
||||||
|
from perf._resource import build_resource
|
||||||
|
|
||||||
|
resource = build_resource()
|
||||||
|
assert "service.version" in resource.attributes
|
||||||
|
|
||||||
|
def test_optional_vcs_revision(self):
|
||||||
|
from perf._resource import build_resource
|
||||||
|
|
||||||
|
resource = build_resource()
|
||||||
|
# vcs.revision is set when git rev-parse succeeds; not strict
|
||||||
|
if "vcs.revision" in resource.attributes:
|
||||||
|
assert isinstance(resource.attributes["vcs.revision"], str)
|
||||||
|
|
||||||
|
def test_optional_deployment_environment(self):
|
||||||
|
from perf._resource import build_resource
|
||||||
|
|
||||||
|
resource = build_resource()
|
||||||
|
# deployment.environment is set when conf.context() succeeds
|
||||||
|
if "deployment.environment" in resource.attributes:
|
||||||
|
assert isinstance(resource.attributes["deployment.environment"], str)
|
||||||
|
|
||||||
|
|
||||||
|
class TestServiceName:
|
||||||
|
"""_service_name() resolves from env, then conf, then default."""
|
||||||
|
|
||||||
|
def test_env_var_takes_precedence(self, monkeypatch):
|
||||||
|
monkeypatch.setenv("OTEL_SERVICE_NAME", "my-service")
|
||||||
|
from perf._resource import _service_name
|
||||||
|
|
||||||
|
assert _service_name() == "my-service"
|
||||||
|
|
||||||
|
def test_falls_back_to_conf(self, monkeypatch):
|
||||||
|
monkeypatch.delenv("OTEL_SERVICE_NAME", raising=False)
|
||||||
|
from perf._resource import _service_name
|
||||||
|
|
||||||
|
# conf.cfg.telemetry.service_name should resolve from stack.toml
|
||||||
|
name = _service_name()
|
||||||
|
assert isinstance(name, str)
|
||||||
|
assert name # non-empty
|
||||||
|
|
||||||
|
def test_default_when_conf_unavailable(self, monkeypatch):
|
||||||
|
monkeypatch.delenv("OTEL_SERVICE_NAME", raising=False)
|
||||||
|
# Force the conf import path to raise
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import perf._resource as mod
|
||||||
|
|
||||||
|
real_import = (
|
||||||
|
__builtins__["__import__"]
|
||||||
|
if isinstance(__builtins__, dict)
|
||||||
|
else __builtins__.__import__
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_import(name, *args, **kwargs):
|
||||||
|
if name == "conf":
|
||||||
|
raise ImportError("conf unavailable")
|
||||||
|
return real_import(name, *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr("builtins.__import__", fake_import)
|
||||||
|
# Drop any cached conf module so the import is re-attempted
|
||||||
|
monkeypatch.delitem(sys.modules, "conf", raising=False)
|
||||||
|
assert mod._service_name() == "stack"
|
||||||
|
|
||||||
|
|
||||||
|
class TestVersion:
|
||||||
|
"""_version() reads installed package version, defaults on failure."""
|
||||||
|
|
||||||
|
def test_returns_string(self):
|
||||||
|
from perf._resource import _version
|
||||||
|
|
||||||
|
v = _version()
|
||||||
|
assert isinstance(v, str)
|
||||||
|
assert v # non-empty
|
||||||
|
|
||||||
|
def test_default_on_metadata_error(self, monkeypatch):
|
||||||
|
import perf._resource as mod
|
||||||
|
|
||||||
|
def boom(_pkg):
|
||||||
|
raise Exception("no metadata")
|
||||||
|
|
||||||
|
monkeypatch.setattr("importlib.metadata.version", boom)
|
||||||
|
assert mod._version() == "0.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
class TestGitSha:
|
||||||
|
"""_git_sha() returns short SHA from git rev-parse, None on failure."""
|
||||||
|
|
||||||
|
def test_success_returns_sha(self, monkeypatch):
|
||||||
|
from perf._resource import _git_sha
|
||||||
|
|
||||||
|
class FakeResult:
|
||||||
|
returncode = 0
|
||||||
|
stdout = "abcd1234\n"
|
||||||
|
|
||||||
|
monkeypatch.setattr(subprocess, "run", lambda *a, **kw: FakeResult())
|
||||||
|
assert _git_sha() == "abcd1234"
|
||||||
|
|
||||||
|
def test_failure_returns_none(self, monkeypatch):
|
||||||
|
from perf._resource import _git_sha
|
||||||
|
|
||||||
|
def raises(*a, **kw):
|
||||||
|
raise FileNotFoundError("git not found")
|
||||||
|
|
||||||
|
monkeypatch.setattr(subprocess, "run", raises)
|
||||||
|
assert _git_sha() is None
|
||||||
|
|
||||||
|
def test_nonzero_returncode_returns_none(self, monkeypatch):
|
||||||
|
from perf._resource import _git_sha
|
||||||
|
|
||||||
|
class FakeResult:
|
||||||
|
returncode = 1
|
||||||
|
stdout = ""
|
||||||
|
|
||||||
|
monkeypatch.setattr(subprocess, "run", lambda *a, **kw: FakeResult())
|
||||||
|
assert _git_sha() is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestContextName:
|
||||||
|
"""_context_name() returns conf.context().db_backend or None."""
|
||||||
|
|
||||||
|
def test_returns_string_or_none(self):
|
||||||
|
from perf._resource import _context_name
|
||||||
|
|
||||||
|
result = _context_name()
|
||||||
|
assert result is None or isinstance(result, str)
|
||||||
|
|
||||||
|
def test_returns_none_on_exception(self, monkeypatch):
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import perf._resource as mod
|
||||||
|
|
||||||
|
real_import = (
|
||||||
|
__builtins__["__import__"]
|
||||||
|
if isinstance(__builtins__, dict)
|
||||||
|
else __builtins__.__import__
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_import(name, *args, **kwargs):
|
||||||
|
if name == "conf":
|
||||||
|
raise ImportError("conf unavailable")
|
||||||
|
return real_import(name, *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr("builtins.__import__", fake_import)
|
||||||
|
monkeypatch.delitem(sys.modules, "conf", raising=False)
|
||||||
|
assert mod._context_name() is None
|
||||||
@@ -143,6 +143,16 @@ class TestCreateTracer:
|
|||||||
assert not isinstance(t, MockTracer)
|
assert not isinstance(t, MockTracer)
|
||||||
mod._provider_ready = False
|
mod._provider_ready = False
|
||||||
|
|
||||||
|
def test_returns_mock_when_enabled_but_provider_missing(self, monkeypatch):
|
||||||
|
# Provider not yet ready, but telemetry IS enabled — covers the
|
||||||
|
# second MockTracer return inside create_tracer().
|
||||||
|
monkeypatch.setenv("STACK_TELEMETRY", "true")
|
||||||
|
import perf._tracer as mod
|
||||||
|
from perf._tracer import MockTracer
|
||||||
|
|
||||||
|
mod._provider_ready = False
|
||||||
|
assert isinstance(mod.create_tracer("test"), MockTracer)
|
||||||
|
|
||||||
|
|
||||||
class TestEndpointResolution:
|
class TestEndpointResolution:
|
||||||
"""Verify OTLP endpoint reads from env and config."""
|
"""Verify OTLP endpoint reads from env and config."""
|
||||||
@@ -159,3 +169,110 @@ class TestEndpointResolution:
|
|||||||
|
|
||||||
endpoint = _get_endpoint()
|
endpoint = _get_endpoint()
|
||||||
assert "4317" in endpoint
|
assert "4317" in endpoint
|
||||||
|
|
||||||
|
def test_default_when_conf_unavailable(self, monkeypatch):
|
||||||
|
monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False)
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from perf._tracer import _get_endpoint
|
||||||
|
|
||||||
|
real_import = (
|
||||||
|
__builtins__["__import__"]
|
||||||
|
if isinstance(__builtins__, dict)
|
||||||
|
else __builtins__.__import__
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_import(name, *args, **kwargs):
|
||||||
|
if name == "conf":
|
||||||
|
raise ImportError("conf unavailable")
|
||||||
|
return real_import(name, *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr("builtins.__import__", fake_import)
|
||||||
|
monkeypatch.delitem(sys.modules, "conf", raising=False)
|
||||||
|
assert _get_endpoint() == "http://otel-collector:4317"
|
||||||
|
|
||||||
|
|
||||||
|
class TestSetupTracerProvider:
|
||||||
|
"""setup_tracer_provider() configures the global TracerProvider."""
|
||||||
|
|
||||||
|
def test_sets_provider_ready(self):
|
||||||
|
import perf._tracer as mod
|
||||||
|
|
||||||
|
original_ready = mod._provider_ready
|
||||||
|
try:
|
||||||
|
mod._provider_ready = False
|
||||||
|
mod.setup_tracer_provider()
|
||||||
|
assert mod._provider_ready is True
|
||||||
|
finally:
|
||||||
|
mod._provider_ready = original_ready
|
||||||
|
|
||||||
|
def test_falls_back_to_file_exporter_when_otlp_fails(self, monkeypatch):
|
||||||
|
# Force OTLPSpanExporter import to raise so the fallback path runs.
|
||||||
|
import perf._tracer as mod
|
||||||
|
|
||||||
|
real_import = (
|
||||||
|
__builtins__["__import__"]
|
||||||
|
if isinstance(__builtins__, dict)
|
||||||
|
else __builtins__.__import__
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_import(name, *args, **kwargs):
|
||||||
|
if "otlp" in name:
|
||||||
|
raise ImportError("otlp grpc not available")
|
||||||
|
return real_import(name, *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr("builtins.__import__", fake_import)
|
||||||
|
original_ready = mod._provider_ready
|
||||||
|
try:
|
||||||
|
mod._provider_ready = False
|
||||||
|
mod.setup_tracer_provider()
|
||||||
|
assert mod._provider_ready is True
|
||||||
|
finally:
|
||||||
|
mod._provider_ready = original_ready
|
||||||
|
|
||||||
|
|
||||||
|
class TestShutdownTracerProvider:
|
||||||
|
"""shutdown_tracer_provider() flushes and resets state."""
|
||||||
|
|
||||||
|
def test_noop_when_not_ready(self):
|
||||||
|
import perf._tracer as mod
|
||||||
|
|
||||||
|
mod._provider_ready = False
|
||||||
|
mod.shutdown_tracer_provider()
|
||||||
|
assert mod._provider_ready is False
|
||||||
|
|
||||||
|
def test_resets_provider_ready(self):
|
||||||
|
import perf._tracer as mod
|
||||||
|
|
||||||
|
mod._provider_ready = True
|
||||||
|
mod.shutdown_tracer_provider()
|
||||||
|
assert mod._provider_ready is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestMockObjects:
|
||||||
|
"""MockSpan and MockTracer methods are zero-cost no-ops."""
|
||||||
|
|
||||||
|
def test_mock_span_methods(self):
|
||||||
|
from perf._tracer import MockSpan
|
||||||
|
|
||||||
|
span = MockSpan()
|
||||||
|
span.set_attribute("k", "v")
|
||||||
|
span.set_status("OK", description="ok")
|
||||||
|
span.record_exception(RuntimeError("boom"))
|
||||||
|
span.add_event("ev", {"a": 1})
|
||||||
|
|
||||||
|
def test_mock_tracer_start_span(self):
|
||||||
|
from perf._tracer import MockSpan, MockTracer
|
||||||
|
|
||||||
|
tracer = MockTracer()
|
||||||
|
span = tracer.start_span("test")
|
||||||
|
assert isinstance(span, MockSpan)
|
||||||
|
with tracer.start_as_current_span("ctx") as s:
|
||||||
|
assert isinstance(s, MockSpan)
|
||||||
|
|
||||||
|
def test_mock_span_as_context_manager(self):
|
||||||
|
from perf._tracer import MockSpan
|
||||||
|
|
||||||
|
span = MockSpan()
|
||||||
|
with span as s:
|
||||||
|
assert s is span
|
||||||
|
|||||||
@@ -111,6 +111,19 @@ class TestFunctionCoverageRatio:
|
|||||||
n2 = _node(kind=NodeKind.BRANCH_ELSE, hit=True, module="m", symbol_ctx=["fn"])
|
n2 = _node(kind=NodeKind.BRANCH_ELSE, hit=True, module="m", symbol_ctx=["fn"])
|
||||||
assert _function_coverage_ratio(n1, [n1, n2]) == 1.0
|
assert _function_coverage_ratio(n1, [n1, n2]) == 1.0
|
||||||
|
|
||||||
|
def test_node_not_in_all_nodes_returns_zero(self):
|
||||||
|
"""When the node has a function context but no siblings match, return 0."""
|
||||||
|
from sem.plan import _function_coverage_ratio
|
||||||
|
|
||||||
|
# Node has symbol_ctx ["lonely"] but all_nodes only has nodes from "other"
|
||||||
|
node = _node(
|
||||||
|
kind=NodeKind.BRANCH_IF, hit=False, module="m", symbol_ctx=["lonely"]
|
||||||
|
)
|
||||||
|
unrelated = _node(
|
||||||
|
kind=NodeKind.BRANCH_IF, hit=True, module="m", symbol_ctx=["other"]
|
||||||
|
)
|
||||||
|
assert _function_coverage_ratio(node, [unrelated]) == 0.0
|
||||||
|
|
||||||
def test_rank_nodes_deduplicates(self):
|
def test_rank_nodes_deduplicates(self):
|
||||||
"""rank_nodes scores and sorts without error."""
|
"""rank_nodes scores and sorts without error."""
|
||||||
nodes = [
|
nodes = [
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ class TestBindMountSourcesExist:
|
|||||||
|
|
||||||
missing = []
|
missing = []
|
||||||
for svc, host_path, full in mounts:
|
for svc, host_path, full in mounts:
|
||||||
|
# ./data/ is gitignored runtime state — populated by service
|
||||||
|
# bootstrap on first run, not present in a fresh checkout (CI).
|
||||||
|
if host_path.startswith("./data/"):
|
||||||
|
continue
|
||||||
resolved = ROOT / host_path
|
resolved = ROOT / host_path
|
||||||
if not resolved.exists():
|
if not resolved.exists():
|
||||||
missing.append(f" {svc}: {host_path}")
|
missing.append(f" {svc}: {host_path}")
|
||||||
|
|||||||
Reference in New Issue
Block a user