XCE_RESF_BUILTIN (0x01) 在生产链路上从未被任何一层置位:全树只有 types.h 定义、scheduler repl_info.c 显示表、一个测试 fixture 三处引用。worker init_builtin_load_indices()(mem/io/r1m…)、controller add_hostattrs_resvals()/ set_host_slots() 硬编码生产这些资源的值,但定义的 flags 从 YAML→capnp→wire 全程没人打这个位——CLI 的 BUILTIN 列对 mem/io/slots 等恒显 false,与 extlic 等用户资源无法区分。 - resources.schema.json(+xceadmin 打包副本,构建时从 src/configs/schema 同步):加 builtin: boolean 键 - mapping.py:RESOURCE_FLAG_BUILTIN=0x01,_build_resource 映射 builtin:true (与 consumable/release 同模式:缺省/false 不置位) - build.py dump:补渲染 builtin=True;顺手补同样一直缺失的 external=True (此前 EXTERNAL 池 dump 只显示 shared=true) - src/configs/resources.yaml:20 个硬编码生产者资源(slots + 11 load index + 8 host attributes)声明 builtin: true - golden fixture full/expected.bin 相应再生(6696→6856B,diff 恰为 20 行 builtin=True,非 builtin 行 0 差异) 验证:xceadmin 389 测试全绿(dev venv + meson build.dbg 各一遍);t1 实证: 树内 venv 重建 cluster.bin(与 live 逐行比对确认 delta 仅 builtin 20 行)→ config push gen=17 → apiserver 重启拾取(定义 store 只吃 join 时 config push,激活只 re-push scheduler)→ xcecli definition list -l mem/io/slots BUILTIN=true,extlic false、distribution/extlic 值/liccheck 作业 RUN→DONE 全链正常,5 daemon 健康。 Co-Authored-By: Claude Code <noreply@anthropic.com>
154 lines
5.9 KiB
Python
154 lines
5.9 KiB
Python
"""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))
|