mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
231 lines
6.9 KiB
Python
231 lines
6.9 KiB
Python
"""env_branches — shim, ladder pairs, head/prod resolution, normalization.
|
|||
|
|
|
||
|
|
Pure domain helpers (pydantic-only); no DB. The read-time shim synthesizes a
|
||
|
|
degenerate single-branch ladder from ``default_branch`` when ``environments``
|
||
|
|
is null, so every consumer behaves identically until a real ladder is declared.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from roboco.models.env_branches import (
|
||
|
|
EnvRung,
|
||
|
|
effective_environments,
|
||
|
|
head_branch,
|
||
|
|
ladder_pairs,
|
||
|
|
normalize_environments,
|
||
|
|
prod_branch,
|
||
|
|
promotion_chain,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class _Proj:
|
||
|
|
"""Duck-typed project row (matches Project + ProjectTable surface)."""
|
||
|
|
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
*,
|
||
|
|
default_branch: str = "master",
|
||
|
|
environments: list[dict[str, str]] | None = None,
|
||
|
|
) -> None:
|
||
|
|
self.default_branch = default_branch
|
||
|
|
self.environments = environments
|
||
|
|
|
||
|
|
|
||
|
|
# --- effective_environments shim ------------------------------------------
|
||
|
|
|
||
|
|
|
||
|
|
def test_null_environments_synthesizes_degenerate_ladder() -> None:
|
||
|
|
proj = _Proj(default_branch="slave")
|
||
|
|
rungs = effective_environments(proj)
|
||
|
|
assert [(r.name, r.branch) for r in rungs] == [("head", "slave"), ("prod", "slave")]
|
||
|
|
|
||
|
|
|
||
|
|
def test_empty_environments_synthesizes_degenerate_ladder() -> None:
|
||
|
|
proj = _Proj(default_branch="master", environments=[])
|
||
|
|
rungs = effective_environments(proj)
|
||
|
|
assert [(r.name, r.branch) for r in rungs] == [
|
||
|
|
("head", "master"),
|
||
|
|
("prod", "master"),
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def test_missing_default_branch_falls_back_to_master() -> None:
|
||
|
|
proj = _Proj(default_branch="") # falsy default_branch
|
||
|
|
assert head_branch(proj) == "master"
|
||
|
|
assert prod_branch(proj) == "master"
|
||
|
|
|
||
|
|
|
||
|
|
def test_set_environments_returned_as_is_preserving_order() -> None:
|
||
|
|
proj = _Proj(
|
||
|
|
environments=[
|
||
|
|
{"name": "head", "branch": "dev"},
|
||
|
|
{"name": "qa", "branch": "qa"},
|
||
|
|
{"name": "prod", "branch": "master"},
|
||
|
|
]
|
||
|
|
)
|
||
|
|
rungs = effective_environments(proj)
|
||
|
|
assert [r.branch for r in rungs] == ["dev", "qa", "master"]
|
||
|
|
assert all(isinstance(r, EnvRung) for r in rungs)
|
||
|
|
|
||
|
|
|
||
|
|
# --- head_branch / prod_branch --------------------------------------------
|
||
|
|
|
||
|
|
|
||
|
|
def test_head_and_prod_single_rung() -> None:
|
||
|
|
proj = _Proj(environments=[{"name": "prod", "branch": "master"}])
|
||
|
|
assert head_branch(proj) == "master"
|
||
|
|
assert prod_branch(proj) == "master"
|
||
|
|
|
||
|
|
|
||
|
|
def test_head_and_prod_two_rungs() -> None:
|
||
|
|
proj = _Proj(
|
||
|
|
environments=[
|
||
|
|
{"name": "head", "branch": "slave"},
|
||
|
|
{"name": "prod", "branch": "master"},
|
||
|
|
]
|
||
|
|
)
|
||
|
|
assert head_branch(proj) == "slave"
|
||
|
|
assert prod_branch(proj) == "master"
|
||
|
|
|
||
|
|
|
||
|
|
def test_head_and_prod_four_rungs() -> None:
|
||
|
|
proj = _Proj(
|
||
|
|
environments=[
|
||
|
|
{"name": "head", "branch": "dev"},
|
||
|
|
{"name": "qa", "branch": "qa"},
|
||
|
|
{"name": "stag", "branch": "stag"},
|
||
|
|
{"name": "prod", "branch": "master"},
|
||
|
|
]
|
||
|
|
)
|
||
|
|
assert head_branch(proj) == "dev"
|
||
|
|
assert prod_branch(proj) == "master"
|
||
|
|
|
||
|
|
|
||
|
|
# --- ladder_pairs (prod -> head cascade) -----------------------------------
|
||
|
|
|
||
|
|
|
||
|
|
def test_ladder_pairs_empty_for_single_rung() -> None:
|
||
|
|
assert (
|
||
|
|
ladder_pairs(_Proj(environments=[{"name": "prod", "branch": "master"}])) == []
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_ladder_pairs_two_rungs() -> None:
|
||
|
|
proj = _Proj(
|
||
|
|
environments=[
|
||
|
|
{"name": "head", "branch": "dev"},
|
||
|
|
{"name": "prod", "branch": "master"},
|
||
|
|
]
|
||
|
|
)
|
||
|
|
pairs = ladder_pairs(proj)
|
||
|
|
assert [(u.branch, lower.branch) for u, lower in pairs] == [("master", "dev")]
|
||
|
|
|
||
|
|
|
||
|
|
def test_ladder_pairs_four_rungs_top_down() -> None:
|
||
|
|
proj = _Proj(
|
||
|
|
environments=[
|
||
|
|
{"name": "head", "branch": "dev"},
|
||
|
|
{"name": "qa", "branch": "qa"},
|
||
|
|
{"name": "stag", "branch": "stag"},
|
||
|
|
{"name": "prod", "branch": "master"},
|
||
|
|
]
|
||
|
|
)
|
||
|
|
# [(prod, stag), (stag, qa), (qa, head)] — merge upper into lower.
|
||
|
|
pairs = ladder_pairs(proj)
|
||
|
|
assert [(u.branch, lower.branch) for u, lower in pairs] == [
|
||
|
|
("master", "stag"),
|
||
|
|
("stag", "qa"),
|
||
|
|
("qa", "dev"),
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def test_ladder_pairs_lower_rung_is_never_prod() -> None:
|
||
|
|
"""The cascade's target is never prod by construction — only CEO merges prod."""
|
||
|
|
proj = _Proj(
|
||
|
|
environments=[
|
||
|
|
{"name": "head", "branch": "dev"},
|
||
|
|
{"name": "qa", "branch": "qa"},
|
||
|
|
{"name": "prod", "branch": "master"},
|
||
|
|
]
|
||
|
|
)
|
||
|
|
prod_name = prod_branch(proj)
|
||
|
|
for _upper, lower in ladder_pairs(proj):
|
||
|
|
assert lower.branch != prod_name
|
||
|
|
|
||
|
|
|
||
|
|
# --- promotion_chain (full-chain release promotion) ----------------------
|
||
|
|
|
||
|
|
|
||
|
|
def test_promotion_chain_empty_for_degenerate_ladder() -> None:
|
||
|
|
"""head==prod => nothing to promote (no-op release promotion)."""
|
||
|
|
assert promotion_chain(_Proj(default_branch="master")) == []
|
||
|
|
assert (
|
||
|
|
promotion_chain(_Proj(environments=[{"name": "prod", "branch": "master"}]))
|
||
|
|
== []
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_promotion_chain_two_rungs() -> None:
|
||
|
|
proj = _Proj(
|
||
|
|
environments=[
|
||
|
|
{"name": "head", "branch": "dev"},
|
||
|
|
{"name": "prod", "branch": "master"},
|
||
|
|
]
|
||
|
|
)
|
||
|
|
assert promotion_chain(proj) == ["dev"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_promotion_chain_four_rungs_head_first_excluding_prod() -> None:
|
||
|
|
proj = _Proj(
|
||
|
|
environments=[
|
||
|
|
{"name": "head", "branch": "dev"},
|
||
|
|
{"name": "qa", "branch": "qa"},
|
||
|
|
{"name": "stag", "branch": "stag"},
|
||
|
|
{"name": "prod", "branch": "master"},
|
||
|
|
]
|
||
|
|
)
|
||
|
|
assert promotion_chain(proj) == ["dev", "qa", "stag"]
|
||
|
|
|
||
|
|
|
||
|
|
# --- normalize_environments ------------------------------------------------
|
||
|
|
|
||
|
|
|
||
|
|
def test_normalize_none_returns_none() -> None:
|
||
|
|
assert normalize_environments(None) is None
|
||
|
|
assert normalize_environments([]) is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_normalize_strips_and_preserves_order() -> None:
|
||
|
|
out = normalize_environments(
|
||
|
|
[{"name": " head ", "branch": " dev "}, {"name": "prod", "branch": "master"}]
|
||
|
|
)
|
||
|
|
assert out == [
|
||
|
|
{"name": "head", "branch": "dev"},
|
||
|
|
{"name": "prod", "branch": "master"},
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def test_normalize_rejects_empty_name() -> None:
|
||
|
|
with pytest.raises(ValueError, match="non-empty name"):
|
||
|
|
normalize_environments([{"name": "", "branch": "dev"}])
|
||
|
|
|
||
|
|
|
||
|
|
def test_normalize_rejects_empty_branch() -> None:
|
||
|
|
with pytest.raises(ValueError, match="non-empty name"):
|
||
|
|
normalize_environments(
|
||
|
|
[{"name": "head", "branch": " "}]
|
||
|
|
) # branch trimmed to empty
|
||
|
|
|
||
|
|
|
||
|
|
def test_normalize_rejects_duplicate_branch() -> None:
|
||
|
|
with pytest.raises(ValueError, match="duplicate environment branch"):
|
||
|
|
normalize_environments(
|
||
|
|
[{"name": "head", "branch": "dev"}, {"name": "prod", "branch": "dev"}]
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_normalize_accepts_envrung_models() -> None:
|
||
|
|
out = normalize_environments([EnvRung(name="head", branch="dev")])
|
||
|
|
assert out == [{"name": "head", "branch": "dev"}]
|