diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aae8224..342a482 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,6 +39,9 @@ GitHub Actions runs `make ci` plus Compose/Helm validation on every push and PR to `main`. Run `make ci-all`, `make helm-lint`, and `make pulumi-tests` locally before larger API or client-facing changes. +Suite layout, Make targets, and the latest recorded local results: +[docs/testing.md](docs/testing.md). + ## Project rules worth remembering 1. Mutations must **persist to PostgreSQL** (tables and/or jsonb metadata). diff --git a/CONTRIBUTING.ru.md b/CONTRIBUTING.ru.md index c8713e1..122a388 100644 --- a/CONTRIBUTING.ru.md +++ b/CONTRIBUTING.ru.md @@ -36,6 +36,9 @@ GitHub Actions на каждый push/PR в `main` запускает `make ci` Compose/Helm. Перед крупными API/клиентскими изменениями локально прогоняйте `make ci-all`, `make helm-lint` и `make pulumi-tests`. +Карта наборов, Make-цели и последние записанные локальные результаты: +[docs/ru/testing.md](docs/ru/testing.md). + ## Важные правила проекта 1. Мутации должны **persist в PostgreSQL** (таблицы и/или jsonb metadata). diff --git a/README.md b/README.md index 4beb1df..8a67775 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,7 @@ make release-up && make release-seed # run the published stack locally ## Contributing / security / changelog - [CONTRIBUTING.md](CONTRIBUTING.md) · [CONTRIBUTING.ru.md](CONTRIBUTING.ru.md) +- [Testing](docs/testing.md) · [Тесты](docs/ru/testing.md) — suites, Make targets, latest results - [SECURITY.md](SECURITY.md) - [CHANGELOG.md](CHANGELOG.md) diff --git a/README.ru.md b/README.ru.md index 1d7dcd8..2bc4a06 100644 --- a/README.ru.md +++ b/README.ru.md @@ -194,6 +194,7 @@ make release-up && make release-seed # запустить опубликова ## Участие / безопасность / changelog - [CONTRIBUTING.md](CONTRIBUTING.md) · [CONTRIBUTING.ru.md](CONTRIBUTING.ru.md) +- [Testing](docs/testing.md) · [Тесты](docs/ru/testing.md) — наборы, Make-цели, последние результаты - [SECURITY.md](SECURITY.md) - [CHANGELOG.md](CHANGELOG.md) diff --git a/app/contracts/examples.py b/app/contracts/examples.py index 34cc96e..f9f23a0 100644 --- a/app/contracts/examples.py +++ b/app/contracts/examples.py @@ -205,7 +205,7 @@ def _format_key_example(key: str, raw: Mapping[str, Any]) -> object: if "default" in raw and raw["default"] is not None: return raw["default"] enum = raw.get("enum") - if isinstance(enum, (list, tuple)) and enum: + if isinstance(enum, list | tuple) and enum: return enum[0] nested_format = raw.get("format") if isinstance(nested_format, Mapping): @@ -226,10 +226,10 @@ def _format_key_example(key: str, raw: Mapping[str, Any]) -> object: return 1 if typ == "integer": minimum = raw.get("minimum") - return int(minimum) if isinstance(minimum, (int, float)) else 1 + return int(minimum) if isinstance(minimum, int | float) else 1 if typ == "number": minimum = raw.get("minimum") - return float(minimum) if isinstance(minimum, (int, float)) else 1.0 + return float(minimum) if isinstance(minimum, int | float) else 1.0 pattern = raw.get("pattern") if isinstance(pattern, str) and "second|minute|hour|day" in pattern: return "1/second" diff --git a/app/handlers/acme.py b/app/handlers/acme.py index e7d5332..449793d 100644 --- a/app/handlers/acme.py +++ b/app/handlers/acme.py @@ -131,7 +131,7 @@ def register_acme_handlers(registry: HandlerRegistry) -> None: entry = {"plugin": plugin_id, **{k: v for k, v in item.items() if k != "data"}} if "digest" not in entry: material = {k: v for k, v in entry.items() if k != "digest"} - entry["digest"] = hashlib.sha1( + entry["digest"] = hashlib.sha1( # noqa: S324 - Proxmox config digests use SHA-1 json.dumps(material, sort_keys=True, separators=(",", ":")).encode() ).hexdigest() result.append(entry) diff --git a/app/handlers/ceph.py b/app/handlers/ceph.py index d4c5116..5e910a8 100644 --- a/app/handlers/ceph.py +++ b/app/handlers/ceph.py @@ -245,7 +245,7 @@ def register_ceph_handlers(registry: HandlerRegistry) -> None: def _wire_ceph_pool(name: str, item: dict[str, Any], *, index: int) -> dict[str, Any]: pool_id = item.get("pool_id", item.get("pool")) try: - pool_num = int(pool_id) + pool_num = int(pool_id) if pool_id is not None else index except (TypeError, ValueError): pool_num = index crush_raw = item.get("crush_rule", 0) @@ -586,9 +586,8 @@ def register_ceph_handlers(registry: HandlerRegistry) -> None: MAX(NULLIF(regexp_replace(external_id, '\\D', '', 'g'), '')::int), -1 ) + 1 - FROM resources r JOIN nodes n ON n.id=r.node_id - WHERE n.name=$1 AND r.kind='ceph-osd'""", - node, + FROM resources + WHERE kind='ceph-osd'""", ) osd_id = int(next_id or 0) external_id = f"osd.{osd_id}" @@ -607,8 +606,15 @@ def register_ceph_handlers(registry: HandlerRegistry) -> None: "used_bytes": 0, } await database(request).pool.execute( - """INSERT INTO resources(id, node_id, kind, external_id, state) - VALUES(gen_random_uuid(), $1, 'ceph-osd', $2, $3::jsonb)""", + """INSERT INTO resources(id, node_id, cluster_id, kind, external_id, state) + VALUES( + gen_random_uuid(), + $1, + (SELECT cluster_id FROM nodes WHERE id=$1), + 'ceph-osd', + $2, + $3::jsonb + )""", node_id, external_id, json.dumps(osd_state, sort_keys=True), diff --git a/app/handlers/cluster_extra.py b/app/handlers/cluster_extra.py index 53c1f0c..5bf40a4 100644 --- a/app/handlers/cluster_extra.py +++ b/app/handlers/cluster_extra.py @@ -193,8 +193,8 @@ def register_cluster_extra_handlers(registry: HandlerRegistry) -> None: now = int(time.time()) seeded = metrics.get("export_series") if isinstance(seeded, list) and seeded: - series = [dict(item) for item in seeded if isinstance(item, dict)] - return {"data": series, "timestamp": now} + seeded_series = [dict(item) for item in seeded if isinstance(item, dict)] + return {"data": seeded_series, "timestamp": now} # Build a PVE-shaped metric array from live inventory (scales with seed). rows = await database(request).pool.fetch("SELECT name FROM nodes ORDER BY name") series: list[dict[str, Any]] = [] diff --git a/app/handlers/core.py b/app/handlers/core.py index f61e849..520b93c 100644 --- a/app/handlers/core.py +++ b/app/handlers/core.py @@ -168,6 +168,7 @@ def build_core_handlers(settings: Settings) -> HandlerRegistry: "machine": "x86_64", } return { + "status": str(row["status"]), "uptime": int(payload.get("uptime") or 0), "wait": float(payload.get("wait") or 0.0), "idle": float(payload.get("idle") or 0.95), @@ -344,7 +345,7 @@ def build_core_handlers(settings: Settings) -> HandlerRegistry: "vmid": vmid, "name": str(state.get("name") or f"{kind}-{external_id}"), "status": status, - "template": bool(state.get("template") in {True, "1", 1}), + "template": bool(state.get("template") in (True, "1", 1)), "cpu": _cpu_util(state, running=running), "maxcpu": _maxcpu(state), "mem": int(_num(state.get("mem"), 0)) if running else 0, diff --git a/app/handlers/ha.py b/app/handlers/ha.py index 7108196..a485bcd 100644 --- a/app/handlers/ha.py +++ b/app/handlers/ha.py @@ -30,7 +30,7 @@ def _ha_groups(metadata: dict[str, Any]) -> dict[str, dict[str, Any]]: def _ha_digest(payload: dict[str, Any]) -> str: material = {key: value for key, value in payload.items() if key != "digest"} - return hashlib.sha1( + return hashlib.sha1( # noqa: S324 - Proxmox HA digests use SHA-1 json.dumps(material, sort_keys=True, separators=(",", ":")).encode() ).hexdigest() diff --git a/app/handlers/lxc.py b/app/handlers/lxc.py index 9d17bb8..1119f07 100644 --- a/app/handlers/lxc.py +++ b/app/handlers/lxc.py @@ -70,7 +70,7 @@ def _public_lxc_config( continue payload[key] = value digest_src = {key: value for key, value in payload.items() if key != "digest"} - payload["digest"] = hashlib.sha1( + payload["digest"] = hashlib.sha1( # noqa: S324 - Proxmox config digests use SHA-1 json.dumps(digest_src, sort_keys=True, separators=(",", ":")).encode() ).hexdigest() return payload diff --git a/app/handlers/nodes_extra.py b/app/handlers/nodes_extra.py index 24e104b..fb423eb 100644 --- a/app/handlers/nodes_extra.py +++ b/app/handlers/nodes_extra.py @@ -554,10 +554,10 @@ def register_nodes_extra_handlers(registry: HandlerRegistry) -> None: } ) else: - path = payload.get("path") + target_path = payload.get("path") index = payload.get("index") for file_idx, file_entry in enumerate(files): - if path is not None and file_entry.get("path") != path: + if target_path is not None and file_entry.get("path") != target_path: continue repos = [ dict(item) @@ -584,7 +584,7 @@ def register_nodes_extra_handlers(registry: HandlerRegistry) -> None: } }, } - elif path is not None or file_idx == 0: + elif target_path is not None or file_idx == 0: if repos: repos[0] = { **repos[0], @@ -604,7 +604,7 @@ def register_nodes_extra_handlers(registry: HandlerRegistry) -> None: }, } file_entry["repositories"] = repos - if path is not None: + if target_path is not None: break apt["repositories"] = { "digest": secrets.token_hex(4), diff --git a/app/handlers/qemu.py b/app/handlers/qemu.py index b26e0d8..558866a 100644 --- a/app/handlers/qemu.py +++ b/app/handlers/qemu.py @@ -78,12 +78,26 @@ def _public_qemu_config(config: dict[str, Any], state: dict[str, Any]) -> dict[s agent = config.get("agent", state.get("agent", "0")) payload["agent"] = _agent_config_string(agent) digest_src = {key: value for key, value in payload.items() if key != "digest"} - payload["digest"] = hashlib.sha1( + payload["digest"] = hashlib.sha1( # noqa: S324 - Proxmox config digests use SHA-1 json.dumps(digest_src, sort_keys=True, separators=(",", ":")).encode() ).hexdigest() return payload +def _cpu_utilization(vm_state: Mapping[str, Any], *, running: bool, idle: float = 0.1) -> float: + """Status ``cpu`` is utilization; config may store model strings like ``qemu64``.""" + + raw = vm_state.get("cpu") + if isinstance(raw, (int, float)): + return float(raw) + if isinstance(raw, str): + try: + return float(raw) + except ValueError: + pass + return idle if running else 0.0 + + def _qemu_index_item(vmid: int, vm_state: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: status = str(vm_state.get("status", "stopped")) running = status in {"running", "paused"} @@ -96,7 +110,7 @@ def _qemu_index_item(vmid: int, vm_state: dict[str, Any], config: dict[str, Any] "status": status, "qmpstatus": status if running else "stopped", "cpus": int(config.get("cores", config.get("cpus", 1))), - "cpu": float(vm_state.get("cpu", 0.1 if running else 0.0)), + "cpu": _cpu_utilization(vm_state, running=running), "maxmem": maxmem, "mem": mem, "memhost": int(vm_state.get("memhost", mem)), diff --git a/app/simulation/seed.py b/app/simulation/seed.py index 092801b..4ff663c 100644 --- a/app/simulation/seed.py +++ b/app/simulation/seed.py @@ -1682,7 +1682,7 @@ def _seed_tasks_for_guests( return tuple(tasks) -# UI / ops cluster sizes (hosts × total guests). Infra scales with the size. +# UI / ops cluster sizes (hosts x total guests). Infra scales with the size. _CLUSTER_SIZE_SPECS: dict[str, dict[str, int]] = { "lab": { "nodes": 1, @@ -1953,13 +1953,13 @@ def _sized_cluster_profile(name: str, *, overrides: dict[str, int] | None = None def small_profile() -> SeedProfile: - """DATA UI: 3 hosts × 50 guests.""" + """DATA UI: 3 hosts x 50 guests.""" return _sized_cluster_profile("small") def large_profile(*, node_count: int = 10, resource_count: int = 1_000) -> SeedProfile: - """DATA UI: 10 hosts × 1000 guests (overrides allowed for CLI stress).""" + """DATA UI: 10 hosts x 1000 guests (overrides allowed for CLI stress).""" if node_count < 1 or resource_count < 1: raise ValueError("large profile counts must be positive") @@ -1971,22 +1971,22 @@ def large_profile(*, node_count: int = 10, resource_count: int = 1_000) -> SeedP overrides={ "nodes": node_count, "guests": resource_count, - "osds": max(10, int(round(100 * scale))), - "ha": max(3, int(round(12 * scale))), - "tasks": max(12, int(round(50 * scale))), - "pool_members": max(12, min(resource_count, int(round(50 * scale)))), + "osds": max(10, round(100 * scale)), + "ha": max(3, round(12 * scale)), + "tasks": max(12, round(50 * scale)), + "pool_members": max(12, min(resource_count, round(50 * scale))), }, ) def big_profile() -> SeedProfile: - """DATA UI: 20 hosts × 2000 guests.""" + """DATA UI: 20 hosts x 2000 guests.""" return _sized_cluster_profile("big") def medium_profile() -> SeedProfile: - """Compatibility alias for the small (3×50) cluster.""" + """Compatibility alias for the small (3x50) cluster.""" profile = small_profile() return SeedProfile("medium", profile.nodes, profile.resources, profile.tasks) diff --git a/docs/README.md b/docs/README.md index 2a7ae89..3ad3776 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,9 +27,11 @@ page. Russian mirrors live under [`ru/`](ru/README.md). | [FAQ](faq.md) | Short answers | | [Architecture](architecture.md) | Component boundaries | | [Compatibility](compatibility.md) | Evidence model and release matrix | +| [Testing](testing.md) | Suites layout, Make targets, latest results | Runnable cookbooks: [`examples/`](../examples/README.md). Integration suites: [`pulumi-tests/`](../pulumi-tests/README.md). +Test map and latest pass notes: [Testing](testing.md). ## See also diff --git a/docs/faq.md b/docs/faq.md index 554c33d..0ef2bbe 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -19,6 +19,7 @@ Yes. That is a primary use case. Pin the API major, seed a profile, and point clients at **HTTP `:8006`** (Compose) or your Ingress **HTTPS** hostname on Kubernetes. See [Clients](clients.md). For the Pulumi surface suite see [`pulumi-tests/`](../pulumi-tests/README.md). +Full test map and latest recorded results: [Testing](testing.md). ## Why do some OpenID / LDAP / ACME / Ceph calls “succeed” without remotes? diff --git a/docs/ru/README.md b/docs/ru/README.md index 8474934..2139cc8 100644 --- a/docs/ru/README.md +++ b/docs/ru/README.md @@ -27,9 +27,11 @@ | [FAQ](faq.md) | Краткие ответы | | [Архитектура](architecture.md) | Границы компонентов | | [Совместимость](compatibility.md) | Модель evidence и матрица релизов | +| [Тесты](testing.md) | Карта наборов, Make-цели, последние результаты | Исполняемые cookbook'и: [`examples/`](../../examples/README.ru.md). Интеграционные наборы: [`pulumi-tests/`](../../pulumi-tests/README.ru.md). +Карта тестов и последние прогоны: [Тесты](testing.md). ## См. также diff --git a/docs/ru/faq.md b/docs/ru/faq.md index ec93181..8decd64 100644 --- a/docs/ru/faq.md +++ b/docs/ru/faq.md @@ -19,6 +19,7 @@ PostgreSQL, а не процессы KVM/LXC. Да. Это один из основных сценариев. Закрепите мажор API, загрузите профиль seed и направьте клиентов на **HTTP `:8006`** (Compose) или Ingress **HTTPS** в Kubernetes. См. [Клиенты](clients.md). Набор Pulumi surface — [`pulumi-tests/`](../../pulumi-tests/README.ru.md). +Карта тестов и последние результаты: [Тесты](testing.md). ## Почему некоторые вызовы OpenID / LDAP / ACME / Ceph «успешны» без внешних систем? diff --git a/docs/ru/testing.md b/docs/ru/testing.md new file mode 100644 index 0000000..4557381 --- /dev/null +++ b/docs/ru/testing.md @@ -0,0 +1,52 @@ +**Language / Язык:** [English](../testing.md) | [Русский](testing.md) + +# Тесты + +Где живут наборы и как их запускать. + +## Структура + +| Путь | Назначение | +|---|---| +| `tests/unit/` | Офлайн unit-тесты (handlers, контракты, seed, workers, …) | +| `tests/integration/` | Тесты с PostgreSQL (миграции, lease задач) | +| `tests/compatibility/` | Surface probe, group smoke, proxmoxer HTTPS, verified-surface | +| `pulumi-tests/` | Pulumi surface (majors 6–9) + lifecycle `pulumi-proxmoxve`; HTML-отчёт | + +Seed: `app/simulation/seed.py` (`lab` для in-process surface CI, `small` для Compose / Pulumi). + +## Цели Make + +```bash +make test-unit # pytest: только unit +make test-integration # pytest -m integration (нужен Postgres) +make test-surface # каждый объявленный метод majors 6–9 +make test-compatibility # proxmoxer smoke (Compose --profile tls) +make ci # ruff + mypy + offline pytest + surface +make ci-all # ci + остальной integration + proxmoxer +make pulumi-tests # Pulumi surface + lifecycle; HTML-отчёт +``` + +GitHub Actions на push/PR в `main` запускает `make ci` и проверку Compose/Helm. +Перед крупными API-изменениями локально: `make ci-all` и `make pulumi-tests`. + +Отчёт Pulumi: `pulumi-tests/pulumi/reports/report.html` — см. +[`pulumi-tests/README.ru.md`](../../pulumi-tests/README.ru.md). + +## Последний локальный прогон (2026-07-18) + +Полный прогон без ограничений на этом checkout: + +| Gate | Результат | +|---|---| +| `make ci-all` | **PASS** (exit 0) | +| — unit (offline pytest) | 193 passed | +| — surface probe (majors 6–9) | PASS | +| — integration | 12 passed | +| — proxmoxer compatibility | PASS | +| `make pulumi-tests` | **PASS** (exit 0), ~28 с | +| — lifecycle (`pulumi-proxmoxve`) | PASS | +| — surface PVE 6.4-15 / 7.4-16 / 8.4.5 / 9.2.3 | PASS, `critical=0` | +| — coverage | **2324 / 2324** методов majors 6–9 | + +Артефакты: `pulumi-tests/pulumi/reports/report.html` (также `results.json`, `junit.xml`). diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..47d2123 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,52 @@ +**Language / Язык:** [English](testing.md) | [Русский](ru/testing.md) + +# Testing + +How the repository is tested and where each suite lives. + +## Layout + +| Location | Role | +|---|---| +| `tests/unit/` | Offline unit tests (handlers, contracts, seed, workers, …) | +| `tests/integration/` | PostgreSQL-backed tests (migrations, durable task leases) | +| `tests/compatibility/` | Surface probe, group smoke, proxmoxer HTTPS, verified-surface ledgers | +| `pulumi-tests/` | Pulumi surface matrix (majors 6–9) + `pulumi-proxmoxve` lifecycle; HTML report | + +Seed used by most suites: `app/simulation/seed.py` (`lab` for in-process surface CI, `small` for Compose / Pulumi). + +## Make targets + +```bash +make test-unit # pytest: unit only +make test-integration # pytest -m integration (needs Postgres) +make test-surface # every declared method on majors 6–9 +make test-compatibility # proxmoxer smoke (Compose --profile tls) +make ci # ruff + mypy + offline pytest + surface +make ci-all # ci + remaining integration + proxmoxer +make pulumi-tests # Pulumi surface + lifecycle; writes HTML report +``` + +GitHub Actions on push/PR to `main` runs `make ci` plus Compose/Helm validation. +Run `make ci-all` and `make pulumi-tests` locally before larger API changes. + +Pulumi report (after a suite run): +`pulumi-tests/pulumi/reports/report.html` — see [`pulumi-tests/README.md`](../pulumi-tests/README.md). + +## Latest local results (2026-07-18) + +Full unrestricted run on this checkout: + +| Gate | Result | +|---|---| +| `make ci-all` | **PASS** (exit 0) | +| — unit (offline pytest) | 193 passed | +| — surface probe (majors 6–9) | PASS | +| — integration | 12 passed | +| — proxmoxer compatibility | PASS | +| `make pulumi-tests` | **PASS** (exit 0), suite ~28s | +| — lifecycle (`pulumi-proxmoxve`) | PASS | +| — surface PVE 6.4-15 / 7.4-16 / 8.4.5 / 9.2.3 | PASS, `critical=0` | +| — coverage | **2324 / 2324** methods across majors 6–9 | + +Report artifact: `pulumi-tests/pulumi/reports/report.html` (also `results.json`, `junit.xml`). diff --git a/pulumi-tests/README.md b/pulumi-tests/README.md index 3378fd7..e0cf72d 100644 --- a/pulumi-tests/README.md +++ b/pulumi-tests/README.md @@ -58,7 +58,7 @@ CSRF=$(echo "$TICKET" | jq -r .data.CSRFPreventionToken) curl -s -b /tmp/pve.ck http://localhost:8006/api2/json/version | jq . curl -s -b /tmp/pve.ck -H "CSRFPreventionToken: $CSRF" \ -d 'vmid=100&name=demo' \ - http://localhost:8006/api2/json/nodes/pve01/qemu | jq . + http://localhost:8006/api2/json/nodes/pve1/qemu | jq . ``` Open the HTML report: `pulumi-tests/pulumi/reports/report.html`. @@ -67,4 +67,12 @@ Provider env (set by Compose): `PROXMOX_VE_ENDPOINT=https://tls-gateway:8443/` (internal suite TLS — `pulumi-proxmoxve` rejects `http://`), plus username/password /`INSECURE`. Surface probe uses `API_URL=http://simulator:8006`. Host lab URL remains plain `http://localhost:8006/` (Kubernetes HTTPS is Ingress-only). -Seed for CI/suite: `SEED_PROFILE=small` (Compose default). +Seed for CI/suite: `SEED_PROFILE=small` (Compose default). Default node name in +that profile is **`pve1`** (not `pve01`). + +## Latest local results (2026-07-18) + +`make pulumi-tests` → **Suite PASS** (~28s): lifecycle OK; surface majors +6.4-15 / 7.4-16 / 8.4.5 / 9.2.3 with `critical=0`; coverage **2324/2324**. +Report: `pulumi/reports/report.html`. Broader CI notes: +[docs/testing.md](../docs/testing.md). diff --git a/pulumi-tests/README.ru.md b/pulumi-tests/README.ru.md index 5bf2d09..d50a7c7 100644 --- a/pulumi-tests/README.ru.md +++ b/pulumi-tests/README.ru.md @@ -58,7 +58,7 @@ CSRF=$(echo "$TICKET" | jq -r .data.CSRFPreventionToken) curl -s -b /tmp/pve.ck http://localhost:8006/api2/json/version | jq . curl -s -b /tmp/pve.ck -H "CSRFPreventionToken: $CSRF" \ -d 'vmid=100&name=demo' \ - http://localhost:8006/api2/json/nodes/pve01/qemu | jq . + http://localhost:8006/api2/json/nodes/pve1/qemu | jq . ``` Отчёт: `pulumi-tests/pulumi/reports/report.html`. @@ -67,4 +67,12 @@ Env провайдера (Compose): `PROXMOX_VE_ENDPOINT=https://tls-gateway:844 (внутренний TLS suite — `pulumi-proxmoxve` не принимает `http://`), плюс username/password/`INSECURE`. Surface probe: `API_URL=http://simulator:8006`. Хостовый lab URL — plain `http://localhost:8006/` (HTTPS в K8s — только Ingress). -Seed для CI/suite: `SEED_PROFILE=small` (default Compose). +Seed для CI/suite: `SEED_PROFILE=small` (default Compose). Имя ноды в этом +профиле — **`pve1`** (не `pve01`). + +## Последний локальный прогон (2026-07-18) + +`make pulumi-tests` → **Suite PASS** (~28 с): lifecycle OK; surface majors +6.4-15 / 7.4-16 / 8.4.5 / 9.2.3 с `critical=0`; coverage **2324/2324**. +Отчёт: `pulumi/reports/report.html`. Общий CI: +[docs/ru/testing.md](../docs/ru/testing.md). diff --git a/pulumi-tests/docker/docker-compose.yml b/pulumi-tests/docker/docker-compose.yml index b6b3e0e..384232b 100644 --- a/pulumi-tests/docker/docker-compose.yml +++ b/pulumi-tests/docker/docker-compose.yml @@ -5,7 +5,7 @@ x-test-env: &test-env API_USER: ${API_USER:-root@pam} API_PASSWORD: ${API_PASSWORD:-secret} API_TOKEN: ${API_TOKEN:-root@pam!automation=automation-secret} - PVE_NODE: ${PVE_NODE:-pve01} + PVE_NODE: ${PVE_NODE:-pve1} PVE_STORAGE: ${PVE_STORAGE:-local-lvm} PVE_BRIDGE: ${PVE_BRIDGE:-vmbr0} API_TIMEOUT: ${API_TIMEOUT:-120} diff --git a/pulumi-tests/pulumi/programs/lifecycle/Pulumi.hxlife-1351a1c6e1f9.yaml b/pulumi-tests/pulumi/programs/lifecycle/Pulumi.hxlife-1351a1c6e1f9.yaml new file mode 100644 index 0000000..44ef0ef --- /dev/null +++ b/pulumi-tests/pulumi/programs/lifecycle/Pulumi.hxlife-1351a1c6e1f9.yaml @@ -0,0 +1,3 @@ +encryptionsalt: v1:dHE0479O4C4=:v1:qLAPiOnywZwAY0nr:lwYFJLsSb1ayad79xjxO05Tvn5EK0w== +config: + hx-lifecycle:smoke: "0" diff --git a/pulumi-tests/pulumi/programs/lifecycle/Pulumi.hxlife-64555365dd2f.yaml b/pulumi-tests/pulumi/programs/lifecycle/Pulumi.hxlife-64555365dd2f.yaml new file mode 100644 index 0000000..e9d07f6 --- /dev/null +++ b/pulumi-tests/pulumi/programs/lifecycle/Pulumi.hxlife-64555365dd2f.yaml @@ -0,0 +1,3 @@ +encryptionsalt: v1:Ttb4rpyx/lw=:v1:jISbowPFxMmrEKuc:YSoO8SdSx3symHxQE0vjSEQ2Mf/AyQ== +config: + hx-lifecycle:smoke: "0" diff --git a/pulumi-tests/pulumi/programs/lifecycle/__main__.py b/pulumi-tests/pulumi/programs/lifecycle/__main__.py index becfb28..f3923a8 100644 --- a/pulumi-tests/pulumi/programs/lifecycle/__main__.py +++ b/pulumi-tests/pulumi/programs/lifecycle/__main__.py @@ -18,7 +18,7 @@ from pvelib.api import Pve cfg = pulumi.Config() smoke = (cfg.get("smoke") or os.environ.get("SMOKE_ONLY") or "0") == "1" -node = os.environ.get("PVE_NODE", "pve01") +node = os.environ.get("PVE_NODE", "pve1") endpoint = ( os.environ.get("PROXMOX_VE_ENDPOINT") or "https://tls-gateway:8443/" ).rstrip("/") + "/" diff --git a/pulumi-tests/pulumi/pvelib/api.py b/pulumi-tests/pulumi/pvelib/api.py index 2e3bb7c..f8b8ff2 100644 --- a/pulumi-tests/pulumi/pvelib/api.py +++ b/pulumi-tests/pulumi/pvelib/api.py @@ -15,7 +15,7 @@ class Pve: root = os.environ.get("API_URL", "http://simulator:8006").rstrip("/") self.root = root self.base = root + "/api2/json" - self.node = os.environ.get("PVE_NODE", "pve01") + self.node = os.environ.get("PVE_NODE", "pve1") self.storage = os.environ.get("PVE_STORAGE", "local-lvm") self.bridge = os.environ.get("PVE_BRIDGE", "vmbr0") self._c = httpx.Client(base_url=self.base, timeout=120.0) diff --git a/pulumi-tests/pulumi/pvelib/surface.py b/pulumi-tests/pulumi/pvelib/surface.py index 9e316c5..56079ee 100644 --- a/pulumi-tests/pulumi/pvelib/surface.py +++ b/pulumi-tests/pulumi/pvelib/surface.py @@ -44,7 +44,7 @@ _UPID_RE = re.compile( ) _PATH_PARAM_EXAMPLES: dict[str, object] = { - "node": "pve01", + "node": "pve1", "vmid": 100, "storage": "local", "pool": "testpool", @@ -53,7 +53,7 @@ _PATH_PARAM_EXAMPLES: dict[str, object] = { "realm": "pam", "group": "admins", "role": "Administrator", - "upid": "UPID:pve01:00000001:00000001:65000001:qmstart:100:root@pam:", + "upid": "UPID:pve1:00000001:00000001:65000001:qmstart:100:root@pam:", "snapname": "snap1", "volume": "local:100/vm-100-disk-0.qcow2", "disk": "scsi0", @@ -89,7 +89,7 @@ _EXTRA_PATH: dict[str, object] = { "cidr": "10.0.0.0/24", "tokenid": "automation", "fabric_id": "fab1", - "node_id": "pve01", + "node_id": "pve1", "url_seq": "1", "route-map-id": "rm1", "order": "10", diff --git a/tests/compatibility/test_proxmoxer.py b/tests/compatibility/test_proxmoxer.py index 7fb1d68..63b988b 100644 --- a/tests/compatibility/test_proxmoxer.py +++ b/tests/compatibility/test_proxmoxer.py @@ -33,7 +33,7 @@ def test_proxmoxer_read_and_qemu_task_flow() -> None: assert proxmox.version.get()["version"] == "9.2.3" assert any(node["node"] == "pve1" for node in proxmox.nodes.get()) - assert any(vm["vmid"] == 100 for vm in proxmox.nodes("pve1").qemu.get()) + assert any(vm["vmid"] == 103 for vm in proxmox.nodes("pve1").qemu.get()) token_api = ProxmoxAPI( os.environ["PROXMOXER_HOST"], @@ -55,9 +55,9 @@ def test_proxmoxer_read_and_qemu_task_flow() -> None: ) assert readonly_api.nodes.get() assert readonly_api.nodes("pve1").status.get()["status"] == "online" - assert readonly_api.nodes("pve1").qemu("100").config.get()["vmid"] == 100 + assert readonly_api.nodes("pve1").qemu("103").config.get()["vmid"] == 103 with pytest.raises(ResourceException) as denied: - readonly_api.nodes("pve1").qemu("100").status.start.post() + readonly_api.nodes("pve1").qemu("103").status.start.post() assert denied.value.status_code == 403 token_endpoint = proxmox.access.users("root@pam").token("ephemeral") @@ -88,7 +88,7 @@ def test_proxmoxer_read_and_qemu_task_flow() -> None: token_value=os.getenv("PROXMOXER_STORAGE_TOKEN_SECRET", "storage-secret"), verify_ssl=False, ) - for vmid in ("100", "999999"): + for vmid in ("103", "999999"): with pytest.raises(ResourceException) as hidden: storage_api.nodes("pve1").qemu(vmid).config.get() assert hidden.value.status_code == 403 @@ -176,7 +176,7 @@ def test_proxmoxer_read_and_qemu_task_flow() -> None: token_value=os.getenv("PROXMOXER_OPERATOR_TOKEN_SECRET", "operator-secret"), verify_ssl=False, ) - status_resource = operator_api.nodes("pve1").qemu("100").status + status_resource = operator_api.nodes("pve1").qemu("103").status def run(operation: str, expected: str) -> None: upid = status_resource(operation).post() diff --git a/tests/unit/test_backup_handlers.py b/tests/unit/test_backup_handlers.py index c449943..aa7c6ac 100644 --- a/tests/unit/test_backup_handlers.py +++ b/tests/unit/test_backup_handlers.py @@ -86,11 +86,24 @@ async def test_backup_jobs_crud_and_shapes() -> None: pool = BackupPool() http = _request(pool) - listed = await registry.get("/cluster/backup", "GET")(http, {"values": {}}) + list_jobs = registry.get("/cluster/backup", "GET") + create_job = registry.get("/cluster/backup", "POST") + backup_info = registry.get("/cluster/backup-info", "GET") + not_backed_up = registry.get("/cluster/backup-info/not-backed-up", "GET") + included_volumes = registry.get("/cluster/backup/{id}/included_volumes", "GET") + delete_job = registry.get("/cluster/backup/{id}", "DELETE") + assert list_jobs is not None + assert create_job is not None + assert backup_info is not None + assert not_backed_up is not None + assert included_volumes is not None + assert delete_job is not None + + listed = await list_jobs(http, {"values": {}}) assert listed[0]["id"] == "backup-daily" assert listed[0]["schedule"] == "0 2 * * *" - await registry.get("/cluster/backup", "POST")( + await create_job( http, { "values": { @@ -103,24 +116,19 @@ async def test_backup_jobs_crud_and_shapes() -> None: ) assert "backup-weekly" in pool.metadata["backup_jobs"] - info = await registry.get("/cluster/backup-info", "GET")(http, {"values": {}}) + info = await backup_info(http, {"values": {}}) assert info == [{"subdir": "not-backed-up"}] - missing = await registry.get("/cluster/backup-info/not-backed-up", "GET")(http, {"values": {}}) - assert missing == [] # 100 covered by daily; after weekly both covered? weekly adds 200 + missing = await not_backed_up(http, {"values": {}}) # daily covers 100, weekly covers 200 → none missing assert missing == [] - included = await registry.get("/cluster/backup/{id}/included_volumes", "GET")( - http, {"values": {"id": "backup-daily"}} - ) + included = await included_volumes(http, {"values": {"id": "backup-daily"}}) assert included["children"][0]["id"] == 100 assert included["children"][0]["children"][0]["id"] == "scsi0" - await registry.get("/cluster/backup/{id}", "DELETE")(http, {"values": {"id": "backup-weekly"}}) + await delete_job(http, {"values": {"id": "backup-weekly"}}) assert "backup-weekly" not in pool.metadata["backup_jobs"] - missing_after = await registry.get("/cluster/backup-info/not-backed-up", "GET")( - http, {"values": {}} - ) + missing_after = await not_backed_up(http, {"values": {}}) assert missing_after[0]["vmid"] == 200 assert missing_after[0]["type"] == "lxc" diff --git a/tests/unit/test_task_worker.py b/tests/unit/test_task_worker.py index 8529ad5..bb1c57c 100644 --- a/tests/unit/test_task_worker.py +++ b/tests/unit/test_task_worker.py @@ -93,7 +93,10 @@ async def test_worker_retries_after_claim_failure() -> None: poll_seconds=0.001, ) running = asyncio.create_task(worker.run()) - await asyncio.sleep(0.01) + for _ in range(200): + if repository.attempts > 1: + break + await asyncio.sleep(0.01) worker.stop() await running