forked from avocado-framework/avocado
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
VM image dependencies in tests * Comprehensive functional tests in `runner_vmimage.py` * Documentation section in dependencies guide * Example test and recipe JSON * Integration with resolver and check systems * Setup.py entry points for plugin discovery Reference: avocado-framework#6043 Signed-off-by: Harvey Lynden <[email protected]>
- Loading branch information
1 parent
0e6226f
commit a3e6528
Showing
9 changed files
with
488 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import multiprocessing | ||
import signal | ||
import sys | ||
import time | ||
import traceback | ||
from multiprocessing import set_start_method | ||
|
||
from avocado.core.exceptions import TestInterrupt | ||
from avocado.core.nrunner.app import BaseRunnerApp | ||
from avocado.core.nrunner.runner import RUNNER_RUN_CHECK_INTERVAL, BaseRunner | ||
from avocado.core.utils import messages | ||
from avocado.core.utils.messages import start_logging | ||
from avocado.plugins.vmimage import download_image | ||
from avocado.utils import vmimage | ||
|
||
|
||
class VMImageRunner(BaseRunner): | ||
""" | ||
Runner for dependencies of type vmimage. | ||
This runner uses the vmimage plugin's download_image function which handles: | ||
1. Checking if the image exists in cache | ||
2. Downloading the image if not in cache | ||
3. Storing the image in the configured cache directory | ||
4. Returning the cached image path | ||
""" | ||
|
||
name = "vmimage" | ||
description = "Runner for dependencies of type vmimage" | ||
|
||
@staticmethod | ||
def signal_handler(signum, frame): # pylint: disable=W0613 | ||
if signum == signal.SIGTERM.value: | ||
raise TestInterrupt("VM image operation interrupted: Timeout reached") | ||
|
||
@staticmethod | ||
def _run_vmimage_operation(runnable, queue): | ||
try: | ||
signal.signal(signal.SIGTERM, VMImageRunner.signal_handler) | ||
start_logging(runnable.config, queue) | ||
provider = runnable.kwargs.get("provider") | ||
version = runnable.kwargs.get("version") | ||
arch = runnable.kwargs.get("arch") | ||
|
||
if not all([provider, version, arch]): | ||
stderr = "Missing required parameters: provider, version, and arch" | ||
queue.put(messages.StderrMessage.get(stderr.encode())) | ||
queue.put(messages.FinishedMessage.get("error")) | ||
return | ||
|
||
queue.put( | ||
messages.StdoutMessage.get( | ||
f"Getting VM image for {provider} {version} {arch}".encode() | ||
) | ||
) | ||
|
||
try: | ||
# download_image will use cache if available, otherwise download | ||
# It will raise AttributeError if provider is not found | ||
provider_normalized = provider.lower() | ||
image = download_image(provider_normalized, version, arch) | ||
if not image: | ||
raise RuntimeError("Failed to get image") | ||
|
||
queue.put( | ||
messages.StdoutMessage.get( | ||
f"Successfully retrieved VM image from cache or downloaded to: {image['file']}".encode() | ||
) | ||
) | ||
queue.put(messages.FinishedMessage.get("pass")) | ||
|
||
except (AttributeError, RuntimeError, vmimage.ImageProviderError) as e: | ||
# AttributeError: provider not found | ||
# RuntimeError: failed to get image | ||
# ImageProviderError: provider-specific errors | ||
queue.put( | ||
messages.StderrMessage.get( | ||
f"Failed to download image: {str(e)}".encode() | ||
) | ||
) | ||
queue.put( | ||
messages.FinishedMessage.get( | ||
"error", | ||
fail_reason=str(e), | ||
fail_class=e.__class__.__name__, | ||
traceback=traceback.format_exc(), | ||
) | ||
) | ||
|
||
except (TestInterrupt, multiprocessing.TimeoutError) as e: | ||
queue.put(messages.StderrMessage.get(traceback.format_exc().encode())) | ||
queue.put( | ||
messages.FinishedMessage.get( | ||
"error", | ||
fail_reason=str(e), | ||
fail_class=e.__class__.__name__, | ||
traceback=traceback.format_exc(), | ||
) | ||
) | ||
|
||
@staticmethod | ||
def _monitor(queue): | ||
while True: | ||
time.sleep(RUNNER_RUN_CHECK_INTERVAL) | ||
if queue.empty(): | ||
yield messages.RunningMessage.get() | ||
else: | ||
message = queue.get() | ||
yield message | ||
if message.get("status") == "finished": | ||
break | ||
|
||
def run(self, runnable): | ||
signal.signal(signal.SIGTERM, VMImageRunner.signal_handler) | ||
yield messages.StartedMessage.get() | ||
try: | ||
queue = multiprocessing.SimpleQueue() | ||
process = multiprocessing.Process( | ||
target=self._run_vmimage_operation, args=(runnable, queue) | ||
) | ||
|
||
process.start() | ||
|
||
for message in self._monitor(queue): | ||
yield message | ||
|
||
except TestInterrupt: | ||
process.terminate() | ||
for message in self._monitor(queue): | ||
yield message | ||
except (multiprocessing.ProcessError, OSError) as e: | ||
# ProcessError: Issues with process management | ||
# OSError: System-level errors (e.g. resource limits) | ||
yield messages.StderrMessage.get(traceback.format_exc().encode()) | ||
yield messages.FinishedMessage.get( | ||
"error", | ||
fail_reason=str(e), | ||
fail_class=e.__class__.__name__, | ||
traceback=traceback.format_exc(), | ||
) | ||
|
||
|
||
class RunnerApp(BaseRunnerApp): | ||
PROG_NAME = "avocado-runner-vmimage" | ||
PROG_DESCRIPTION = "nrunner application for dependencies of type vmimage" | ||
RUNNABLE_KINDS_CAPABLE = ["vmimage"] | ||
|
||
|
||
def main(): | ||
if sys.platform == "darwin": | ||
set_start_method("fork") | ||
app = RunnerApp(print) | ||
app.run() | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"kind": "vmimage", "kwargs": {"provider": "fedora", "version": "41", "arch": "x86_64"}} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import os | ||
|
||
from avocado import Test | ||
from avocado.core.settings import settings | ||
|
||
|
||
class VmimageTest(Test): | ||
""" | ||
Test demonstrating VM image dependency usage. | ||
The vmimage dependency runner will ensure the required VM image | ||
is downloaded and cached before the test execution begins. | ||
:avocado: dependency={"type": "vmimage", "provider": "fedora", "version": "41", "arch": "s390x"} | ||
""" | ||
|
||
def test_vmimage_exists(self): | ||
""" | ||
Verify that the VM image was downloaded by the vmimage runner. | ||
""" | ||
# Get cache directory from settings | ||
cache_dir = settings.as_dict().get("datadir.paths.cache_dirs")[0] | ||
cache_base = os.path.join(cache_dir, "by_location") | ||
|
||
# The image should be in the cache since the runner downloaded it | ||
self.assertTrue( | ||
os.path.exists(cache_base), f"Cache directory {cache_base} does not exist" | ||
) | ||
|
||
# Log cache contents for debugging | ||
self.log.info("Cache directory contents:") | ||
for root, _, files in os.walk(cache_base): | ||
for f in files: | ||
if f.endswith((".qcow2", ".raw", ".img")): | ||
self.log.info("Found image: %s", os.path.join(root, f)) | ||
|
||
|
||
class MultiArchVmimageTest(Test): | ||
""" | ||
Test demonstrating multiple VM image dependencies with different architectures. | ||
:avocado: dependency={"type": "vmimage", "provider": "fedora", "version": "41", "arch": "s390x"} | ||
:avocado: dependency={"type": "vmimage", "provider": "fedora", "version": "41", "arch": "x86_64"} | ||
""" | ||
|
||
def test_multiple_images(self): | ||
""" | ||
Verify that multiple VM images can be handled by the runner. | ||
Checks that both s390x and x86_64 images exist in the cache | ||
and have the expected properties. | ||
""" | ||
# Get cache directory from settings | ||
cache_dir = settings.as_dict().get("datadir.paths.cache_dirs")[0] | ||
cache_base = os.path.join(cache_dir, "by_location") | ||
|
||
# The cache directory should exist | ||
self.assertTrue( | ||
os.path.exists(cache_base), f"Cache directory {cache_base} does not exist" | ||
) | ||
|
||
# Track if we found both architectures | ||
found_s390x = False | ||
found_x86_64 = False | ||
|
||
# Search for both architecture images | ||
self.log.info("Searching for Fedora 41 images (s390x and x86_64):") | ||
for root, _, files in os.walk(cache_base): | ||
for f in files: | ||
if not f.endswith((".qcow2", ".raw", ".img")): | ||
continue | ||
|
||
filepath = os.path.join(root, f) | ||
self.log.info("Found image: %s", filepath) | ||
|
||
# Check for architecture markers in path/filename | ||
if "s390x" in filepath: | ||
found_s390x = True | ||
if "x86_64" in filepath: | ||
found_x86_64 = True | ||
|
||
# Verify both architectures were found | ||
self.assertTrue(found_s390x, "s390x Fedora 41 image not found in cache") | ||
self.assertTrue(found_x86_64, "x86_64 Fedora 41 image not found in cache") | ||
|
||
|
||
class UbuntuVmimageTest(Test): | ||
""" | ||
Test demonstrating VM image dependency with a different provider. | ||
:avocado: dependency={"type": "vmimage", "provider": "ubuntu", "version": "22.04", "arch": "x86_64"} | ||
""" | ||
|
||
def test_ubuntu_image(self): | ||
""" | ||
Verify that Ubuntu images can be handled by the runner. | ||
Checks that the Ubuntu x86_64 image exists in the cache | ||
and has the expected properties. | ||
""" | ||
# Get cache directory from settings | ||
cache_dir = settings.as_dict().get("datadir.paths.cache_dirs")[0] | ||
cache_base = os.path.join(cache_dir, "by_location") | ||
|
||
# The cache directory should exist | ||
self.assertTrue( | ||
os.path.exists(cache_base), f"Cache directory {cache_base} does not exist" | ||
) | ||
|
||
# Track if we found the Ubuntu image | ||
found_ubuntu = False | ||
|
||
# Search for Ubuntu x86_64 image | ||
self.log.info("Searching for Ubuntu 22.04 x86_64 image:") | ||
for root, _, files in os.walk(cache_base): | ||
for f in files: | ||
if not f.endswith((".qcow2", ".raw", ".img")): | ||
continue | ||
|
||
filepath = os.path.join(root, f) | ||
|
||
# Check for Ubuntu cloud image filename pattern | ||
if "ubuntu-22.04-server-cloudimg-amd64.img" in filepath.lower(): | ||
self.log.info("Found Ubuntu image: %s", filepath) | ||
found_ubuntu = True | ||
|
||
# Verify Ubuntu image was found | ||
self.assertTrue(found_ubuntu, "Ubuntu 22.04 x86_64 image not found in cache") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.