From eb437016f337a9d902f5a93d0b7f90ba92d8e08e Mon Sep 17 00:00:00 2001 From: Atul Nair Date: Wed, 13 May 2026 20:26:17 -0400 Subject: [PATCH 1/2] Port install_rvs to the orch fixture; update orchestrator scope docs Switch cvs/tests/health/install/install_rvs.py from the legacy phdl/shdl Pssh handles to the shared orch fixture in cvs/tests/conftest.py, mirroring rvs_cvs.py. 21 phdl.exec / hdl.exec callsites become orch.exec. Drop the head-only-on-NFS dispatch (config_dict['nfs_install'] is True). On the canonical mi300_health_config.json the JSON value is the string "True" while the test compared with Python `is True`, so the branch was dead in shipped configs and head-only behavior was never engaged. Custom configs that relied on JSON boolean true to install from a single head node will now install on all nodes in parallel; this matches what cvs run install_rvs already did via parallel SSH. Layer-3 doc updates so install_rvs is no longer omitted from the "which suites consume the orchestrator" claims: - docs/_includes/orchestrator-scope.rst: scope note now names rvs_cvs and install_rvs. - docs/reference/configuration-files/cluster-file.rst: L42 (container backend scope) and L107 (orchestrator key scope) updated. - cvs/tests/health/README.md: Container-mode example pins both suites. - docs/how-to/run-cvs-tests.rst: container-mode note pins both suites. Verified via cvs run install_rvs in baremetal Tier-2 on a single MI300X node (a06u01): pytest exit 0, "RVS installation and verification completed successfully" emitted, auto-bundle written. Container Tier-2 not exercised in this commit because the orch fixture's container backend has two pre-existing regressions independent of this port: MultiProcessPssh.exec drops the `detailed=` kwarg (PR #136 backward-compat regression) which blocks orch.setup_containers(), and cvs/core/runtimes/docker.py exec does not wrap commands in `bash -c` so shell chains like `cd X; cmake ...` get split between host and container. Both will be fixed in follow-up PRs. --- cvs/tests/health/README.md | 2 +- cvs/tests/health/install/install_rvs.py | 115 +++++------------- docs/_includes/orchestrator-scope.rst | 2 +- docs/how-to/run-cvs-tests.rst | 2 +- .../configuration-files/cluster-file.rst | 4 +- 5 files changed, 34 insertions(+), 91 deletions(-) diff --git a/cvs/tests/health/README.md b/cvs/tests/health/README.md index 3777fa85..b8d568f8 100644 --- a/cvs/tests/health/README.md +++ b/cvs/tests/health/README.md @@ -61,7 +61,7 @@ RVS provides comprehensive GPU validation through multiple test modules. The tes #### Container mode -To use it, copy the `cluster_container.json` template (`cvs copy-config cluster_container.json --output ...`), set the container `image` and `name`, and pass the resulting cluster file to `cvs run rvs_cvs`. See the in-tree reference at [`cvs/input/cluster_file/README.md`](../../input/cluster_file/README.md) and the published [container-mode how-to](https://rocm.docs.amd.com/projects/cvs/en/latest/how-to/run-with-containers.html). +To use it, copy the `cluster_container.json` template (`cvs copy-config cluster_container.json --output ...`), set the container `image` and `name`, and pass the resulting cluster file to `cvs run install_rvs` or `cvs run rvs_cvs`. See the in-tree reference at [`cvs/input/cluster_file/README.md`](../../input/cluster_file/README.md) and the published [container-mode how-to](https://rocm.docs.amd.com/projects/cvs/en/latest/how-to/run-with-containers.html). #### Supported Test Modules diff --git a/cvs/tests/health/install/install_rvs.py b/cvs/tests/health/install/install_rvs.py index f2a341ff..b66f6853 100644 --- a/cvs/tests/health/install/install_rvs.py +++ b/cvs/tests/health/install/install_rvs.py @@ -10,7 +10,6 @@ import re import json -from cvs.lib.parallel_ssh_lib import * from cvs.lib.utils_lib import * from cvs.lib.verify_lib import * @@ -22,12 +21,12 @@ # Importing additional cmd line args to script .. -def detect_rocm_path(phdl, config_rocm_path): +def detect_rocm_path(orch, config_rocm_path): """ Detect the ROCm installation path, supporting both old (/opt/rocm) and new (/opt/rocm/core-X.Y) layouts. Args: - phdl: Parallel SSH handle + orch: Orchestrator instance config_rocm_path (str): Configured ROCm path from config file (empty string for auto-detect) Returns: @@ -42,7 +41,7 @@ def detect_rocm_path(phdl, config_rocm_path): log.info('Auto-detecting ROCm path...') # Try new ROCm 7.x structure first (/opt/rocm/core-X.Y) - out_dict = phdl.exec('ls -d /opt/rocm/core-* 2>/dev/null | sort -V | tail -1') + out_dict = orch.exec('ls -d /opt/rocm/core-* 2>/dev/null | sort -V | tail -1') for node, output in out_dict.items(): if output and '/opt/rocm/core-' in output: rocm_path = output.strip() @@ -50,7 +49,7 @@ def detect_rocm_path(phdl, config_rocm_path): return rocm_path # Fall back to legacy /opt/rocm - out_dict = phdl.exec('test -d /opt/rocm && echo "/opt/rocm"') + out_dict = orch.exec('test -d /opt/rocm && echo "/opt/rocm"') for node, output in out_dict.items(): if '/opt/rocm' in output: log.info('Detected ROCm path (legacy layout): /opt/rocm') @@ -61,26 +60,26 @@ def detect_rocm_path(phdl, config_rocm_path): return '/opt/rocm' -def detect_hip_compiler(phdl, rocm_path): +def detect_hip_compiler(orch, rocm_path): """ Detect the HIP compiler (hipcc or amdclang++) for the given ROCm installation. Args: - phdl: Parallel SSH handle + orch: Orchestrator instance rocm_path (str): ROCm installation path Returns: str: Full path to the HIP compiler """ # Try hipcc first (ROCm 7.x) - out_dict = phdl.exec(f'test -f {rocm_path}/bin/hipcc && echo "{rocm_path}/bin/hipcc"') + out_dict = orch.exec(f'test -f {rocm_path}/bin/hipcc && echo "{rocm_path}/bin/hipcc"') for node, output in out_dict.items(): if output and 'hipcc' in output: log.info(f'Detected HIP compiler: {rocm_path}/bin/hipcc') return f'{rocm_path}/bin/hipcc' # Fall back to amdclang++ (older ROCm versions) - out_dict = phdl.exec(f'test -f {rocm_path}/bin/amdclang++ && echo "{rocm_path}/bin/amdclang++"') + out_dict = orch.exec(f'test -f {rocm_path}/bin/amdclang++ && echo "{rocm_path}/bin/amdclang++"') for node, output in out_dict.items(): if output and 'amdclang++' in output: log.info(f'Detected HIP compiler: {rocm_path}/bin/amdclang++') @@ -167,52 +166,8 @@ def config_dict(config_file, cluster_dict): return config_dict -@pytest.fixture(scope="module") -def shdl(cluster_dict): - """ - Build and return a parallel SSH handle (Pssh) for the head node only. - - Args: - cluster_dict (dict): Cluster metadata fixture (see phdl docstring). - - Returns: - Pssh: Handle configured for the first node (head node) in node_dict. - - Notes: - - Useful when commands should be executed only from a designated head node. - - Module scope ensures a single connection context for the duration of the module. - """ - node_list = list(cluster_dict['node_dict'].keys()) - env_vars = cluster_dict.get("env_vars") - head_node = node_list[0] - shdl = Pssh(log, [head_node], user=cluster_dict['username'], pkey=cluster_dict['priv_key_file'], env_vars=env_vars) - return shdl - - -# Create connection to DUTs and export for later use .. -@pytest.fixture(scope="module") -def phdl(cluster_dict): - """ - Create a parallel SSH handle for all cluster nodes. - - Args: - cluster_dict (dict): Loaded cluster configuration with at least: - - node_dict: mapping of node -> details - - username: SSH username - - priv_key_file: path to SSH key - - Returns: - Pssh: Handle that executes commands on all nodes and returns dict[node] -> output. - """ - log.info("%s", cluster_dict) - env_vars = cluster_dict.get("env_vars") - node_list = list(cluster_dict['node_dict'].keys()) - phdl = Pssh(log, node_list, user=cluster_dict['username'], pkey=cluster_dict['priv_key_file'], env_vars=env_vars) - return phdl - - @pytest.mark.dependency(name="init") -def test_install_rvs(phdl, shdl, config_dict): +def test_install_rvs(orch, config_dict): """ Install/Build ROCmValidationSuite (RVS) and verify installation on all nodes. @@ -222,31 +177,19 @@ def test_install_rvs(phdl, shdl, config_dict): - Build and install RVS on all nodes - Verify RVS executable exists and configuration files are accessible - Depending on the flag nfs_install in the config_file, decide if you want to use shdl (single node) - or all nodes (phdl) - If the nfs_install flag is set to True, then it assumes the install_dir is on a common file system - that is accessible from all the nodes and installs from just a single node, otherwise install on - all the nodes. + Install commands and verification run across all nodes via ``orch.exec(...)``. Args: - phdl: Parallel SSH handle for all nodes. - shdl: Head-node SSH handle + orch: Orchestrator instance. config_dict (dict): Includes: - git_install_path: directory to clone/build - git_url: repository URL - path: expected installation path for RVS binary - - nfs_install: whether to use NFS-shared installation """ globals.error_list = [] - # For install case, if the systems are using NFS, use single connection to head node - if config_dict['nfs_install'] is True: - hdl = shdl - else: - hdl = phdl - # Detect ROCm path early for use throughout function - rocm_path = detect_rocm_path(phdl, config_dict.get('rocm_path', '')) + rocm_path = detect_rocm_path(orch, config_dict.get('rocm_path', '')) log.info(f"Using ROCm path: {rocm_path}") # Update config paths to use detected rocm_path (support both old and new ROCm layouts) @@ -271,7 +214,7 @@ def test_install_rvs(phdl, shdl, config_dict): git_url = config_dict['git_url'] # Check if RVS is already installed via system packages - out_dict = phdl.exec('which rvs', timeout=30) + out_dict = orch.exec('which rvs', timeout=30) rvs_found = False for node in out_dict.keys(): if out_dict[node].strip() and re.search('rvs', out_dict[node], re.I): @@ -281,7 +224,7 @@ def test_install_rvs(phdl, shdl, config_dict): # Check if RVS config files exist # Check MI300X path first (same order as final verification) and suppress stderr # so a missing fallback path's "No such file" does not contaminate the output. - out_dict = phdl.exec( + out_dict = orch.exec( f'ls -l {config_dict["config_path_mi300x"]}/gst_single.conf 2>/dev/null || ls -l {config_dict["config_path_default"]}/gst_single.conf 2>/dev/null', timeout=30, ) @@ -297,12 +240,12 @@ def test_install_rvs(phdl, shdl, config_dict): # First try to install from artifactory repo package_installed = False - out_dict = hdl.exec('sudo apt-get update -y', timeout=600) - out_dict = hdl.exec( + out_dict = orch.exec('sudo apt-get update -y', timeout=600) + out_dict = orch.exec( 'sudo apt-get install -y libpci3 libpci-dev doxygen unzip cmake git libyaml-cpp-dev', timeout=600 ) - out_dict = hdl.exec('sudo apt-get install -y rocblas rocm-smi-lib', timeout=600) - out_dict = hdl.exec('sudo apt-get install -y rocm-validation-suite', timeout=600) + out_dict = orch.exec('sudo apt-get install -y rocblas rocm-smi-lib', timeout=600) + out_dict = orch.exec('sudo apt-get install -y rocm-validation-suite', timeout=600) for node in out_dict.keys(): if re.search( @@ -320,7 +263,7 @@ def test_install_rvs(phdl, shdl, config_dict): # The rocm-validation-suite package may install to /opt/rocm even when the # detected rocm_path is /opt/rocm/core-X.Y (new layout), causing path mismatches. if package_installed: - verify_bin = phdl.exec( + verify_bin = orch.exec( f'which rvs 2>/dev/null || ls {config_dict["path"]} 2>/dev/null', timeout=60, ) @@ -349,27 +292,27 @@ def test_install_rvs(phdl, shdl, config_dict): log.info('Installing RVS from source') # Check if install directory exists, otherwise create - out_dict = hdl.exec(f'ls -ld {git_install_path}') + out_dict = orch.exec(f'ls -ld {git_install_path}') for node in out_dict.keys(): if re.search('No such file', out_dict[node]): - hdl.exec(f'mkdir -p {git_install_path}') + orch.exec(f'mkdir -p {git_install_path}') # Remove any existing RVS directory and clone fresh - out_dict = hdl.exec(f'rm -rf {git_install_path}/ROCmValidationSuite') - out_dict = hdl.exec(f'cd {git_install_path};git clone {git_url}', timeout=300) + out_dict = orch.exec(f'rm -rf {git_install_path}/ROCmValidationSuite') + out_dict = orch.exec(f'cd {git_install_path};git clone {git_url}', timeout=300) # Build and install RVS (using rocm_path detected earlier) try: - out_dict = hdl.exec( + out_dict = orch.exec( f'cd {git_install_path}/ROCmValidationSuite; cmake -B ./build -DROCM_PATH={rocm_path} -DCMAKE_INSTALL_PREFIX={rocm_path} -DCPACK_PACKAGING_INSTALL_PREFIX={rocm_path} -DHIP_PLATFORM=amd', timeout=1200, ) - out_dict = hdl.exec(f'cd {git_install_path}/ROCmValidationSuite/build; make -j$(nproc)', timeout=1200) + out_dict = orch.exec(f'cd {git_install_path}/ROCmValidationSuite/build; make -j$(nproc)', timeout=1200) - out_dict = hdl.exec( + out_dict = orch.exec( f'cd {git_install_path}/ROCmValidationSuite/build; make -j$(nproc) package', timeout=1200 ) - out_dict = hdl.exec( + out_dict = orch.exec( f'cd {git_install_path}/ROCmValidationSuite/build; sudo make install; echo "RVS_INSTALL_STATUS:$?"', timeout=1200, ) @@ -382,13 +325,13 @@ def test_install_rvs(phdl, shdl, config_dict): fail_test(f'RVS installation failed with exception: {e}') # Verify RVS installation - out_dict = phdl.exec(f'which rvs || ls -l {rocm_path}/bin/rvs*', timeout=60) + out_dict = orch.exec(f'which rvs || ls -l {rocm_path}/bin/rvs*', timeout=60) for node in out_dict.keys(): if re.search('not found|No such file', out_dict[node], re.I) and not re.search('rvs', out_dict[node]): fail_test(f'RVS installation verification failed on node {node}') # Verify config files are accessible - out_dict = phdl.exec( + out_dict = orch.exec( f'ls -l {config_dict["config_path_mi300x"]}/gst_single.conf || ls -l {config_dict["config_path_default"]}/gst_single.conf', timeout=60, ) diff --git a/docs/_includes/orchestrator-scope.rst b/docs/_includes/orchestrator-scope.rst index 1e1c4228..e88dace9 100644 --- a/docs/_includes/orchestrator-scope.rst +++ b/docs/_includes/orchestrator-scope.rst @@ -1,3 +1,3 @@ .. note:: - **Scope today.** Of the test suites shipped with CVS, only ``rvs_cvs`` consumes the orchestrator and honors the ``orchestrator`` key in the cluster file. All other ``cvs run`` test suites and the ``cvs exec`` CLI run on the host regardless of the ``orchestrator`` value. Migrating additional suites to the orchestrator is tracked separately. Custom Python scripts can use the ``OrchestratorFactory`` API directly as an escape hatch. + **Scope today.** Of the test suites shipped with CVS, only ``rvs_cvs`` and ``install_rvs`` consume the orchestrator and honor the ``orchestrator`` key in the cluster file. All other ``cvs run`` test suites and the ``cvs exec`` CLI run on the host regardless of the ``orchestrator`` value. Migrating additional suites to the orchestrator is tracked separately. Custom Python scripts can use the ``OrchestratorFactory`` API directly as an escape hatch. diff --git a/docs/how-to/run-cvs-tests.rst b/docs/how-to/run-cvs-tests.rst index 9f9f91f6..9b5f18ff 100644 --- a/docs/how-to/run-cvs-tests.rst +++ b/docs/how-to/run-cvs-tests.rst @@ -332,7 +332,7 @@ Use these scripts to start the test: .. note:: - RVS additionally supports running inside a per-host container instead of on the host filesystem. Pass a ``cluster_container.json`` cluster file with ``orchestrator: container`` to route ``rvs`` invocations through the container backend. See :doc:`/how-to/run-with-containers`. + Both ``cvs run install_rvs`` and ``cvs run rvs_cvs`` support running inside a per-host container instead of on the host filesystem. Pass a ``cluster_container.json`` cluster file with ``orchestrator: container`` to route invocations through the container backend. See :doc:`/how-to/run-with-containers`. InfiniBand (IB Perf) test script -------------------------------- diff --git a/docs/reference/configuration-files/cluster-file.rst b/docs/reference/configuration-files/cluster-file.rst index 2fab825d..4d48c59c 100644 --- a/docs/reference/configuration-files/cluster-file.rst +++ b/docs/reference/configuration-files/cluster-file.rst @@ -39,7 +39,7 @@ Container Container mode routes workload commands through a container runtime on each node. CVS uses host SSH to the node, then ``docker exec`` into a long-lived per-host container. Inside the container, an SSH daemon listens on port ``2224`` so that MPI-style workloads can fan out using the in-container SSH transport. -The container backend is currently consumed by ``rvs_cvs`` only. The container lifecycle (start, verify, sshd setup, teardown) runs from the test fixture in `cvs/tests/health/rvs_cvs.py `_. +The container backend is currently consumed by ``rvs_cvs`` and ``install_rvs``. The container lifecycle (start, verify, sshd setup, teardown) runs from the test fixture in `cvs/tests/health/rvs_cvs.py `_. Cluster file shape ================== @@ -104,7 +104,7 @@ Top-level parameters - Description * - ``orchestrator`` - ``baremetal`` - - Execution backend. Set to ``container`` to route workload commands through the container runtime. Honored today only by ``rvs_cvs``; see the Backends section above. + - Execution backend. Set to ``container`` to route workload commands through the container runtime. Honored today only by ``rvs_cvs`` and ``install_rvs``; see the Backends section above. * - ``username`` - ``{user-id}`` - SSH username for all hosts. ``{user-id}`` resolves to the current login user at runtime. From cf34cbf29d11f594d0dd2d157b8bde7d109638ef Mon Sep 17 00:00:00 2001 From: Atul Nair Date: Thu, 14 May 2026 18:36:39 -0400 Subject: [PATCH 2/2] Add Tier-1 dispatch tests for install_rvs orch port Pin three properties of the migrated install_rvs.py against both BaremetalOrchestrator and ContainerOrchestrator orch instances: 1. dispatch flows through orch.exec (not orch.exec_on_head); 2. orch.exec(cmd, timeout=...) is accepted by both backends; 3. the legacy nfs_install head-only branch is gone (regression canary if someone re-introduces it). Tests stub orch.exec / orch.exec_on_head directly on the constructed orchestrator instance, sidestepping the signature drift between BaremetalOrchestrator.exec(cmd, hosts=None, timeout=None) and ContainerOrchestrator.exec(cmd, hosts=None, timeout=None, detailed=False). Run from the repo root: python -m unittest \ cvs.tests.health.install.unittests.test_install_rvs_dispatch -v --- .../health/install/unittests/__init__.py | 8 ++ .../unittests/test_install_rvs_dispatch.py | 126 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 cvs/tests/health/install/unittests/__init__.py create mode 100644 cvs/tests/health/install/unittests/test_install_rvs_dispatch.py diff --git a/cvs/tests/health/install/unittests/__init__.py b/cvs/tests/health/install/unittests/__init__.py new file mode 100644 index 00000000..23cf8cde --- /dev/null +++ b/cvs/tests/health/install/unittests/__init__.py @@ -0,0 +1,8 @@ +''' +Copyright 2025 Advanced Micro Devices, Inc. +All rights reserved. This notice is intended as a precaution against inadvertent publication and does not imply publication or any waiver of confidentiality. +The year included in the foregoing notice is the year of creation of the work. +All code contained here is Property of Advanced Micro Devices, Inc. +''' + +# Unit tests for the cvs.tests.health.install package diff --git a/cvs/tests/health/install/unittests/test_install_rvs_dispatch.py b/cvs/tests/health/install/unittests/test_install_rvs_dispatch.py new file mode 100644 index 00000000..f873c312 --- /dev/null +++ b/cvs/tests/health/install/unittests/test_install_rvs_dispatch.py @@ -0,0 +1,126 @@ +''' +Copyright 2025 Advanced Micro Devices, Inc. +All rights reserved. This notice is intended as a precaution against inadvertent publication and does not imply publication or any waiver of confidentiality. +The year included in the foregoing notice is the year of creation of the work. +All code contained here is Property of Advanced Micro Devices, Inc. +''' + +# Tier-1 dispatch tests for cvs/tests/health/install/install_rvs.py after the +# orch-fixture port. Pins: +# 1. install_rvs dispatches via orch.exec (not orch.exec_on_head). +# 2. The call signature install_rvs uses (`orch.exec(cmd, timeout=...)`) is +# accepted by both BaremetalOrchestrator and ContainerOrchestrator orch +# instances. +# 3. The legacy nfs_install head-only dispatch was dropped (regression canary +# if someone re-introduces it). +# +# orch.exec / orch.exec_on_head are stubbed directly on the constructed +# orchestrator instance (not at the Pssh layer) to sidestep the signature drift +# between BaremetalOrchestrator.exec(cmd, hosts=None, timeout=None) and +# ContainerOrchestrator.exec(cmd, hosts=None, timeout=None, detailed=False). + +import unittest +from unittest.mock import MagicMock, patch + +from cvs.core.orchestrators.factory import OrchestratorConfig +from cvs.core.orchestrators.baremetal import BaremetalOrchestrator +from cvs.core.orchestrators.container import ContainerOrchestrator +from cvs.lib import globals +from cvs.tests.health.install import install_rvs as install_rvs_mod + + +def _bm_config(): + """Minimal OrchestratorConfig that satisfies BaremetalOrchestrator.__init__ + without touching disk or SSH.""" + return OrchestratorConfig( + orchestrator="baremetal", + node_dict={"10.0.0.1": {}, "10.0.0.2": {}}, + username="testuser", + priv_key_file="/dev/null", + password=None, + head_node_dict={"mgmt_ip": "10.0.0.1"}, + container={}, + ) + + +def _ct_config(): + """Minimal OrchestratorConfig for ContainerOrchestrator.__init__. launch=False + keeps setup_containers from being triggered as a side effect.""" + return OrchestratorConfig( + orchestrator="container", + node_dict={"10.0.0.1": {}, "10.0.0.2": {}}, + username="testuser", + priv_key_file="/dev/null", + password=None, + head_node_dict={"mgmt_ip": "10.0.0.1"}, + container={ + "enabled": True, + "launch": False, + "image": "rocm/cvs:test", + "name": "cvs_iter_test", + "runtime": {"name": "docker", "args": {}}, + }, + ) + + +# Single happy-path output that satisfies every regex check in +# install_rvs.test_install_rvs: +# line 220: re.search('rvs', out, re.I) -> matches "rvs" +# line 233: re.search(r'gst_single\.conf', out, re.I) -> matches "gst_single.conf" +# line 330: re.search('not found|No such file', out) -> does NOT match +# line 339: re.search('No such file', out) -> does NOT match +_HAPPY_OUT = { + "10.0.0.1": "/opt/rocm/bin/rvs and gst_single.conf", + "10.0.0.2": "/opt/rocm/bin/rvs and gst_single.conf", +} + + +def _config_dict(): + """Minimal config_dict for install_rvs.test_install_rvs. `rocm_path` is set + explicitly so detect_rocm_path returns early without consuming any exec calls.""" + return { + "git_install_path": "/tmp/install", + "git_url": "https://example.invalid/rvs", + "path": "/opt/rocm/bin/rvs", + "config_path_mi300x": "/opt/rocm/share/rocm-validation-suite/conf", + "config_path_default": "/opt/rocm/share/rocm-validation-suite/conf", + "rocm_path": "/opt/rocm", + } + + +class TestInstallRvsBaremetalDispatch(unittest.TestCase): + @patch("cvs.core.orchestrators.baremetal.Pssh") + def test_dispatches_via_orch_exec_only(self, _mock_pssh): + orch = BaremetalOrchestrator(MagicMock(), _bm_config()) + orch.exec = MagicMock(return_value=_HAPPY_OUT) + orch.exec_on_head = MagicMock() + globals.error_list = [] + + install_rvs_mod.test_install_rvs(orch, _config_dict()) + + orch.exec.assert_any_call("which rvs", timeout=30) + orch.exec_on_head.assert_not_called() + self.assertEqual(globals.error_list, []) + + +class TestInstallRvsContainerDispatch(unittest.TestCase): + @patch("cvs.core.orchestrators.container.RuntimeFactory") + @patch("cvs.core.orchestrators.baremetal.Pssh") + def test_dispatches_via_orch_exec_only(self, _mock_pssh, mock_rf): + runtime = MagicMock(name="docker_runtime") + mock_rf.create.return_value = runtime + + orch = ContainerOrchestrator(MagicMock(), _ct_config()) + orch.exec = MagicMock(return_value=_HAPPY_OUT) + orch.exec_on_head = MagicMock() + globals.error_list = [] + + install_rvs_mod.test_install_rvs(orch, _config_dict()) + + orch.exec.assert_any_call("which rvs", timeout=30) + orch.exec_on_head.assert_not_called() + self.assertEqual(globals.error_list, []) + + +if __name__ == "__main__": + unittest.main()