Files
nix-estimator/tests/test_cli.py
T
Oleks 9553d3263d provision: reconcile EphemeralBuilder renderer with real CRD (#14)
Emit N documents (one per node; no replica field) using the real
arbitrage.oleks.space/v1alpha1 schema: top-level class + requirements
{arch,cores,ramGb}, namespace builder-arbitrage, class label. Rename
public API to render_ephemeral_builders; wire --node-ram-gb into ramGb.
2026-07-08 19:59:24 +03:00

192 lines
6.2 KiB
Python
Raw 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.
"""CLI-level tests for grid parsing / normalization (issue #11)."""
import json
import pytest
from nix_estimator import cli
def _p(name):
return f"/nix/store/{'0' * 32}-{name}.drv"
@pytest.fixture
def stub_graph(monkeypatch, tmp_path):
"""Stub out the nix-invoking graph layer with a deterministic toy DAG."""
leaves = [_p(f"heavylib{i}") for i in range(4)]
root, sink = _p("root"), _p("sink")
closure = {d: {} for d in [root, *leaves, sink]}
preds = {root: [], sink: leaves, **{leaf: [root] for leaf in leaves}}
nodes = set(closure)
monkeypatch.setattr(cli.graph, "derivation_closure", lambda *a, **k: closure)
monkeypatch.setattr(cli.graph, "to_build_set", lambda *a, **k: nodes)
monkeypatch.setattr(cli.graph, "build_dag", lambda *a, **k: (preds, nodes))
hist = tmp_path / "history.json"
hist.write_text(
json.dumps(
{"root": 1.0, "sink": 1.0, **{f"heavylib{i}": 20.0 for i in range(4)}}
)
)
return str(hist)
def _run_json(capsys, argv):
assert cli.main(argv) == 0
return json.loads(capsys.readouterr().out)
def test_unsorted_cores_match_sorted(stub_graph, capsys):
sorted_out = _run_json(
capsys, [".#x", "--history", stub_graph, "--cores", "8,16,32", "--json"]
)
unsorted_out = _run_json(
capsys, [".#x", "--history", stub_graph, "--cores", "32,8,16", "--json"]
)
assert unsorted_out["recommendation"] == sorted_out["recommendation"]
assert unsorted_out["grid"] == sorted_out["grid"]
def test_unsorted_nodes_match_sorted(stub_graph, capsys):
sorted_out = _run_json(
capsys, [".#x", "--history", stub_graph, "--nodes", "1,2,4,8", "--json"]
)
unsorted_out = _run_json(
capsys, [".#x", "--history", stub_graph, "--nodes", "8,2,1,4", "--json"]
)
assert unsorted_out["recommendation"] == sorted_out["recommendation"]
def test_non_positive_cores_rejected(stub_graph, capsys):
with pytest.raises(SystemExit) as exc:
cli.main([".#x", "--history", stub_graph, "--cores", "8,0,16", "--json"])
assert exc.value.code != 0
assert "positive" in capsys.readouterr().err
def test_negative_nodes_rejected(stub_graph, capsys):
with pytest.raises(SystemExit) as exc:
cli.main([".#x", "--history", stub_graph, "--nodes", "1,-2,4", "--json"])
assert exc.value.code != 0
def test_non_integer_cores_rejected(stub_graph, capsys):
with pytest.raises(SystemExit) as exc:
cli.main([".#x", "--history", stub_graph, "--cores", "8,abc", "--json"])
assert exc.value.code != 0
assert "integer" in capsys.readouterr().err
# --- new feature surfaces (issues #12#16) ---------------------------------
def test_backcompat_plain_estimate(stub_graph, capsys):
"""Plain `nix-estimate <attr> --json` still parses with no subcommand."""
out = _run_json(capsys, [".#foo", "--history", stub_graph, "--json"])
assert out["attr"] == ".#foo"
assert out["max_jobs"] == 1
assert out["node_ram_gb"] is None
def test_max_jobs_accepted_and_affects_output(stub_graph, capsys):
base = _run_json(capsys, [".#x", "--history", stub_graph, "--json"])
wide = _run_json(
capsys, [".#x", "--history", stub_graph, "--max-jobs", "4", "--json"]
)
assert base["max_jobs"] == 1 and wide["max_jobs"] == 4
# Sharing cores across 4 jobs changes at least one grid makespan.
assert wide["grid"] != base["grid"]
def test_node_ram_gb_accepted(stub_graph, capsys):
out = _run_json(
capsys,
[
".#x",
"--history",
stub_graph,
"--max-jobs",
"4",
"--node-ram-gb",
"8",
"--json",
],
)
assert out["node_ram_gb"] == 8.0
def test_timeline_renders_report(stub_graph, capsys):
assert cli.main([".#x", "--history", stub_graph, "--timeline"]) == 0
out = capsys.readouterr().out
assert "timeline — recommended shape" in out
assert "node0" in out
def test_timeline_json_carries_assignments(stub_graph, capsys):
out = _run_json(capsys, [".#x", "--history", stub_graph, "--timeline", "--json"])
assert "timeline" in out
tl = out["timeline"]
assert tl["makespan_min"] > 0
# every scheduled task name appears in some lane
tasks = [t[0] for lane in tl["assignments"].values() for t in lane]
assert "sink" in tasks
def test_provision_emits_yaml_to_stdout(stub_graph, capsys):
assert cli.main([".#x", "--history", stub_graph, "--provision"]) == 0
cap = capsys.readouterr()
assert "kind: EphemeralBuilder" in cap.out
assert "apiVersion: arbitrage.oleks.space/v1alpha1" in cap.out
assert "class: nix-builder" in cap.out
assert "requirements:" in cap.out and "cores:" in cap.out
assert "namespace: builder-arbitrage" in cap.out
# human report is diverted to stderr, not stdout
assert "RECOMMENDATION" in cap.err
assert "RECOMMENDATION" not in cap.out
def test_provision_custom_class_and_name(stub_graph, capsys):
assert (
cli.main(
[
".#x",
"--history",
stub_graph,
"--provision",
"runner",
"--provision-name",
"my-builder",
]
)
== 0
)
out = capsys.readouterr().out
assert "class: runner" in out
assert "name: my-builder-1" in out
def test_mine_subcommand_writes_json(monkeypatch, capsys, tmp_path):
"""`mine` runs mine_history (stubbed) and writes {name: minutes} JSON."""
calls = []
def fake_mine_history(installables, **kw):
calls.append(list(installables))
return {"hello": 2.0, "world": 5.0}
monkeypatch.setattr(cli.miner, "mine_history", fake_mine_history)
out_file = tmp_path / "hist.json"
rc = cli.main(["mine", ".#hello", ".#world", "--repeat", "3", "-o", str(out_file)])
assert rc == 0
assert len(calls) == 3 # repeated
data = json.loads(out_file.read_text())
assert data == {"hello": 2.0, "world": 5.0}
def test_mine_subcommand_to_stdout(monkeypatch, capsys):
monkeypatch.setattr(cli.miner, "mine_history", lambda inst, **kw: {"pkg": 1.5})
assert cli.main(["mine", ".#pkg"]) == 0
assert json.loads(capsys.readouterr().out) == {"pkg": 1.5}