Files
Oleks ae74db435a Add versioned --json sizing block and on-demand CI window to --provision (oleks/nix-estimator#25)
--json now emits a stable sizing:{version:1,arch,nodes,cores,ram_gb,est_makespan_min}
block reusing provision.arch_for_system + estimate.peak_ram_gb_per_node. render_ephemeral_builders
now derives requirements.maxDuration + parking + policy from est_makespan_min for class ci.
2026-07-12 20:41:50 +03:00

209 lines
7.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for the --provision EphemeralBuilder renderer (issue #14).
These pin the real ``arbitrage.oleks.space/v1alpha1`` EphemeralBuilder CRD
contract: one document per node (no replica field), the ``requirements`` block,
arch mapping, namespace/label conventions, and enum/count validation.
"""
import pytest
from nix_estimator.provision import arch_for_system, render_ephemeral_builders
def _docs(yaml: str) -> list[str]:
return [d for d in yaml.split("---\n") if d.strip()]
def test_arch_mapping():
assert arch_for_system("aarch64-linux") == "arm64"
assert arch_for_system("x86_64-linux") == "amd64"
assert arch_for_system("aarch64-darwin") == "arm64"
assert arch_for_system("x86_64-darwin") == "amd64"
# unknown / missing systems fall back to amd64
assert arch_for_system(None) == "amd64"
assert arch_for_system("riscv64-linux") == "amd64"
def test_real_schema_shape():
yaml = render_ephemeral_builders(
nodes=4, cores=16, system="aarch64-linux", est_makespan_min=12.34
)
assert "apiVersion: arbitrage.oleks.space/v1alpha1" in yaml
assert "kind: EphemeralBuilder" in yaml
# top-level class + requirements block (not replicas/top-level cores/arch)
assert "class: nix-builder" in yaml
assert "requirements:" in yaml
assert "arch: arm64" in yaml
assert "cores: 16" in yaml
assert "ramGb: 32" in yaml # default ~2 GiB/core
assert "replicas" not in yaml
# metadata conventions
assert "namespace: builder-arbitrage" in yaml
assert "arbitrage.oleks.space/class: nix-builder" in yaml
# makespan lives in a non-operator comment, not an arbitrage.* annotation
assert "# nix-estimator: estimated makespan 12.3 min" in yaml
assert "arbitrage.oleks.space/estimated-makespan" not in yaml
assert yaml.endswith("\n")
def test_one_document_per_node():
yaml = render_ephemeral_builders(nodes=4, cores=16, system="aarch64-linux")
docs = _docs(yaml)
assert len(docs) == 4
# each document has a unique metadata.name suffixed by its index
names = [ln.strip() for d in docs for ln in d.splitlines() if "name:" in ln]
assert names == [
"name: nix-estimator-arm64-16c-1",
"name: nix-estimator-arm64-16c-2",
"name: nix-estimator-arm64-16c-3",
"name: nix-estimator-arm64-16c-4",
]
def test_single_node_single_document():
yaml = render_ephemeral_builders(
nodes=1, cores=32, system="x86_64-linux", builder_class="ci", name="my-builder"
)
assert len(_docs(yaml)) == 1
assert "arch: amd64" in yaml
assert "class: ci" in yaml
assert "arbitrage.oleks.space/class: ci" in yaml
assert "name: my-builder-1" in yaml
def test_explicit_ram_gb_wins():
yaml = render_ephemeral_builders(nodes=1, cores=8, system="x86_64-linux", ram_gb=64)
assert "ramGb: 64" in yaml
# default would have been 16 (2 GiB * 8 cores)
assert "ramGb: 16" not in yaml
def test_makespan_omitted_when_none():
yaml = render_ephemeral_builders(nodes=2, cores=8, system="x86_64-linux")
assert "estimated makespan" not in yaml
# a sensible default name is derived from arch + cores
assert "name: nix-estimator-amd64-8c-1" in yaml
def test_output_is_deterministic():
kwargs = dict(nodes=3, cores=16, system="aarch64-linux", est_makespan_min=5.0)
assert render_ephemeral_builders(**kwargs) == render_ephemeral_builders(**kwargs)
def test_invalid_counts_rejected():
with pytest.raises(ValueError):
render_ephemeral_builders(nodes=0, cores=8, system="x86_64-linux")
with pytest.raises(ValueError):
render_ephemeral_builders(nodes=1, cores=0, system="x86_64-linux")
def test_invalid_class_rejected():
with pytest.raises(ValueError):
render_ephemeral_builders(
nodes=1, cores=8, system="x86_64-linux", builder_class="bogus"
)
def test_invalid_arch_rejected():
# arch_for_system never yields an invalid arch, so exercise the guard by
# confirming only the two GOARCH labels are ever emitted.
for system in ("aarch64-linux", "x86_64-linux", None, "riscv64-linux"):
arch = arch_for_system(system)
assert arch in ("amd64", "arm64")
# --- on-demand CI window (issue #25) ---------------------------------------
def test_ci_window_byte_exact():
"""class ci + a known makespan emits a byte-identical windowed CR.
makespan 12.34 -> maxDuration ceil(12.34*1.5)=19m, maxRunningTTL
ceil(19*1.5)=29m, ttlHardDelete ceil(19*2)=38m. This fixture is the frozen
machine contract; update it deliberately if the derivation changes.
"""
yaml = render_ephemeral_builders(
nodes=1,
cores=32,
system="x86_64-linux",
builder_class="ci",
name="ci-slot",
ram_gb=64,
est_makespan_min=12.34,
)
expected = (
"# nix-estimator: estimated makespan 12.3 min (1 node(s) × 32 cores)\n"
"apiVersion: arbitrage.oleks.space/v1alpha1\n"
"kind: EphemeralBuilder\n"
"metadata:\n"
" name: ci-slot-1\n"
" namespace: builder-arbitrage\n"
" labels:\n"
" arbitrage.oleks.space/class: ci\n"
"spec:\n"
" class: ci\n"
" requirements:\n"
" arch: amd64\n"
" cores: 32\n"
" ramGb: 64\n"
" maxDuration: 19m\n"
" policy:\n"
" prefer: dependable\n"
" allowNixbuildFallback: false\n"
" parking:\n"
" policy: cost\n"
" idleGrace: 5m\n"
" maxRunningTTL: 29m\n"
" ttlHardDelete: 38m\n"
)
assert yaml == expected
def test_ci_window_derivation_math():
"""maxDuration/TTLs are ceil-derived multiples of the makespan."""
yaml = render_ephemeral_builders(
nodes=1,
cores=8,
system="x86_64-linux",
builder_class="ci",
est_makespan_min=10.0,
)
# 10 -> maxDuration ceil(15)=15m, maxRunningTTL ceil(22.5)=23m, ttl 30m
assert "maxDuration: 15m" in yaml
assert "maxRunningTTL: 23m" in yaml
assert "ttlHardDelete: 30m" in yaml
assert "policy: cost" in yaml
assert "idleGrace: 5m" in yaml
assert "prefer: dependable" in yaml
assert "allowNixbuildFallback: false" in yaml
def test_non_ci_class_has_no_window():
"""nix-builder/runner classes never carry the window fields (unchanged)."""
for cls in ("nix-builder", "runner"):
yaml = render_ephemeral_builders(
nodes=1,
cores=16,
system="aarch64-linux",
builder_class=cls,
est_makespan_min=12.34,
)
assert "maxDuration" not in yaml
assert "parking" not in yaml
assert "policy:" not in yaml
def test_ci_window_omitted_without_makespan():
"""class ci with no makespan can't derive a window; emit none."""
yaml = render_ephemeral_builders(
nodes=1,
cores=16,
system="x86_64-linux",
builder_class="ci",
)
assert "maxDuration" not in yaml
assert "parking" not in yaml
# base requirements still present
assert "class: ci" in yaml
assert "arch: amd64" in yaml