Some checks failed
continuous-integration/drone/push Build is failing
Declare intentional skips + custom-html-tiny functional test; 4-rung level ladder
- recipe_meta.EXPECTED_NA = {rung: reason} lists intentionally-skipped rungs; any
essential rung skipped and not listed is unintentional. Skips still cap the level
(never inflate). results.json: skips:{intentional,unintentional} + level_cap_rung.
- Level ladder = the four essential rungs (install, upgrade, backup/restore,
functional; top = L4). integration & recipe-local are optional, not leveled
(SSO still enforced for the run verdict, unchanged).
- Card shows skipped rungs as INTENTIONAL SKIP (green, reason below) / UNINTENTIONAL
SKIP (amber); level badge gains an expected/gap? third segment.
- custom-html-tiny: functional serve test (exact-byte round-trip + 404); declares
backup_restore intentionally skipped (stateless static server).
Independently verified by the adversary: 138 unit tests pass cold; live full-stage
run on custom-html-tiny green (upgrade tier ran; level 2; correct skips/badge);
clean teardown.
129 lines
3.7 KiB
Python
129 lines
3.7 KiB
Python
"""Unit tests for the Phase-3 level ladder (harness.level), plan-phase3-results-ux.md §4.1 / R1.
|
|
|
|
Pure function — no I/O. Proves the YunoHost gap-caps-the-level semantics, including the U0 gate
|
|
acceptance: a recipe that climbs through L4 reports 4, and one that fails at L2 is capped at 1
|
|
(the level just below the failed rung). Run cold with: cc-ci-run -m pytest tests/unit/test_level.py -q
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
|
|
from harness import level as L # noqa: E402
|
|
|
|
|
|
def _rungs(
|
|
install="pass",
|
|
upgrade="pass",
|
|
backup_restore="pass",
|
|
functional="pass",
|
|
):
|
|
return {
|
|
"install": install,
|
|
"upgrade": upgrade,
|
|
"backup_restore": backup_restore,
|
|
"functional": functional,
|
|
}
|
|
|
|
|
|
# ---- the ladder: four essential rungs, top is L4 (functional) ----
|
|
|
|
|
|
def test_full_clean_climb_to_L4():
|
|
# All four essential rungs pass → L4 (the top; integration/recipe-local are optional, not leveled).
|
|
lvl, reason = L.compute_level(_rungs())
|
|
assert lvl == 4
|
|
assert reason == ""
|
|
|
|
|
|
def test_fails_at_L2_capped_at_L1():
|
|
# GATE: upgrade fails → capped at L1 even though higher rungs would pass.
|
|
lvl, reason = L.compute_level(_rungs(upgrade="fail", backup_restore="pass", functional="pass"))
|
|
assert lvl == 1
|
|
assert "L2" in reason and "FAILED" in reason
|
|
|
|
|
|
# ---- L0 / install ----
|
|
|
|
|
|
def test_install_fail_is_L0():
|
|
lvl, reason = L.compute_level(_rungs(install="fail"))
|
|
assert lvl == 0
|
|
assert "L1" in reason and "FAILED" in reason
|
|
|
|
|
|
# ---- gap-caps semantics: a higher pass can't rescue a lower gap ----
|
|
|
|
|
|
def test_higher_pass_does_not_rescue_lower_na():
|
|
# backup/restore N/A (stateless app) caps at L2 even though functional would pass.
|
|
lvl, reason = L.compute_level(_rungs(backup_restore="na", functional="pass"))
|
|
assert lvl == 2
|
|
assert "L3" in reason and "N/A" in reason
|
|
|
|
|
|
def test_upgrade_na_caps_at_L1():
|
|
# only one published version → no upgrade possible → N/A caps at L1 (upgrade is essential).
|
|
lvl, reason = L.compute_level(_rungs(upgrade="na"))
|
|
assert lvl == 1
|
|
assert "L2" in reason and "N/A" in reason
|
|
|
|
|
|
def test_functional_na_caps_at_L3():
|
|
# no recipe-specific functional tests → functional N/A caps at L3.
|
|
lvl, reason = L.compute_level(_rungs(functional="na"))
|
|
assert lvl == 3
|
|
assert "L4" in reason and "N/A" in reason
|
|
|
|
|
|
def test_functional_fail_caps_at_L3():
|
|
lvl, reason = L.compute_level(_rungs(functional="fail"))
|
|
assert lvl == 3
|
|
assert "L4" in reason and "FAILED" in reason
|
|
|
|
|
|
# ---- input validation ----
|
|
|
|
|
|
def test_invalid_status_raises():
|
|
bad = _rungs()
|
|
bad["functional"] = "passed" # not in the vocabulary
|
|
try:
|
|
L.compute_level(bad)
|
|
except ValueError:
|
|
return
|
|
raise AssertionError("expected ValueError on invalid rung status")
|
|
|
|
|
|
# ---- helpers: backup_restore_status ----
|
|
|
|
|
|
def test_backup_restore_status_pass():
|
|
assert L.backup_restore_status("pass", "pass", True) == "pass"
|
|
|
|
|
|
def test_backup_restore_status_not_capable_is_na():
|
|
assert L.backup_restore_status("skip", "skip", False) == "na"
|
|
|
|
|
|
def test_backup_restore_status_fail_on_either():
|
|
assert L.backup_restore_status("pass", "fail", True) == "fail"
|
|
assert L.backup_restore_status("fail", "pass", True) == "fail"
|
|
|
|
|
|
def test_backup_restore_partial_is_na():
|
|
# backup-capable but restore didn't run cleanly (not pass, not fail) → cannot claim L3
|
|
assert L.backup_restore_status("pass", "skip", True) == "na"
|
|
|
|
|
|
# ---- helpers: tier_to_rung ----
|
|
|
|
|
|
def test_tier_to_rung_mapping():
|
|
assert L.tier_to_rung("pass") == "pass"
|
|
assert L.tier_to_rung("fail") == "fail"
|
|
assert L.tier_to_rung("skip") == "na"
|
|
assert L.tier_to_rung(None) == "na"
|