"""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..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"}