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 Jan 8, 2024
1 parent abddc34 commit edce2a0
Show file tree
Hide file tree
Showing 9 changed files with 67 additions and 8 deletions.
10 changes: 10 additions & 0 deletions avocado/core/plugin_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,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 @@ -289,6 +289,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 @@ -325,8 +335,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
11 changes: 6 additions & 5 deletions avocado/plugins/spawners/lxc.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ class LXCSpawner(Spawner, SpawnerMixin):
METHODS = [SpawnMethod.STANDALONE_EXECUTABLE]
slots_cache = {}

def is_operational(self):
if not LXC_AVAILABLE:
LOG.error("LXC python bindings not available on the system")
return False
return True

@staticmethod
def run_container_cmd(container, command):
with LXCStreamsFile() as tmp_out, LXCStreamsFile() as tmp_err:
Expand Down Expand Up @@ -209,11 +215,6 @@ async def spawn_task(self, runtime_task):
arch = self.config.get("spawner.lxc.arch")
create_hook = self.config.get("spawner.lxc.create_hook")

if not LXC_AVAILABLE:
msg = "LXC python bindings not available on the system"
runtime_task.status = msg
return False

container_id = runtime_task.spawner_handle
container = lxc.Container(container_id)
if not container.defined:
Expand Down
14 changes: 14 additions & 0 deletions avocado/plugins/spawners/podman.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,20 @@ def _get_podman_version(self):
f'output received "{out}" does not match expected output'
)

def is_operational(self):
try:
_ = self.podman_version
except PodmanException as ex:
LOG.error(ex)
return False

if self.podman_version[0] >= 3:
return True
LOG.error(
f"The podman binary f{self.podman_bin} did not report a suitable version (>= 3.0)"
)
return False

@property
def podman_version(self):
if self._podman_version == (None, None, None):
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 @@ -33,6 +33,9 @@ class ProcessSpawner(Spawner, SpawnerMixin):
description = "Process based spawner"
METHODS = [SpawnMethod.STANDALONE_EXECUTABLE]

def is_operational(self):
return True

@staticmethod
def is_task_alive(runtime_task):
if runtime_task.spawner_handle is None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ class RemoteSpawner(Spawner, SpawnerMixin):
METHODS = [SpawnMethod.STANDALONE_EXECUTABLE]
slots_cache = {}

def is_operational(self):
return True

@staticmethod
async def run_remote_cmd_async(session, command, timeout):
loop = asyncio.get_event_loop()
Expand Down
2 changes: 1 addition & 1 deletion selftests/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"nrunner-requirement": 16,
"unit": 667,
"jobs": 11,
"functional-parallel": 297,
"functional-parallel": 298,
"functional-serial": 4,
"optional-plugins": 0,
"optional-plugins-golang": 2,
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 edce2a0

Please sign in to comment.