Skip to content

Commit

Permalink
test: refactor monolithic test_smoke into multiple tests
Browse files Browse the repository at this point in the history
The existing test_smoke was becoming a bit too big and especially
the fact that part have to be skipped depending on the environment
is really not nice.

This commit refactors it to be more module. For this it uses the
pytest session scope to ensure that the build container and the
test image are only build once and then shared accross the
various individual tests.
  • Loading branch information
mvo5 authored and supakeen committed Dec 13, 2023
1 parent 17d6bef commit 57e26b6
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 52 deletions.
136 changes: 84 additions & 52 deletions test/test_smoke.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import collections
import json
import os
import pathlib
Expand All @@ -11,39 +12,102 @@
from vm import VM


@pytest.fixture(name="output_path")
def output_path_fixture(tmp_path):
output_path = tmp_path / "output"
output_path.mkdir(exist_ok=True)
return output_path
if not testutil.has_executable("podman"):
pytest.skip("no podman, skipping integration tests that required podman", allow_module_level=True)

if os.getuid() != 0:
pytest.skip("tests require root to run", allow_module_level=True)

# building an ELN image needs x86_64-v3 to work, we use avx2 as a proxy
# to detect if we have x86-64-v3 (not perfect but should be good enough)
if not testutil.has_x86_64_v3_cpu():
pytest.skip("need x86_64-v3 capable CPU", allow_module_level=True)


@pytest.fixture(name="build_container", scope="session")
def build_container_fixture():
"""Build a container from the Containerfile and returns the name"""
container_tag = "bootc-image-builder-test"
subprocess.check_call([
"podman", "build",
"-f", "Containerfile",
"-t", container_tag,
])
return container_tag


@pytest.fixture(name="config_json")
def config_json_fixture(output_path):
@pytest.fixture(name="build_image", scope="session")
def build_image_fixture(tmpdir_factory, build_container):
"""
Build an image inside the passed build_container and return a
named tuple with the resulting image path and user/password
"""
username = "test"
password = "password"
CFG = {
"blueprint": {
"customizations": {
"user": [
{
"name": "test",
"password": "password",
"name": username,
"password": password,
"groups": ["wheel"],
},
],
},
},
}
output_path = pathlib.Path(tmpdir_factory.mktemp("data")) / "output"
output_path.mkdir(exist_ok=True)

config_json_path = output_path / "config.json"
config_json_path.write_text(json.dumps(CFG), encoding="utf-8")
return config_json_path

cursor = testutil.journal_cursor()
# run container to deploy an image into output/qcow2/disk.qcow2
subprocess.check_call([
"podman", "run", "--rm",
"--privileged",
"--security-opt", "label=type:unconfined_t",
"-v", f"{output_path}:/output",
build_container,
"quay.io/centos-bootc/fedora-bootc:eln",
"--config", "/output/config.json",
])
journal_output = testutil.journal_after_cursor(cursor)
generated_img = pathlib.Path(output_path) / "qcow2/disk.qcow2"

ImageFixtureResult = collections.namedtuple(
"BuildImage", ["img_path", "username", "password", "journal_output"])
return ImageFixtureResult(generated_img, username, password, journal_output)


def test_container_builds(build_container):
output = subprocess.check_output([
"podman", "images", "-n", build_container], encoding="utf-8")
assert build_container in output


def test_image_is_generated(build_image):
assert build_image.img_path.exists(), "output file missing, dir "\
f"content: {os.listdir(os.fspath(build_image.img_path))}"


def test_image_boots(build_image):
with VM(build_image.img_path) as test_vm:
exit_status, _ = test_vm.run("true", user=build_image.username, password=build_image.password)
assert exit_status == 0
exit_status, output = test_vm.run("echo hello", user="test", password="password")
assert exit_status == 0
assert "hello" in output


def log_has_osbuild_selinux_denials(log):
OSBUID_SELINUX_DENIALS_RE = re.compile(r"(?ms)avc:\ +denied.*osbuild")
return re.search(OSBUID_SELINUX_DENIALS_RE, log)


def test_osbuild_selinux_denails_re_works():
def test_osbuild_selinux_denials_re_works():
fake_log = (
'Dec 05 07:19:39 other log msg\n'
'Dec 05 07:19:39 fedora audit: SELINUX_ERR'
Expand All @@ -60,45 +124,13 @@ def test_osbuild_selinux_denails_re_works():
assert not log_has_osbuild_selinux_denials("some\nrandom\nlogs")


@pytest.mark.skipif(os.getuid() != 0, reason="needs root")
@pytest.mark.skipif(not testutil.has_executable("podman"), reason="need podman")
def test_smoke(output_path, config_json):
# build local container
subprocess.check_call([
"podman", "build",
"-f", "Containerfile",
"-t", "bootc-image-builder-test",
])
cursor = testutil.journal_cursor()
# and run container to deploy an image into output/disk.qcow2
subprocess.check_call([
"podman", "run", "--rm",
"--privileged",
"--security-opt", "label=type:unconfined_t",
"-v", f"{output_path}:/output",
"bootc-image-builder-test",
"quay.io/centos-bootc/fedora-bootc:eln",
"--config", "/output/config.json",
])
generated_img = pathlib.Path(output_path) / "qcow2/disk.qcow2"
assert generated_img.exists(), f"output file missing, dir content: {os.listdir(os.fspath(output_path))}"
def has_selinux():
return testutil.has_executable("selinuxenabled") and subprocess.run("selinuxenabled").returncode == 0

# check that there are no selinux denials
journal_output = testutil.journal_after_cursor(cursor)
assert journal_output != ""
if testutil.has_executable("selinuxenabled") and subprocess.run("selinuxenabled").returncode == 0:
assert not log_has_osbuild_selinux_denials(journal_output), f"denials in log {journal_output}"
else:
print("WARNING: selinux not enabled, cannot check for denials")

# building an ELN image needs x86_64-v3 to work, we use avx2 as a proxy
# to detect if we have x86-64-v3 (not perfect but should be good enough)
if " avx2 " not in pathlib.Path("/proc/cpuinfo").read_text():
print("WARNING: no x86_64-v3 cpu detected, skipping VM boot test")
else:
with VM(generated_img) as test_vm:
exit_status, _ = test_vm.run("true", user="test", password="password")
assert exit_status == 0
exit_status, output = test_vm.run("echo hello", user="test", password="password")
assert exit_status == 0
assert "hello" in output

@pytest.mark.skipif(not has_selinux(), reason="selinux not enabled")
def test_image_build_without_se_linux_denials(build_image):
# the journal always contains logs from the image building
assert build_image.journal_output != ""
assert not log_has_osbuild_selinux_denials(build_image.journal_output), \
f"denials in log {build_image.journal_output}"
8 changes: 8 additions & 0 deletions test/testutil.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pathlib
import socket
import shutil
import subprocess
Expand Down Expand Up @@ -39,3 +40,10 @@ def wait_ssh_ready(port, sleep, max_wait_sec):
pass
time.sleep(sleep)
raise ConnectionRefusedError(f"cannot connect to port {port} after {max_wait_sec}s")


def has_x86_64_v3_cpu():
# x86_64-v3 has multiple features, see
# https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels
# but "avx2" is probably a good enough proxy
return " avx2 " in pathlib.Path("/proc/cpuinfo").read_text()

0 comments on commit 57e26b6

Please sign in to comment.