From 3891bd909a23371c3cad93d041260e11b50de1e5 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Thu, 27 Aug 2026 02:39:52 +0000 Subject: [PATCH] test(xceadmin): add env-var override for live E2E binary discovery _live_e2e_common.py's CONTROLLER_CANDIDATES/XCEADMIN_CANDIDATES only ever resolved to the three default profile-dir names (xce.build, build.release, build.dbg), with no way to point the live E2E suite at a binary built to a custom/explicit build dir. Adds XCE_CONTROLLER_BIN/XCEADMIN_BIN env-var overrides via a new resolve_binary_candidates() helper -- when set, the override becomes the sole candidate; otherwise the existing three-name fallback list is unchanged. Mirrors the ${XCE_CONTROLLER_BIN:-default} convention already used by clean_failover_preservation.sh/run-ha-soak.sh. Skip messages in both live E2E test files now mention the override. TDD: 4 new unit tests in test_live_e2e_common.py (no override, empty override, override-becomes-sole-candidate, and an end-to-end reload proving the real module constants respect their env vars). Full xceadmin suite (288 tests) confirmed unaffected: 287 pass, 1 pre-existing unrelated failure (a stale redirect-gap assertion, unrelated to this change). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg --- src/xceadmin/tests/_live_e2e_common.py | 49 ++++++++++--- src/xceadmin/tests/test_e2e_live_ha_push.py | 15 +++- src/xceadmin/tests/test_e2e_live_push.py | 9 ++- src/xceadmin/tests/test_live_e2e_common.py | 76 +++++++++++++++++++++ 4 files changed, 133 insertions(+), 16 deletions(-) create mode 100644 src/xceadmin/tests/test_live_e2e_common.py diff --git a/src/xceadmin/tests/_live_e2e_common.py b/src/xceadmin/tests/_live_e2e_common.py index fa402405..c6408393 100644 --- a/src/xceadmin/tests/_live_e2e_common.py +++ b/src/xceadmin/tests/_live_e2e_common.py @@ -27,6 +27,7 @@ it -- the same mechanism that already lets every test file here import from __future__ import annotations +import os import re import socket import time @@ -36,19 +37,45 @@ SRC_DIR = Path(__file__).resolve().parents[2] REPO_CONFIGS_DIR = SRC_DIR / "configs" DEPLOY_HA_SH = SRC_DIR / "tools" / "deploy-ha.sh" + +def resolve_binary_candidates(env_var: str, defaults: list[Path]) -> list[Path]: + """If ``env_var`` is set to a non-empty value, it becomes the SOLE + candidate -- lets a caller point discovery at a binary built to a + custom/explicit build dir (``xce-build.sh``'s "legacy explicit + build_dir" override), which none of the fixed default profile-dir names + below can ever match. Otherwise falls back to ``defaults`` unchanged. + + Mirrors the ``${XCE_CONTROLLER_BIN:-default}``-style env-var-override + convention already used by ``clean_failover_preservation.sh``/ + ``run-ha-soak.sh`` elsewhere in this repo, rather than a Python-only + ad hoc mechanism. + """ + override = os.environ.get(env_var) + if override: + return [Path(override)] + return defaults + + # Mirrors deploy-ha.sh's own xceadmin/controller discovery order # (src/tools/deploy-ha.sh) -- xce.build (asan, the default profile) first, -# then release, then debug. -CONTROLLER_CANDIDATES = [ - SRC_DIR / "xce.build" / "controller" / "xce_controller", - SRC_DIR / "build.release" / "controller" / "xce_controller", - SRC_DIR / "build.dbg" / "controller" / "xce_controller", -] -XCEADMIN_CANDIDATES = [ - SRC_DIR / "xce.build" / "xceadmin" / ".venv" / "bin" / "xceadmin", - SRC_DIR / "build.release" / "xceadmin" / ".venv" / "bin" / "xceadmin", - SRC_DIR / "build.dbg" / "xceadmin" / ".venv" / "bin" / "xceadmin", -] +# then release, then debug. XCE_CONTROLLER_BIN/XCEADMIN_BIN override this +# entirely when set (e.g. a build to a custom/explicit build dir). +CONTROLLER_CANDIDATES = resolve_binary_candidates( + "XCE_CONTROLLER_BIN", + [ + SRC_DIR / "xce.build" / "controller" / "xce_controller", + SRC_DIR / "build.release" / "controller" / "xce_controller", + SRC_DIR / "build.dbg" / "controller" / "xce_controller", + ], +) +XCEADMIN_CANDIDATES = resolve_binary_candidates( + "XCEADMIN_BIN", + [ + SRC_DIR / "xce.build" / "xceadmin" / ".venv" / "bin" / "xceadmin", + SRC_DIR / "build.release" / "xceadmin" / ".venv" / "bin" / "xceadmin", + SRC_DIR / "build.dbg" / "xceadmin" / ".venv" / "bin" / "xceadmin", + ], +) READY_TIMEOUT_S = 20.0 VERSION_TAG_RE = re.compile(r"versionTag=(\d+)\s+ackedCount=(\d+)") diff --git a/src/xceadmin/tests/test_e2e_live_ha_push.py b/src/xceadmin/tests/test_e2e_live_ha_push.py index 29bed699..078f32cb 100644 --- a/src/xceadmin/tests/test_e2e_live_ha_push.py +++ b/src/xceadmin/tests/test_e2e_live_ha_push.py @@ -296,7 +296,8 @@ def live_ha_cluster(tmp_path: Path): if controller_bin is None: pytest.skip( "no xce_controller binary found under xce.build/build.release/" - "build.dbg -- run xce-build.sh controller [profile] first to " + "build.dbg -- run xce-build.sh controller [profile] first, or " + "set XCE_CONTROLLER_BIN to a custom build's binary, to " "exercise this live E2E test" ) if shutil.which("openssl") is None: @@ -433,7 +434,11 @@ def test_live_config_push_against_primary_reaches_success_path( """ xceadmin_bin = find_first_executable(XCEADMIN_CANDIDATES) if xceadmin_bin is None: - pytest.skip("no xceadmin binary found under xce.build/build.release/build.dbg") + pytest.skip( + "no xceadmin binary found under xce.build/build.release/" + "build.dbg -- set XCEADMIN_BIN to a custom build's binary " + "if using a non-default build dir" + ) cluster_bin = _build_cluster_bin(xceadmin_bin, tmp_path) @@ -519,7 +524,11 @@ def test_live_config_push_against_standby_returns_standby_rejection_not_redirect """ xceadmin_bin = find_first_executable(XCEADMIN_CANDIDATES) if xceadmin_bin is None: - pytest.skip("no xceadmin binary found under xce.build/build.release/build.dbg") + pytest.skip( + "no xceadmin binary found under xce.build/build.release/" + "build.dbg -- set XCEADMIN_BIN to a custom build's binary " + "if using a non-default build dir" + ) cluster_bin = _build_cluster_bin(xceadmin_bin, tmp_path) diff --git a/src/xceadmin/tests/test_e2e_live_push.py b/src/xceadmin/tests/test_e2e_live_push.py index a7022be9..efbe0b9c 100644 --- a/src/xceadmin/tests/test_e2e_live_push.py +++ b/src/xceadmin/tests/test_e2e_live_push.py @@ -93,7 +93,8 @@ def live_controller(tmp_path: Path): if controller_bin is None: pytest.skip( "no xce_controller binary found under xce.build/build.release/" - "build.dbg -- run xce-build.sh controller [profile] first to " + "build.dbg -- run xce-build.sh controller [profile] first, or " + "set XCE_CONTROLLER_BIN to a custom build's binary, to " "exercise this live E2E test" ) if shutil.which("openssl") is None: @@ -154,7 +155,11 @@ def test_live_config_push_against_real_single_node_controller(live_controller, t controller_addr, log_file = live_controller xceadmin_bin = _find_first_executable(_XCEADMIN_CANDIDATES) if xceadmin_bin is None: - pytest.skip("no xceadmin binary found under xce.build/build.release/build.dbg") + pytest.skip( + "no xceadmin binary found under xce.build/build.release/" + "build.dbg -- set XCEADMIN_BIN to a custom build's binary " + "if using a non-default build dir" + ) cluster_bin = tmp_path / "cluster.bin" build_result = subprocess.run( diff --git a/src/xceadmin/tests/test_live_e2e_common.py b/src/xceadmin/tests/test_live_e2e_common.py new file mode 100644 index 00000000..20c1a9dd --- /dev/null +++ b/src/xceadmin/tests/test_live_e2e_common.py @@ -0,0 +1,76 @@ +"""Unit tests for ``_live_e2e_common``'s binary-discovery env-var override. + +``CONTROLLER_CANDIDATES``/``XCEADMIN_CANDIDATES`` previously only ever +resolved to the three default profile-dir names (``xce.build``, +``build.release``, ``build.dbg``) -- no way to point the live E2E suite at +a binary built to a custom/explicit build dir (``xce-build.sh``'s +"legacy explicit build_dir" override) without editing this module directly. +This adds ``XCE_CONTROLLER_BIN``/``XCEADMIN_BIN`` env-var overrides, +consistent with the ``${XCE_CONTROLLER_BIN:-default}``-style pattern +already used by ``clean_failover_preservation.sh``/``run-ha-soak.sh``. + +Tests the pure resolution helper directly (not the frozen module-level +``CONTROLLER_CANDIDATES``/``XCEADMIN_CANDIDATES`` constants, which are +computed once at import time from whatever the environment happened to be +-- re-importing the module under a patched environment is fragile/awkward +under pytest's module caching, so the override LOGIC is factored into a +directly-testable function instead). +""" + +from __future__ import annotations + +from pathlib import Path + +from _live_e2e_common import resolve_binary_candidates + + +def test_no_env_override_returns_default_candidates_unchanged(monkeypatch): + monkeypatch.delenv("XCE_SOME_TEST_BIN", raising=False) + defaults = [Path("/a/xce.build/x"), Path("/a/build.release/x")] + + result = resolve_binary_candidates("XCE_SOME_TEST_BIN", defaults) + + assert result == defaults + + +def test_empty_env_override_falls_back_to_default_candidates(monkeypatch): + monkeypatch.setenv("XCE_SOME_TEST_BIN", "") + defaults = [Path("/a/xce.build/x")] + + result = resolve_binary_candidates("XCE_SOME_TEST_BIN", defaults) + + assert result == defaults + + +def test_env_override_becomes_sole_candidate(monkeypatch, tmp_path): + custom_bin = tmp_path / "my-custom-build" / "controller" / "xce_controller" + monkeypatch.setenv("XCE_SOME_TEST_BIN", str(custom_bin)) + defaults = [Path("/a/xce.build/x"), Path("/a/build.dbg/x")] + + result = resolve_binary_candidates("XCE_SOME_TEST_BIN", defaults) + + assert result == [custom_bin] + + +def test_controller_and_xceadmin_candidates_respect_their_own_env_vars(monkeypatch, tmp_path): + """End-to-end through the real module constants' construction: reload + the module with each override env var set and confirm the exported + candidate lists reduce to the single overridden path, proving the + actual production wiring (not just the helper in isolation) is correct. + """ + import importlib + + import _live_e2e_common as mod + + custom_controller = tmp_path / "custom" / "xce_controller" + custom_xceadmin = tmp_path / "custom" / "xceadmin" + monkeypatch.setenv("XCE_CONTROLLER_BIN", str(custom_controller)) + monkeypatch.setenv("XCEADMIN_BIN", str(custom_xceadmin)) + try: + importlib.reload(mod) + assert mod.CONTROLLER_CANDIDATES == [custom_controller] + assert mod.XCEADMIN_CANDIDATES == [custom_xceadmin] + finally: + monkeypatch.delenv("XCE_CONTROLLER_BIN", raising=False) + monkeypatch.delenv("XCEADMIN_BIN", raising=False) + importlib.reload(mod) # restore default candidates for any test after this one