223 lines
7.0 KiB
Python
223 lines
7.0 KiB
Python
"""Tests for dry-run parsing and DAG restriction in :mod:`nix_estimator.graph`."""
|
|
|
|
from nix_estimator import graph
|
|
|
|
# --- captured `nix build --dry-run` text (issue #5) ------------------------
|
|
|
|
WARM = """\
|
|
this path will be fetched (0.02 MiB download, 0.10 MiB unpacked):
|
|
/nix/store/aaa-hello-2.12.1
|
|
"""
|
|
|
|
PARTIAL = """\
|
|
these 2 derivations will be built:
|
|
/nix/store/bbb-foo-1.0.drv
|
|
/nix/store/ccc-bar-2.0.drv
|
|
these 3 paths will be fetched (1.0 MiB download):
|
|
/nix/store/ddd-baz-3.0
|
|
"""
|
|
|
|
GARBAGE = """\
|
|
error: some totally unrelated failure with no dry-run structure
|
|
traceback blah blah
|
|
"""
|
|
|
|
|
|
def test_warm_cache_only_fetched_is_empty_set():
|
|
# dry-run succeeded, nothing to build: distinct from a parse failure
|
|
result = graph.parse_dry_run_text(WARM, returncode=0)
|
|
assert result == set()
|
|
|
|
|
|
def test_partial_returns_only_built_drvs():
|
|
result = graph.parse_dry_run_text(PARTIAL, returncode=0)
|
|
assert result == {"bbb-foo-1.0.drv", "ccc-bar-2.0.drv"}
|
|
|
|
|
|
def test_garbage_without_header_and_failure_is_none():
|
|
assert graph.parse_dry_run_text(GARBAGE, returncode=1) is None
|
|
|
|
|
|
def test_clean_exit_without_header_is_empty_not_none():
|
|
# exit 0 but no recognizable header still counts as "nothing to build"
|
|
assert graph.parse_dry_run_text("", returncode=0) == set()
|
|
|
|
|
|
# --- structured `nix build --dry-run --json` output (issue #8) -------------
|
|
|
|
JSON_PARTIAL = """\
|
|
[
|
|
{"drvPath": "/nix/store/bbb-foo-1.0.drv", "outputs": {"out": "/nix/store/e-foo"}},
|
|
{"drvPath": "/nix/store/ccc-bar-2.0.drv", "outputs": {"out": "/nix/store/f-bar"}}
|
|
]
|
|
"""
|
|
|
|
JSON_WARM = "[]"
|
|
|
|
JSON_ALT_KEY = '[{"drv": "/nix/store/ggg-qux-9.drv"}]'
|
|
|
|
|
|
def test_json_partial_returns_built_drvs():
|
|
assert graph.parse_dry_run_json(JSON_PARTIAL) == {
|
|
"bbb-foo-1.0.drv",
|
|
"ccc-bar-2.0.drv",
|
|
}
|
|
|
|
|
|
def test_json_warm_cache_is_empty_set():
|
|
assert graph.parse_dry_run_json(JSON_WARM) == set()
|
|
|
|
|
|
def test_json_alternate_drv_key():
|
|
assert graph.parse_dry_run_json(JSON_ALT_KEY) == {"ggg-qux-9.drv"}
|
|
|
|
|
|
def test_json_non_structured_is_none():
|
|
# human text or an unexpected shape must fall back (None), not crash
|
|
assert graph.parse_dry_run_json(WARM) is None
|
|
assert graph.parse_dry_run_json('{"not": "a list"}') is None
|
|
assert graph.parse_dry_run_json("not json at all") is None
|
|
|
|
|
|
def test_build_dag_empty_set_yields_no_nodes():
|
|
closure = {"a.drv": {"inputDrvs": {}}, "b.drv": {"inputDrvs": {"a.drv": {}}}}
|
|
preds, nodes = graph.build_dag(closure, set())
|
|
assert nodes == set()
|
|
assert preds == {}
|
|
|
|
|
|
# --- upstream-substitutable filtering (issue #22) --------------------------
|
|
|
|
# legacy schema: outputs.<name>.path holds the store path
|
|
LEGACY_DRV = {
|
|
"outputs": {"out": {"path": "/nix/store/o1-foo"}},
|
|
"inputDrvs": {},
|
|
}
|
|
# versioned (schema v3) shape still carries per-output `path`
|
|
VERSIONED_DRV = {
|
|
"name": "bar",
|
|
"outputs": {
|
|
"out": {"path": "/nix/store/o2-bar"},
|
|
"dev": {"path": "/nix/store/o3-bar-dev"},
|
|
},
|
|
"inputs": {"drvs": {}, "srcs": []},
|
|
}
|
|
# content-addressed / floating output: no path -> skipped
|
|
CA_DRV = {"outputs": {"out": {"hashAlgo": "sha256"}}}
|
|
|
|
|
|
def test_output_paths_legacy_schema():
|
|
assert graph.output_paths(LEGACY_DRV) == ["/nix/store/o1-foo"]
|
|
|
|
|
|
def test_output_paths_versioned_multi_output():
|
|
assert graph.output_paths(VERSIONED_DRV) == [
|
|
"/nix/store/o2-bar",
|
|
"/nix/store/o3-bar-dev",
|
|
]
|
|
|
|
|
|
def test_output_paths_ca_without_path_is_empty():
|
|
assert graph.output_paths(CA_DRV) == []
|
|
|
|
|
|
# Real versioned schema (Nix 2.30+, schema v4) stores the output path BARE, with
|
|
# no /nix/store/ prefix — output_paths must normalize it so cache lookups work
|
|
# (issue #24).
|
|
VERSIONED_BARE_DRV = {
|
|
"name": "bar",
|
|
"outputs": {
|
|
"out": {"path": "o2-bar"},
|
|
"dev": {"path": "o3-bar-dev"},
|
|
},
|
|
"inputs": {"drvs": {}, "srcs": []},
|
|
}
|
|
|
|
|
|
def test_output_paths_versioned_bare_path_normalized():
|
|
assert graph.output_paths(VERSIONED_BARE_DRV) == [
|
|
"/nix/store/o2-bar",
|
|
"/nix/store/o3-bar-dev",
|
|
]
|
|
|
|
|
|
def test_filter_drops_bare_output_paths_via_full_path_query(monkeypatch):
|
|
# End-to-end: a drv whose outputs are bare must still be matched against a
|
|
# cache oracle that keys by full /nix/store/ path (issue #24).
|
|
drv = {"outputs": {"out": {"path": "aaa-cached"}}, "inputDrvs": {}}
|
|
closure = {"aaa-cached.drv": drv}
|
|
monkeypatch.setattr(
|
|
graph,
|
|
"substitutable_paths",
|
|
lambda paths, cache_url=None, **k: {"/nix/store/aaa-cached"},
|
|
)
|
|
reduced, changed = graph.filter_upstream_substitutable(closure, None)
|
|
assert changed is True
|
|
assert reduced == set()
|
|
|
|
|
|
def test_filter_drops_fully_substitutable_keeps_partial_and_none():
|
|
closure = {
|
|
"foo.drv": LEGACY_DRV, # single out, will be cached -> dropped
|
|
"bar.drv": VERSIONED_DRV, # two outs, only one cached -> kept
|
|
"baz.drv": {"outputs": {"out": {"path": "/nix/store/o4-baz"}}}, # kept
|
|
}
|
|
|
|
def fake_query(paths, cache_url):
|
|
# exercises the map-to-output-path logic: caller must have collected
|
|
# exactly the outputs of the candidate drvs
|
|
assert "/nix/store/o1-foo" in paths
|
|
assert "/nix/store/o2-bar" in paths and "/nix/store/o3-bar-dev" in paths
|
|
return {"/nix/store/o1-foo", "/nix/store/o2-bar"}
|
|
|
|
reduced, changed = graph.filter_upstream_substitutable(
|
|
closure, None, _query=fake_query
|
|
)
|
|
assert reduced == {"bar.drv", "baz.drv"}
|
|
assert changed is True
|
|
|
|
|
|
def test_filter_respects_to_build_set(monkeypatch):
|
|
closure = {
|
|
"foo.drv": LEGACY_DRV,
|
|
"bar.drv": VERSIONED_DRV,
|
|
"baz.drv": {"outputs": {"out": {"path": "/nix/store/o4-baz"}}},
|
|
}
|
|
monkeypatch.setattr(
|
|
graph, "substitutable_paths", lambda paths, cache_url: {"/nix/store/o1-foo"}
|
|
)
|
|
# only foo,bar considered; foo fully cached -> dropped, bar kept
|
|
reduced, changed = graph.filter_upstream_substitutable(
|
|
closure, {"foo.drv", "bar.drv"}
|
|
)
|
|
assert reduced == {"bar.drv"}
|
|
assert changed is True
|
|
|
|
|
|
def test_filter_no_op_when_cache_knows_nothing():
|
|
closure = {"foo.drv": LEGACY_DRV, "bar.drv": VERSIONED_DRV}
|
|
reduced, changed = graph.filter_upstream_substitutable(
|
|
closure, None, _query=lambda paths, cache_url: set()
|
|
)
|
|
assert reduced == {"foo.drv", "bar.drv"}
|
|
assert changed is False
|
|
|
|
|
|
def test_parse_path_info_object_and_list_shapes():
|
|
obj = '{"/nix/store/o1-foo": {"narSize": 1}, "/nix/store/o2-bar": null}'
|
|
assert graph._parse_path_info(obj, ["/nix/store/o1-foo", "/nix/store/o2-bar"]) == {
|
|
"/nix/store/o1-foo"
|
|
}
|
|
lst = '[{"path": "/nix/store/o1-foo", "valid": true}]'
|
|
assert graph._parse_path_info(lst, ["/nix/store/o1-foo"]) == {"/nix/store/o1-foo"}
|
|
assert graph._parse_path_info("not json", ["/nix/store/o1-foo"]) == set()
|
|
|
|
|
|
def test_build_dag_none_costs_whole_closure():
|
|
closure = {
|
|
"a.drv": {"inputDrvs": {}},
|
|
"b.drv": {"inputDrvs": {"/nix/store/x-a.drv": {}}},
|
|
}
|
|
_, nodes = graph.build_dag(closure, None)
|
|
assert nodes == {"a.drv", "b.drv"}
|