test(issue-28): make the host-dependent tests portable; POSIX ps in the process scanners
The 33 failures on the Linux / Python 3.14 host (#28) were environment dependencies, not regressions; each test is worth keeping, so fix the dependency instead of deleting the test: - test_workspace_scope: `source` under `shell=True` ran in dash; run the command string under bash. - conftest autouse: MAM_ENV_FILE points at an absent file for every test, so a workspace .mam.env with a reachable broker no longer breaks the "broker unavailable" tests (T1-4). - help-capture guard: compare normalized help text (whitespace collapsed, "optional arguments"/"options" unified) so argparse re-wrapping across Python versions is not drift. - deploy freshness D-11..D-19 and D-22..D-30: skip with a reason when the nats-docker submodule is not initialized. - lib_py.agents.base.conversation_in_use and heartbeat._process_table: `ps -eo pid=,ppid=,args=` (POSIX) instead of the BSD `-axo ... command=` form, the most likely cause of the 12 purge failures on Linux (the holder scan returned nothing); to be confirmed on that host. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
1 parent
46ea4cc1e4
commit
7a94ba2c3e
6 files changed
+48
-4
No files matched your search
@@ -47,7 +47,7 @@ def conversation_in_use(uuid: str, paths=()) -> bool:
|
|||||||
if os.environ.get(var, "").strip() == uuid:
|
if os.environ.get(var, "").strip() == uuid:
|
||||||
return True
|
return True
|
||||||
try:
|
try:
|
||||||
out = subprocess.run(["ps", "-axo", "pid=,ppid=,command="],
|
out = subprocess.run(["ps", "-eo", "pid=,ppid=,args="],
|
||||||
capture_output=True, text=True, timeout=10).stdout
|
capture_output=True, text=True, timeout=10).stdout
|
||||||
except Exception:
|
except Exception:
|
||||||
out = ""
|
out = ""
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ def _is_agent_cli(command: str) -> bool:
|
|||||||
def _process_table():
|
def _process_table():
|
||||||
"""{pid: (ppid, command)} from ps; empty when ps is unavailable."""
|
"""{pid: (ppid, command)} from ps; empty when ps is unavailable."""
|
||||||
try:
|
try:
|
||||||
out = subprocess.run(["ps", "-axo", "pid=,ppid=,command="], capture_output=True, text=True).stdout
|
out = subprocess.run(["ps", "-eo", "pid=,ppid=,args="], capture_output=True, text=True).stdout
|
||||||
except OSError:
|
except OSError:
|
||||||
return {}
|
return {}
|
||||||
table = {}
|
table = {}
|
||||||
|
|||||||
@@ -170,6 +170,11 @@ def _isolate_mam_state_dirs(_ambient_control_plane, tmp_path, monkeypatch):
|
|||||||
state = tmp_path / ".mam-test-state"
|
state = tmp_path / ".mam-test-state"
|
||||||
monkeypatch.setenv("MAM_JOBS_DIR", str(state / "jobs"))
|
monkeypatch.setenv("MAM_JOBS_DIR", str(state / "jobs"))
|
||||||
monkeypatch.setenv("DELEGATE_JOB_LOGS_DIR", str(state / "delegate_job_logs"))
|
monkeypatch.setenv("DELEGATE_JOB_LOGS_DIR", str(state / "delegate_job_logs"))
|
||||||
|
# Issue #28: a workspace whose real .mam.env names a reachable broker made
|
||||||
|
# "broker unavailable" tests (T1-4) read it through the wrapper's env loader.
|
||||||
|
# Point every test at an absent env file; a test that wants a real broker
|
||||||
|
# config sets MAM_ENV_FILE itself.
|
||||||
|
monkeypatch.setenv("MAM_ENV_FILE", str(state / ".mam.env.absent"))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -279,12 +279,19 @@ def _resolve_private_server_doc() -> str:
|
|||||||
|
|
||||||
|
|
||||||
PRIVATE_SERVER_DOC_PATH = _resolve_private_server_doc()
|
PRIVATE_SERVER_DOC_PATH = _resolve_private_server_doc()
|
||||||
|
# Issue #28: these guards read the nats-docker submodule. A checkout without
|
||||||
|
# `git submodule update --init` has nothing to check; skip with the reason
|
||||||
|
# instead of failing on every host that cloned without submodules.
|
||||||
|
needs_private_server_doc = pytest.mark.skipif(
|
||||||
|
not os.path.exists(PRIVATE_SERVER_DOC_PATH),
|
||||||
|
reason="nats-docker submodule not initialized (git submodule update --init)")
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# D-11 — (G-D1) PRIVATE_SERVER.md must only document MQTT_* environment
|
# D-11 — (G-D1) PRIVATE_SERVER.md must only document MQTT_* environment
|
||||||
# variables that broker_config_from_env() actually parses.
|
# variables that broker_config_from_env() actually parses.
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@needs_private_server_doc
|
||||||
def test_d11_private_server_env_names_valid():
|
def test_d11_private_server_env_names_valid():
|
||||||
sys.path.insert(0, os.path.join(REPO_ROOT, ".agents", "skills", "multi-agent-mux-delegate-job", "scripts"))
|
sys.path.insert(0, os.path.join(REPO_ROOT, ".agents", "skills", "multi-agent-mux-delegate-job", "scripts"))
|
||||||
import mqtt_common
|
import mqtt_common
|
||||||
@@ -315,6 +322,7 @@ def test_d11_private_server_env_names_valid():
|
|||||||
# D-12 — (G-D2) PRIVATE_SERVER.md must not contain invalid MAM_MQTT_* in
|
# D-12 — (G-D2) PRIVATE_SERVER.md must not contain invalid MAM_MQTT_* in
|
||||||
# active configuration code blocks.
|
# active configuration code blocks.
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@needs_private_server_doc
|
||||||
def test_d12_private_server_no_mam_mqtt_in_code_fences():
|
def test_d12_private_server_no_mam_mqtt_in_code_fences():
|
||||||
doc_path = PRIVATE_SERVER_DOC_PATH
|
doc_path = PRIVATE_SERVER_DOC_PATH
|
||||||
with open(doc_path, "r", encoding="utf-8") as f:
|
with open(doc_path, "r", encoding="utf-8") as f:
|
||||||
@@ -331,6 +339,7 @@ def test_d12_private_server_no_mam_mqtt_in_code_fences():
|
|||||||
# D-13 — (G-D3) nats-server launch instructions in PRIVATE_SERVER.md must
|
# D-13 — (G-D3) nats-server launch instructions in PRIVATE_SERVER.md must
|
||||||
# use valid config blocks (mqtt {) and not HTTP port flag (-m 1883).
|
# use valid config blocks (mqtt {) and not HTTP port flag (-m 1883).
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@needs_private_server_doc
|
||||||
def test_d13_private_server_nats_config_valid():
|
def test_d13_private_server_nats_config_valid():
|
||||||
doc_path = PRIVATE_SERVER_DOC_PATH
|
doc_path = PRIVATE_SERVER_DOC_PATH
|
||||||
with open(doc_path, "r", encoding="utf-8") as f:
|
with open(doc_path, "r", encoding="utf-8") as f:
|
||||||
@@ -347,6 +356,7 @@ def test_d13_private_server_nats_config_valid():
|
|||||||
# D-14 — (G-D4) Verification commands in PRIVATE_SERVER.md must use valid
|
# D-14 — (G-D4) Verification commands in PRIVATE_SERVER.md must use valid
|
||||||
# CLI flags matching the actual scripts' argparse parsers.
|
# CLI flags matching the actual scripts' argparse parsers.
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@needs_private_server_doc
|
||||||
def test_d14_private_server_cli_args_valid():
|
def test_d14_private_server_cli_args_valid():
|
||||||
doc_path = PRIVATE_SERVER_DOC_PATH
|
doc_path = PRIVATE_SERVER_DOC_PATH
|
||||||
with open(doc_path, "r", encoding="utf-8") as f:
|
with open(doc_path, "r", encoding="utf-8") as f:
|
||||||
@@ -367,6 +377,7 @@ def test_d14_private_server_cli_args_valid():
|
|||||||
# D-15 — (G-D5) store_dir in code blocks must be absolute path (/ or $HOME)
|
# D-15 — (G-D5) store_dir in code blocks must be absolute path (/ or $HOME)
|
||||||
# and heredocs writing it must be unquoted (<<EOF).
|
# and heredocs writing it must be unquoted (<<EOF).
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@needs_private_server_doc
|
||||||
def test_d15_private_server_store_dir_valid():
|
def test_d15_private_server_store_dir_valid():
|
||||||
doc_path = PRIVATE_SERVER_DOC_PATH
|
doc_path = PRIVATE_SERVER_DOC_PATH
|
||||||
with open(doc_path, "r", encoding="utf-8") as f:
|
with open(doc_path, "r", encoding="utf-8") as f:
|
||||||
@@ -388,6 +399,7 @@ def test_d15_private_server_store_dir_valid():
|
|||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# D-16 — (G-D6) nats image references in code fences must use pinned alpine
|
# D-16 — (G-D6) nats image references in code fences must use pinned alpine
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@needs_private_server_doc
|
||||||
def test_d16_private_server_nats_image_alpine_pinned():
|
def test_d16_private_server_nats_image_alpine_pinned():
|
||||||
doc_path = PRIVATE_SERVER_DOC_PATH
|
doc_path = PRIVATE_SERVER_DOC_PATH
|
||||||
with open(doc_path, "r", encoding="utf-8") as f:
|
with open(doc_path, "r", encoding="utf-8") as f:
|
||||||
@@ -404,6 +416,7 @@ def test_d16_private_server_nats_image_alpine_pinned():
|
|||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# D-17 — (G-D7) Port 8222 in docker examples must be bound to 127.0.0.1
|
# D-17 — (G-D7) Port 8222 in docker examples must be bound to 127.0.0.1
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@needs_private_server_doc
|
||||||
def test_d17_private_server_monitoring_port_localhost_bound():
|
def test_d17_private_server_monitoring_port_localhost_bound():
|
||||||
doc_path = PRIVATE_SERVER_DOC_PATH
|
doc_path = PRIVATE_SERVER_DOC_PATH
|
||||||
with open(doc_path, "r", encoding="utf-8") as f:
|
with open(doc_path, "r", encoding="utf-8") as f:
|
||||||
@@ -421,6 +434,7 @@ def test_d17_private_server_monitoring_port_localhost_bound():
|
|||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# D-18 — (G-D8) TLS examples must not use IP literals for MQTT_BROKER
|
# D-18 — (G-D8) TLS examples must not use IP literals for MQTT_BROKER
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@needs_private_server_doc
|
||||||
def test_d18_private_server_tls_examples_use_domain_names():
|
def test_d18_private_server_tls_examples_use_domain_names():
|
||||||
doc_path = PRIVATE_SERVER_DOC_PATH
|
doc_path = PRIVATE_SERVER_DOC_PATH
|
||||||
with open(doc_path, "r", encoding="utf-8") as f:
|
with open(doc_path, "r", encoding="utf-8") as f:
|
||||||
@@ -437,6 +451,7 @@ def test_d18_private_server_tls_examples_use_domain_names():
|
|||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# D-19 — (G-D9) Subject literals in config examples match DEFAULT_TOPIC_ROOT
|
# D-19 — (G-D9) Subject literals in config examples match DEFAULT_TOPIC_ROOT
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@needs_private_server_doc
|
||||||
def test_d19_private_server_subject_literals_match_default_topic_root():
|
def test_d19_private_server_subject_literals_match_default_topic_root():
|
||||||
doc_path = PRIVATE_SERVER_DOC_PATH
|
doc_path = PRIVATE_SERVER_DOC_PATH
|
||||||
with open(doc_path, "r", encoding="utf-8") as f:
|
with open(doc_path, "r", encoding="utf-8") as f:
|
||||||
@@ -492,6 +507,9 @@ def _resolve_docker_dir() -> str:
|
|||||||
|
|
||||||
DOCKER_DIR = _resolve_docker_dir()
|
DOCKER_DIR = _resolve_docker_dir()
|
||||||
COMPOSE_PATH = os.path.join(DOCKER_DIR, "docker-compose.yaml")
|
COMPOSE_PATH = os.path.join(DOCKER_DIR, "docker-compose.yaml")
|
||||||
|
needs_docker_assets = pytest.mark.skipif(
|
||||||
|
not os.path.exists(COMPOSE_PATH),
|
||||||
|
reason="nats-docker submodule not initialized (git submodule update --init)")
|
||||||
NATS_CONF_PATH = os.path.join(DOCKER_DIR, "nats.conf")
|
NATS_CONF_PATH = os.path.join(DOCKER_DIR, "nats.conf")
|
||||||
ENV_EXAMPLE_PATH = os.path.join(DOCKER_DIR, ".env.example")
|
ENV_EXAMPLE_PATH = os.path.join(DOCKER_DIR, ".env.example")
|
||||||
DOCKER_README_PATH = os.path.join(DOCKER_DIR, "README.md")
|
DOCKER_README_PATH = os.path.join(DOCKER_DIR, "README.md")
|
||||||
@@ -523,6 +541,7 @@ def _active_conf_lines():
|
|||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# D-22 — docker/ assets exist and are populated
|
# D-22 — docker/ assets exist and are populated
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@needs_docker_assets
|
||||||
def test_d22_docker_assets_exist_and_are_populated():
|
def test_d22_docker_assets_exist_and_are_populated():
|
||||||
for p in [COMPOSE_PATH, NATS_CONF_PATH, ENV_EXAMPLE_PATH, DOCKER_README_PATH]:
|
for p in [COMPOSE_PATH, NATS_CONF_PATH, ENV_EXAMPLE_PATH, DOCKER_README_PATH]:
|
||||||
assert os.path.exists(p), f"Required asset {p} does not exist"
|
assert os.path.exists(p), f"Required asset {p} does not exist"
|
||||||
@@ -533,6 +552,7 @@ def test_d22_docker_assets_exist_and_are_populated():
|
|||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# D-23 — compose image matches doc and is alpine
|
# D-23 — compose image matches doc and is alpine
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@needs_docker_assets
|
||||||
def test_d23_compose_image_matches_doc_and_is_alpine():
|
def test_d23_compose_image_matches_doc_and_is_alpine():
|
||||||
_, svc = _load_compose()
|
_, svc = _load_compose()
|
||||||
compose_img = svc.get("image", "")
|
compose_img = svc.get("image", "")
|
||||||
@@ -552,6 +572,7 @@ def test_d23_compose_image_matches_doc_and_is_alpine():
|
|||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# D-24 — compose port exposure contract
|
# D-24 — compose port exposure contract
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@needs_docker_assets
|
||||||
def test_d24_compose_port_exposure_contract():
|
def test_d24_compose_port_exposure_contract():
|
||||||
_, svc = _load_compose()
|
_, svc = _load_compose()
|
||||||
ports = svc.get("ports", [])
|
ports = svc.get("ports", [])
|
||||||
@@ -573,6 +594,7 @@ def test_d24_compose_port_exposure_contract():
|
|||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# D-25 — secrets are fail closed
|
# D-25 — secrets are fail closed
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@needs_docker_assets
|
||||||
def test_d25_secrets_are_fail_closed():
|
def test_d25_secrets_are_fail_closed():
|
||||||
_, svc = _load_compose()
|
_, svc = _load_compose()
|
||||||
env = svc.get("environment", {})
|
env = svc.get("environment", {})
|
||||||
@@ -615,6 +637,7 @@ def test_d25_secrets_are_fail_closed():
|
|||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# D-26 — nats_conf jetstream and mqtt contract
|
# D-26 — nats_conf jetstream and mqtt contract
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@needs_docker_assets
|
||||||
def test_d26_nats_conf_jetstream_and_mqtt_contract():
|
def test_d26_nats_conf_jetstream_and_mqtt_contract():
|
||||||
_, svc = _load_compose()
|
_, svc = _load_compose()
|
||||||
volumes = svc.get("volumes", [])
|
volumes = svc.get("volumes", [])
|
||||||
@@ -645,6 +668,7 @@ def test_d26_nats_conf_jetstream_and_mqtt_contract():
|
|||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# D-27 — observer permissions match topic root
|
# D-27 — observer permissions match topic root
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@needs_docker_assets
|
||||||
def test_d27_observer_permissions_match_topic_root():
|
def test_d27_observer_permissions_match_topic_root():
|
||||||
sys.path.insert(0, os.path.join(REPO_ROOT, ".agents", "skills", "multi-agent-mux-delegate-job", "scripts"))
|
sys.path.insert(0, os.path.join(REPO_ROOT, ".agents", "skills", "multi-agent-mux-delegate-job", "scripts"))
|
||||||
import mqtt_common
|
import mqtt_common
|
||||||
@@ -666,6 +690,7 @@ def test_d27_observer_permissions_match_topic_root():
|
|||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# D-28 — healthcheck contract and image coupling
|
# D-28 — healthcheck contract and image coupling
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@needs_docker_assets
|
||||||
def test_d28_healthcheck_contract_and_image_coupling():
|
def test_d28_healthcheck_contract_and_image_coupling():
|
||||||
_, svc = _load_compose()
|
_, svc = _load_compose()
|
||||||
hc = svc.get("healthcheck")
|
hc = svc.get("healthcheck")
|
||||||
@@ -684,6 +709,7 @@ def test_d28_healthcheck_contract_and_image_coupling():
|
|||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# D-29 — env secrets never tracked
|
# D-29 — env secrets never tracked
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@needs_docker_assets
|
||||||
def test_d29_env_secrets_never_tracked():
|
def test_d29_env_secrets_never_tracked():
|
||||||
is_submodule = os.path.exists(os.path.join(REPO_ROOT, ".gitmodules")) and "nats-docker" in DOCKER_DIR
|
is_submodule = os.path.exists(os.path.join(REPO_ROOT, ".gitmodules")) and "nats-docker" in DOCKER_DIR
|
||||||
target_repo = os.path.join(REPO_ROOT, "nats-docker") if is_submodule else REPO_ROOT
|
target_repo = os.path.join(REPO_ROOT, "nats-docker") if is_submodule else REPO_ROOT
|
||||||
@@ -703,6 +729,7 @@ def test_d29_env_secrets_never_tracked():
|
|||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# D-30 — websocket origin policy is startable
|
# D-30 — websocket origin policy is startable
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@needs_docker_assets
|
||||||
def test_d30_websocket_origin_policy_is_startable():
|
def test_d30_websocket_origin_policy_is_startable():
|
||||||
active_lines = _active_conf_lines()
|
active_lines = _active_conf_lines()
|
||||||
active_text = "\n".join(active_lines)
|
active_text = "\n".join(active_lines)
|
||||||
|
|||||||
@@ -497,6 +497,11 @@ def test_importing_delegation_does_not_mutate_environment(mam_sandbox):
|
|||||||
assert set(os.environ) == keys
|
assert set(os.environ) == keys
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_help(text: str) -> str:
|
||||||
|
text = text.replace("optional arguments:", "options:")
|
||||||
|
return re.sub(r"\s+", " ", text).strip()
|
||||||
|
|
||||||
|
|
||||||
def test_shim_help_output_matches_pre_move_capture():
|
def test_shim_help_output_matches_pre_move_capture():
|
||||||
"""The shim must be indistinguishable from the implementation at the CLI.
|
"""The shim must be indistinguishable from the implementation at the CLI.
|
||||||
|
|
||||||
@@ -512,7 +517,12 @@ def test_shim_help_output_matches_pre_move_capture():
|
|||||||
capture_output=True, text=True, timeout=60,
|
capture_output=True, text=True, timeout=60,
|
||||||
)
|
)
|
||||||
assert res.returncode == 0, res.stderr
|
assert res.returncode == 0, res.stderr
|
||||||
assert res.stdout + res.stderr == golden, "--help drifted for {}".format(mod)
|
# argparse re-wraps usage/help text and renamed the "optional arguments"
|
||||||
|
# group across Python versions (Issue #28: the captures were taken on
|
||||||
|
# 3.9, the Linux host runs 3.14). Compare the content, not the layout:
|
||||||
|
# whitespace runs collapsed, the group label unified.
|
||||||
|
assert _normalize_help(res.stdout + res.stderr) == _normalize_help(golden), \
|
||||||
|
"--help drifted for {}".format(mod)
|
||||||
|
|
||||||
|
|
||||||
# --- PV-4: the context matrix, each scenario in a fresh interpreter ------------
|
# --- PV-4: the context matrix, each scenario in a fresh interpreter ------------
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ class TestWorkspaceScope(unittest.TestCase):
|
|||||||
lib_sh = os.path.join(repo_root, ".agents", "skills", "lib.sh")
|
lib_sh = os.path.join(repo_root, ".agents", "skills", "lib.sh")
|
||||||
|
|
||||||
cmd = f"source {lib_sh} && env -u HERDR_SESSION_NAME -u HERDR_SERVER_NAME bash -c 'source {lib_sh} && resolve_herdr_session \"test-session\" \"{self.ws_dir}\"'"
|
cmd = f"source {lib_sh} && env -u HERDR_SESSION_NAME -u HERDR_SERVER_NAME bash -c 'source {lib_sh} && resolve_herdr_session \"test-session\" \"{self.ws_dir}\"'"
|
||||||
res = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
# `source` is a bash builtin; on hosts where /bin/sh is dash `shell=True`
|
||||||
|
# fails with "source: not found" (Issue #28), so run the string under bash.
|
||||||
|
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
|
||||||
self.assertEqual(res.returncode, 0, f"Command failed: {res.stderr}")
|
self.assertEqual(res.returncode, 0, f"Command failed: {res.stderr}")
|
||||||
parent = os.path.basename(os.path.dirname(os.path.abspath(self.ws_dir))).lower().replace('_', '-')
|
parent = os.path.basename(os.path.dirname(os.path.abspath(self.ws_dir))).lower().replace('_', '-')
|
||||||
work = os.path.basename(os.path.abspath(self.ws_dir)).lower().replace('_', '-')
|
work = os.path.basename(os.path.abspath(self.ws_dir)).lower().replace('_', '-')
|
||||||
|
|||||||
Reference in new issue
Block a user