Skip to content

Commit

Permalink
Spawner: require runtime operation suitability
Browse files Browse the repository at this point in the history
This adds a new mandatory method to spawners: is_operational().  The
goal of this method is to signal to the execution of the test suite
whether the spawner is fully set up and capable to operate.  This will
of course, depend on the spawner implementation and requirements.

In the case of the podman spawner, often times jobs configured to use
that spawner will succeed, when they actually need to signal a
failure.  This is what happens on a system without a working podman
installation:

   # avocado run --spawner=podman -- /bin/true
   JOB ID     : daf6869a348f14c52460adc6f18f89f35f8d6ecd
   JOB LOG    : /root/avocado/job-results/job-2023-04-14T20.57-daf6869/job.log
   RESULTS    : PASS 0 | ERROR 0 | FAIL 0 | SKIP 1 | WARN 0 | INTERRUPT 0 | CANCEL 0
   JOB TIME   : 0.32 s
   [root@22d3a3b15197 ~]# echo $?
   0

Clearly, test workflows depending on the tests errouneously succeed.
With this change, an error is reported both on the logs/UI and on the
exit code.

Signed-off-by: Cleber Rosa <[email protected]>
  • Loading branch information
clebergnu committed Jul 18, 2023
1 parent 9132c75 commit d6bc2c2
Show file tree
Hide file tree
Showing 7 changed files with 74 additions and 2 deletions.
10 changes: 10 additions & 0 deletions avocado/core/plugin_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,16 @@ async def update_requirement_cache(runtime_task, result):
:type result: `avocado.core.teststatus.STATUSES`
"""

@abc.abstractmethod
def is_operational(self):
"""Checks whether this spawner is operationally capable to perform.
:result: whether or not this spawner is operational on this system,
that is, whether it has all its requirements set up and
should be ready to operate successfully.
:rtype: bool
"""


class DeploymentSpawner(Spawner):
"""Spawners that needs basic deployment are based on this class.
Expand Down
3 changes: 3 additions & 0 deletions avocado/core/spawners/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ class MockSpawner(Spawner):
def __init__(self): # pylint: disable=W0231
self._known_tasks = {}

def is_operational(self):
return True

def is_task_alive(self, runtime_task): # pylint: disable=W0221
alive = self._known_tasks.get(runtime_task, None)
# task was never spawned
Expand Down
12 changes: 10 additions & 2 deletions avocado/plugins/runner_nrunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,16 @@ def run_suite(self, job, test_suite):
job.interrupted_reason = f"Suite {test_suite.name} is disabled."
return summary

spawner_name = test_suite.config.get("run.spawner")
spawner = SpawnerDispatcher(test_suite.config, job)[spawner_name].obj
if not spawner.is_operational():
suite_name = f" {test_suite.name}" if test_suite.name else ""
msg = f'Spawner "{spawner_name}" is not operational, aborting execution of suite{suite_name}. Please check the logs for more information.'
LOG_JOB.error(msg)
job.interrupted_reason = msg
summary.add("INTERRUPTED")
return summary

test_suite.tests, missing_requirements = check_runnables_runner_requirements(
test_suite.tests
)
Expand Down Expand Up @@ -303,8 +313,6 @@ def run_suite(self, job, test_suite):
if rt.task.category == "test"
]
self.tsm = TaskStateMachine(self.runtime_tasks, self.status_repo)
spawner_name = test_suite.config.get("run.spawner")
spawner = SpawnerDispatcher(test_suite.config, job)[spawner_name].obj
max_running = min(
test_suite.config.get("run.max_parallel_tasks"), len(self.runtime_tasks)
)
Expand Down
3 changes: 3 additions & 0 deletions avocado/plugins/spawners/lxc.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ class LXCSpawner(Spawner, SpawnerMixin):
METHODS = [SpawnMethod.STANDALONE_EXECUTABLE]
slots_cache = {}

def is_operational(self):
return True

@staticmethod
def run_container_cmd(container, command):
with LXCStreamsFile() as tmp_out, LXCStreamsFile() as tmp_err:
Expand Down
28 changes: 28 additions & 0 deletions avocado/plugins/spawners/podman.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,34 @@ def __init__(self, config=None, job=None): # pylint: disable=W0231
SpawnerMixin.__init__(self, config, job)
self.environment = f"podman:{self.config.get('spawner.podman.image')}"

def is_operational(self):
podman_bin = self.config.get("spawner.podman.bin")
cmd = [podman_bin, "version", "--format={{.Version}}"]
try:
process = subprocess.Popen(
cmd,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
)
except FileNotFoundError:
LOG.error(f"The podman binary {podman_bin} could not be found")
return False
out, _ = process.communicate()
if process.returncode != 0:
LOG.error(
f"The execution of the podman binary {podman_bin} signaled failure (return code {process.returncode}"
)
return False
if out:
major, _, _ = [int(v) for v in out.decode().split(".")]
if major >= 3:
return True
LOG.error(
f"The podman binary f{podman_bin} did not report a suitable version (>= 3.0)"
)
return False

def is_task_alive(self, runtime_task): # pylint: disable=W0221
if runtime_task.spawner_handle is None:
return False
Expand Down
3 changes: 3 additions & 0 deletions avocado/plugins/spawners/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ class ProcessSpawner(Spawner, SpawnerMixin):
description = "Process based spawner"
METHODS = [SpawnMethod.STANDALONE_EXECUTABLE]

def is_operational(self):
return True

async def _collect_task(self, task_handle):
await task_handle.wait()

Expand Down
17 changes: 17 additions & 0 deletions selftests/functional/plugin/spawners/podman.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os

from avocado import Test
from avocado.core import exit_codes
from avocado.core.job import Job
from avocado.utils import process, script
from selftests.utils import AVOCADO, BASEDIR
Expand Down Expand Up @@ -116,3 +117,19 @@ def test_asset_files(self):
self.assertEqual(result.exit_status, 0)
self.assertIn("use_data.sh: STARTED", result.stdout_text)
self.assertIn("use_data.sh: PASS", result.stdout_text)


class OperationalTest(Test):
def test_not_operational(self):
fake_podman_bin = os.path.join(BASEDIR, "examples", "tests", "false")
result = process.run(
f"{AVOCADO} run "
f"--job-results-dir {self.workdir} "
f"--disable-sysinfo --spawner=podman "
f"--spawner-podman-bin={fake_podman_bin} "
f"--spawner-podman-image=fedora:36 -- "
f"examples/tests/true",
ignore_status=True,
)
self.assertEqual(result.exit_status, exit_codes.AVOCADO_JOB_INTERRUPTED)
self.assertIn('Spawner "podman" is not operational', result.stderr_text)

0 comments on commit d6bc2c2

Please sign in to comment.