"""Tests for ``xceadmin.build.dump_cluster_config`` -- the reverse of ``build_cluster_config`` (decode a ``cluster.bin`` back to human-readable text). Uses the same ``tests/fixtures/full`` real-config fixture as test_config_build.py rather than a second golden text fixture: the assertions below check for specific substrings (mirrors test_scheduler.py's ``dump()`` test style), not a byte-exact text match, so formatting can evolve without a brittle golden file. """ from __future__ import annotations from pathlib import Path import pytest import yaml from xcecommon.errors import ClusterBinFileError from xceadmin import build FIXTURES_DIR = Path(__file__).parent / "fixtures" @pytest.fixture def full_cluster_bin(tmp_path): data = build.build_cluster_config(str(FIXTURES_DIR / "full" / "configs")) path = tmp_path / "cluster.bin" path.write_bytes(data) return path # --------------------------------------------------------------------------- # Section content # --------------------------------------------------------------------------- def test_dump_lists_cluster_scalars(full_cluster_bin): text = build.dump_cluster_config(str(full_cluster_bin)) assert "=== ClusterConfig ===" in text assert "clusterName:" in text assert "heartbeatInterval:" in text def test_dump_lists_each_resource_with_decoded_type_and_order(full_cluster_bin): text = build.dump_cluster_config(str(full_cluster_bin)) assert "=== resources (" in text # "mem" is numeric/decrease/not consumable -- valtype/ordtype/flags int # codes must render back to their YAML names, not raw ints. assert "name=mem type=numeric order=decrease" in text assert "consumable=False" in text def test_dump_renders_builtin_and_external_flags(full_cluster_bin): """The flag bits that change a resource's semantics must be visible in a dump: builtin (worker/controller hardcode the values -- the repo fixture's load indices and host attributes) and external (values come only from the extres daemon's pushes -- the extlic pool shape). Both were silently invisible before: every builtin resource dumped as a plain user resource, and an external pool dumped as bare shared=true. """ text = build.dump_cluster_config(str(full_cluster_bin)) mem_line = next(l for l in text.splitlines() if "name=mem type=numeric" in l) assert "builtin=True" in mem_line # a user-declared resource must not show a bare "builtin=" at all assert "builtin=False" not in text import yaml as _yaml configs = str(FIXTURES_DIR / "full" / "configs") with open(configs + "/resources.yaml", encoding="utf-8") as fh: names = [r["name"] for r in _yaml.safe_load(fh)["resources"] if (r.get("resourcemap") or []) and (r["resourcemap"][0].get("location") or []) == ["external"]] assert names, "fixture must contain an external resource for this test" for name in names: line = next(l for l in text.splitlines() if f"name={name} " in l) assert "external=True" in line, line def test_dump_renders_mxj_auto_detect_sentinel_not_raw_uint32(full_cluster_bin): """The exact bug that motivated this feature: hosts.yaml's `default` host has `max_slot: -1`, which wire-encodes as HOSTCONFIG_MXJ_AUTO_DETECT (0xFFFFFFFF). The dump must show the human meaning, not the raw UInt32. """ text = build.dump_cluster_config(str(full_cluster_bin)) assert "=== host: default ===" in text assert "max_slot=-1 (auto-detect from CPU count)" in text # worker2 has an explicit max_slot: 256 -- shown as a plain number. assert "=== host: worker2 ===" in text assert "max_slot=256" in text def test_dump_lists_queues_groups_scalers_and_limits(full_cluster_bin): text = build.dump_cluster_config(str(full_cluster_bin)) assert "=== queues (" in text assert "=== ugroups (" in text assert "=== hgroups (" in text assert "=== hproviders (" in text assert "=== limits (" in text assert "=== limit: ug1_slots ===" in text def test_dump_output_ends_with_trailing_newline(full_cluster_bin): text = build.dump_cluster_config(str(full_cluster_bin)) assert text.endswith("\n") assert not text.endswith("\n\n") def test_dump_humanizes_byte_valued_threshold(tmp_path): """A host's sched_stop threshold on a byte-valued resource (maxmem) is shown as a human-readable size, not a raw float byte count -- mirrors the mem/tmp/maxswap/maxtmp/maxmem unit-consistency work this session. """ (tmp_path / "hosts.yaml").write_text( yaml.safe_dump( { "hosts": [ { "hostname": "h1", "sched_stop": [{"resname": "maxmem", "sched": "500MB", "stop": "1GB"}], } ] } ) ) data = build.build_cluster_config(str(tmp_path)) bin_path = tmp_path / "cluster.bin" bin_path.write_bytes(data) text = build.dump_cluster_config(str(bin_path)) assert "threshold: name=maxmem sched=500.00 MB stop=1.00 GB" in text # --------------------------------------------------------------------------- # Error handling # --------------------------------------------------------------------------- def test_dump_missing_file_raises_cluster_bin_file_error(tmp_path): missing = tmp_path / "does-not-exist.bin" with pytest.raises(ClusterBinFileError): build.dump_cluster_config(str(missing)) def test_dump_empty_file_raises_cluster_bin_file_error(tmp_path): empty = tmp_path / "empty.bin" empty.write_bytes(b"") with pytest.raises(ClusterBinFileError): build.dump_cluster_config(str(empty)) def test_dump_garbage_bytes_raises_cluster_bin_file_error_not_crash(tmp_path): garbage = tmp_path / "garbage.bin" garbage.write_bytes(b"not a cluster config, just noise") with pytest.raises(ClusterBinFileError): build.dump_cluster_config(str(garbage))