diff --git a/Architecture/Methodology.md b/Architecture/Methodology.md index 9a2de299e..2ef028226 100644 --- a/Architecture/Methodology.md +++ b/Architecture/Methodology.md @@ -64,6 +64,56 @@ Deliverables: - Test data builders / factories - CI execution hook +### 6.1 Test Categories (Added) +To ensure consistent quality gates, adopt a layered test pyramid for every feature/module: + +| Layer | Purpose | Scope | Frequency | +|-------|---------|-------|-----------| +| Static / Lint | Syntax & style correctness | Whole repo | Every push/PR | +| Unit | Fast logic validation | Single function/class | Every push/PR | +| Component | Interactions of 2-3 modules (in-memory) | Narrow slice | Every push/PR | +| Integration | External boundaries (DB, network, broker) | Real infra or container | Nightly + gated merges | +| Contract | Provider/consumer schema & semantics | API/Events | On provider or schema change | +| Smoke | Sanity of deployable artifact start-up | Minimal runtime | Post-build & pre-deploy | +| End-to-End | Real user path & data flow | Full system | Release candidate | +| Performance | Latency/throughput budgets | Hot paths | Release candidate & quarterly | +| Security | AuthZ, injection, secret handling | Sensitive paths | Release candidate & scheduled | +| Chaos/Resilience | Failure handling, recovery | Selected subsystems | Scheduled | + +#### Minimum Required Per Feature +- Unit tests for new pure logic (>=80% function coverage for changed lines) +- One component test if cross-module interaction added +- Smoke test ensuring startup of new daemon/CLI entry point +- Contract test when altering externally consumed format (CLI JSON, API, event schema) + +#### Test Naming Conventions +``` +/test___.py +Examples: +unit/test_role_assigner__selects_first_management_server.py +component/test_peer_identity_flow__consensus_reached.py +smoke/test_agent_startup__exits_zero_help.py +``` + +#### Execution Profiles +- Quick suite (CI default): unit + component + smoke (<60s target) +- Full suite (nightly): + integration + contract + e2e + +#### Skipping & Markers (Pytest) +```python +import pytest +@pytest.mark.slow +@pytest.mark.integration +@pytest.mark.contract +``` +CI filters: `pytest -m "not slow"` for quick pass. + +#### Flakiness Policy +Any flaky test must be: +1. Quarantined via marker `@pytest.mark.flaky` within 24h +2. Issue created with reproduction details +3. Fixed or removed within 72h + ## 7. Core Business Logic (Happy Path) Implement pure logic first. - Keep logic side-effect free where possible @@ -202,6 +252,16 @@ Deliverables: - Performance: PASS/FAIL (budget adherence) - Documentation: PASS/FAIL (operator readiness) +### Automated Quality Gate Mapping (Added) +| Gate | Enforcement | Tooling | Threshold | +|------|-------------|---------|-----------| +| Build | CI job `build` | compiler + shellcheck | 0 errors | +| Unit Coverage | CI job `test` | pytest + coverage | >= 75% changed lines | +| Lint | CI job `lint` | ruff/flake8/shellcheck | 0 errors (warnings allowed) | +| Security | CI job `security` | bandit + secret scan | 0 HIGH findings | +| Smoke | CI job `smoke` | startup script & py_compile | 100% pass | +| Performance (target) | scheduled | locust/k6/JMH | baseline recorded | + --- ## CloudStack / VNF Mapping Quick Reference - API Contract → Commands (`execute()` wraps errors) @@ -273,4 +333,12 @@ Always prefer delivering one working end-to-end path over 20 partially complete If a change cannot be validated (run, test, observed), it isn't progress. --- -*Last updated: 2025-11-07* +## Test Implementation Checklist (Added) +Before merging any feature branch: +- [ ] Unit tests cover new decision branches +- [ ] Smoke test validates daemon/CLI starts (help, py_compile) +- [ ] Negative test included for at least one failure mode +- [ ] Secret handling validated (no accidental print of shared_secret) +- [ ] HMAC logic verified (sign + tamper fail) + +*Last updated: 2025-11-08* diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..fa5504ad2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "build-hive" +version = "0.0.1" +requires-python = ">=3.11" +dependencies = [] + +[tool.pytest.ini_options] +addopts = "-q --disable-warnings" +pythonpath = ["scripts/bootstrap/ubuntu24"] diff --git a/scripts/bootstrap/ubuntu24/hive_lib.py b/scripts/bootstrap/ubuntu24/hive_lib.py new file mode 100644 index 000000000..1f5c8bd24 --- /dev/null +++ b/scripts/bootstrap/ubuntu24/hive_lib.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Hive shared utilities for unit testing and logic reuse. +Includes role suggestion logic and message signing/validation helpers. +""" +from collections import Counter +import json, hmac, hashlib + +MAGIC = "BUILD/HELLO/v1" + + +def suggest_cloudstack_role(peers, vnf_available=False): + """Suggest a role based on current peers. + peers: list of dicts with 'role' or nested identity role + vnf_available: bool indicating if a VNF broker is reachable + Returns dict: {role, packages, config, reason} + """ + roles = Counter((p.get('role') or (p.get('identity') or {}).get('role') or 'unknown') for p in peers) + if roles.get('management-server', 0) == 0: + return { + 'role': 'management-server', + 'packages': ['mysql-server','nfs-kernel-server','openjdk-17-jdk','git'], + 'config': {'db': 'mysql', 'nfs': True}, + 'reason': 'First management server needed' + } + if roles.get('cloudstack-builder', 0) < 2: + return { + 'role': 'cloudstack-builder', + 'packages': ['openjdk-17-jdk','maven','git','build-essential'], + 'config': {'build_threads': 8}, + 'reason': 'Increase build capacity' + } + if roles.get('kvm-hypervisor', 0) == 0: + return { + 'role': 'kvm-hypervisor', + 'packages': ['qemu-kvm','libvirt-daemon-system','bridge-utils'], + 'config': {'virt_network': 'default'}, + 'reason': 'Provide hypervisor for functional tests' + } + if vnf_available and roles.get('vnf-tester', 0) == 0: + return { + 'role': 'vnf-tester', + 'packages': ['python3-requests','docker.io'], + 'config': {'broker_port': 8443}, + 'reason': 'Validate VNF broker operations' + } + return { + 'role': 'test-runner', + 'packages': ['python3-pytest','curl','git'], + 'config': {'parallel': 4}, + 'reason': 'Expand test execution capacity' + } + + +def sign(payload_bytes: bytes, secret: bytes) -> str: + """HMAC-SHA256 sign a bytes payload with secret; returns hex digest""" + if not secret: + return '' + return hmac.new(secret, payload_bytes, hashlib.sha256).hexdigest() + + +def valid(message: dict, secret: bytes, magic: str = MAGIC) -> bool: + """Validate message envelope of form {magic, payload, sig} using secret.""" + if message.get('magic') != magic: + return False + pl = message.get('payload') + sig = message.get('sig', '') + if not isinstance(pl, dict): + return False + raw = json.dumps(pl, separators=(',',':')).encode('utf-8') + if secret: + mac = hmac.new(secret, raw, hashlib.sha256).hexdigest() + return hmac.compare_digest(mac, sig) + return True diff --git a/scripts/test/run_tests.sh b/scripts/test/run_tests.sh new file mode 100644 index 000000000..8b7375d9d --- /dev/null +++ b/scripts/test/run_tests.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail +python3 -m pip install --upgrade pip >/dev/null 2>&1 || true +python3 -m pip install pytest >/dev/null 2>&1 || true +pytest -q diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..65140f2e3 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# tests package diff --git a/tests/smoke/test_smoke_scripts.py b/tests/smoke/test_smoke_scripts.py new file mode 100644 index 000000000..a39306abb --- /dev/null +++ b/tests/smoke/test_smoke_scripts.py @@ -0,0 +1,26 @@ +import subprocess, sys + + +def run(cmd): + return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + +def test_py_compile_scripts(): + for path in [ + 'scripts/bootstrap/ubuntu24/peer_agent.py', + 'scripts/bootstrap/ubuntu24/advisor.py', + 'scripts/bootstrap/ubuntu24/message_bridge.py', + ]: + r = run([sys.executable, '-m', 'py_compile', path]) + assert r.returncode == 0, f"py_compile failed for {path}: {r.stderr}" + + +def test_advisor_help_exits_zero(): + r = run([sys.executable, 'scripts/bootstrap/ubuntu24/advisor.py', '--help']) + assert r.returncode == 0 + assert 'usage' in (r.stdout + r.stderr).lower() + + +def test_message_bridge_runs(): + r = run([sys.executable, 'scripts/bootstrap/ubuntu24/message_bridge.py', 'identity_assigned', '{"hostname":"x","role":"y"}']) + assert r.returncode == 0 diff --git a/tests/test_hive_lib_roles.py b/tests/test_hive_lib_roles.py new file mode 100644 index 000000000..2dac72f5f --- /dev/null +++ b/tests/test_hive_lib_roles.py @@ -0,0 +1,42 @@ +from scripts.bootstrap.ubuntu24.hive_lib import suggest_cloudstack_role + + +def test_first_management_server(): + peers = [] + advice = suggest_cloudstack_role(peers) + assert advice['role'] == 'management-server' + assert 'mysql-server' in advice['packages'] + + +def test_second_role_cloudstack_builder_needed(): + peers = [ {'role': 'management-server'} ] + advice = suggest_cloudstack_role(peers) + assert advice['role'] == 'cloudstack-builder' + assert 'maven' in advice['packages'] + + +def test_hypervisor_after_two_builders(): + peers = [ {'role': 'management-server'}, {'role': 'cloudstack-builder'}, {'role': 'cloudstack-builder'} ] + advice = suggest_cloudstack_role(peers) + assert advice['role'] == 'kvm-hypervisor' + assert 'qemu-kvm' in advice['packages'] + + +def test_vnf_tester_when_broker_available(): + peers = [ {'role': 'management-server'}, {'role': 'cloudstack-builder'}, {'role': 'cloudstack-builder'}, {'role': 'kvm-hypervisor'} ] + advice = suggest_cloudstack_role(peers, vnf_available=True) + assert advice['role'] == 'vnf-tester' + assert 'docker.io' in advice['packages'] + + +def test_default_test_runner_when_all_present(): + peers = [ + {'role': 'management-server'}, + {'role': 'cloudstack-builder'}, + {'role': 'cloudstack-builder'}, + {'role': 'kvm-hypervisor'}, + {'role': 'vnf-tester'}, + ] + advice = suggest_cloudstack_role(peers) + assert advice['role'] == 'test-runner' + assert any('pytest' in p for p in advice['packages']) diff --git a/tests/test_hive_lib_signing.py b/tests/test_hive_lib_signing.py new file mode 100644 index 000000000..55eadb88b --- /dev/null +++ b/tests/test_hive_lib_signing.py @@ -0,0 +1,33 @@ +from scripts.bootstrap.ubuntu24.hive_lib import sign, valid, MAGIC +import json + +SECRET = b'supersecret' + + +def make_payload(): + return {'kind': 'HELLO', 'node_id': 'x', 'hostname': 'h', 'primary_ip': '1.2.3.4', 'candidate_ip': None, 'ts': '2025-01-01T00:00:00Z'} + + +def test_sign_and_valid_roundtrip(): + pl = make_payload() + raw = json.dumps(pl, separators=(',',':')).encode('utf-8') + sig = sign(raw, SECRET) + envelope = {'magic': MAGIC, 'payload': pl, 'sig': sig} + assert valid(envelope, SECRET) is True + + +def test_invalid_magic(): + pl = make_payload() + raw = json.dumps(pl, separators=(',',':')).encode('utf-8') + sig = sign(raw, SECRET) + envelope = {'magic': 'WRONG', 'payload': pl, 'sig': sig} + assert valid(envelope, SECRET) is False + + +def test_tampered_payload(): + pl = make_payload() + raw = json.dumps(pl, separators=(',',':')).encode('utf-8') + sig = sign(raw, SECRET) + pl['hostname'] = 'evil' + envelope = {'magic': MAGIC, 'payload': pl, 'sig': sig} + assert valid(envelope, SECRET) is False