Skip to content

Commit 836cd49

Browse files
authored
Merge pull request #22 from desultory/tests
Add basic tests support
2 parents 6aba724 + e09a6bf commit 836cd49

File tree

15 files changed

+216
-67
lines changed

15 files changed

+216
-67
lines changed

src/ugrd/base/cmdline.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
__author__ = 'desultory'
2-
__version__ = '2.2.3'
2+
__version__ = '2.3.0'
33

44

55
CMDLINE_BOOLS = ['quiet', 'debug', 'recovery', 'rootwait']
@@ -60,4 +60,9 @@ def mount_cmdline_root(self) -> str:
6060

6161
def export_exports(self) -> list:
6262
""" Returns a bash script exporting all exports defined in the exports key. """
63+
from importlib.metadata import version, PackageNotFoundError
64+
try:
65+
self['exports']['VERSION'] = version(__package__)
66+
except PackageNotFoundError:
67+
self['exports']['VERSION'] = 9999
6368
return [f'setvar {key} "{value}"' for key, value in self['exports'].items()]

src/ugrd/base/core.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ library_paths = [ "/lib64" ]
1010
old_count = 1
1111

1212
binaries = [ "/bin/bash" ]
13+
banner = 'einfo "UGRD v$(readvar VERSION)"'
1314

1415
[nodes.console]
1516
mode = 0o644
@@ -64,4 +65,5 @@ out_dir = "Path" # The directory where the initramfs is packed/output. If no pa
6465
old_count = "int" # The number of times to cycle old files before deleting
6566
clean = "bool" # Add the clean property, used to define if the mounts should be cleaned up after boot
6667
file_owner = "str" # Add the file_owner property, used to define who should own the copied initramfs files
68+
banner = "str" # Banner string to be printed at the start of init
6769

src/ugrd/crypto/cryptsetup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def _validate_crypysetup_key(self, key_paramters: dict) -> None:
6767
key_copy = parent
6868

6969

70+
@check_dict('validate', value=True, log_level=30, message="Skipping cryptsetup configuration validation.")
7071
def _validate_cryptsetup_config(self, mapped_name: str, config: dict) -> None:
7172
self.logger.log(5, "[%s] Validating cryptsetup configuration: %s" % (mapped_name, config))
7273
for parameter in config:

src/ugrd/fs/cpio.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
__author__ = 'desultory'
2-
__version__ = '2.9.0'
2+
__version__ = '3.0.1'
33

44

55
from pycpio import PyCPIO
@@ -14,8 +14,8 @@ def check_cpio(self, cpio: PyCPIO) -> None:
1414
self.logger.debug("Dependency found in CPIO: %s" % dep)
1515

1616

17-
def get_cpio_filename(self) -> str:
18-
""" Generates a CPIO filename based on the current configuration. """
17+
def get_archive_path(self) -> str:
18+
""" Determines the filename for the output CPIO archive based on the current configuration. """
1919
if out_file := self.get('out_file'):
2020
self.logger.info("Using specified out_file: %s" % out_file)
2121
else:
@@ -28,15 +28,17 @@ def get_cpio_filename(self) -> str:
2828
if compression_type.lower() != 'false': # The variable is a string, so we need to check for the string 'false'
2929
out_file += f".{compression_type}"
3030

31-
return self.out_dir / out_file
31+
self['_archive_out_path'] = self.out_dir / out_file
3232

3333

3434
def make_cpio(self) -> None:
3535
"""
36-
Creates a CPIO archive from the build directory and writes it to the output directory.
36+
Populates the CPIO archive using the build directory,
37+
writes it to the output file, and rotates the output file if necessary.
38+
Creates device nodes in the CPIO archive if the mknod_cpio option is set.
3739
Raises FileNotFoundError if the output directory does not exist.
3840
"""
39-
cpio = PyCPIO(logger=self.logger, _log_bump=10)
41+
cpio = self._cpio_archive
4042
cpio.append_recursive(self.build_dir, relative=True)
4143
check_cpio(self, cpio)
4244

@@ -45,7 +47,7 @@ def make_cpio(self) -> None:
4547
self.logger.debug("Adding CPIO node: %s" % node)
4648
cpio.add_chardev(name=node['path'], mode=node['mode'], major=node['major'], minor=node['minor'])
4749

48-
out_cpio = get_cpio_filename(self)
50+
out_cpio = self['_archive_out_path']
4951

5052
if not out_cpio.parent.exists():
5153
self._mkdir(out_cpio.parent)
@@ -80,5 +82,7 @@ def _process_out_file(self, out_file):
8082
self['out_dir'] = Path(out_file).parent
8183
self.logger.info("Resolved out_dir to: %s" % self['out_dir'])
8284
out_file = Path(out_file).name
85+
else:
86+
out_file = Path(out_file)
8387

8488
dict.__setitem__(self, 'out_file', out_file)

src/ugrd/fs/cpio.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ cpio_rotate = true
55
[imports.config_processing]
66
"ugrd.fs.cpio" = [ "_process_out_file" ]
77

8+
[imports.build_tasks]
9+
"ugrd.fs.cpio" = ['get_archive_path']
10+
811
[imports.pack]
912
"ugrd.fs.cpio" = [ "make_cpio" ]
1013

@@ -13,3 +16,5 @@ out_file = "str" # The name of the cpio file to create.
1316
cpio_rotate = "bool" # makes a .old backup of the cpio file if it already exists.
1417
mknod_cpio = "bool" # When enabled, mknod is not used to create device nodes, they are just created in the cpio.
1518
cpio_compression = "str" # The compression method to use for the cpio file. Currently, only xz is supported.
19+
_archive_out_path = "Path" # The name of the file to create, determined based on runtime config.
20+
_cpio_archive = "PyCPIO" # The cpio archive object.

src/ugrd/fs/test_image.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
__version__ = "0.3.0"
2+
3+
4+
def init_test_vars(self):
5+
if not self.get('test_kernel'):
6+
raise ValueError("No test kernel specified")
7+
elif not self['test_kernel'].exists():
8+
raise FileNotFoundError("Test kernel not found: %s" % self['test_kernel'])
9+
10+
self['banner'] = f"echo {self['test_flag']}"
11+
12+
13+
def make_test_image(self):
14+
""" Creates a test image from the build dir """
15+
self.logger.info("Creating test image from: %s" % self.build_dir.resolve())
16+
17+
# Create the test image file, flll with 0s
18+
with open(self._archive_out_path, "wb") as f:
19+
self.logger.info("Creating test image file: %s" % self._archive_out_path)
20+
f.write(b"\0" * self.test_image_size * 2 ** 20)
21+
22+
rootfs_uuid = self['mounts']['root']['uuid']
23+
rootfs_type = self['mounts']['root']['type']
24+
25+
if rootfs_type == 'ext4':
26+
self._run(['mkfs', '-t', rootfs_type, '-d', self.build_dir.resolve(), '-U', rootfs_uuid, '-F', self._archive_out_path])
27+
elif rootfs_type == 'btrfs':
28+
self._run(['mkfs', '-t', rootfs_type, '--rootdir', self.build_dir.resolve(), '-U', rootfs_uuid, self._archive_out_path])
29+
else:
30+
raise Exception("Unsupported test rootfs type: %s" % rootfs_type)
31+

src/ugrd/fs/test_image.toml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
modules = ['ugrd.fs.cpio']
2+
paths = ['dev', 'sys', 'proc']
3+
4+
5+
binaries = ['halt', 'shutdown', 'ls', 'top', 'ps']
6+
shebang = "#!/bin/bash"
7+
8+
qemu_arch = 'x86_64'
9+
test_image_size = 16
10+
test_memory = '256M'
11+
test_cpu = 'host'
12+
test_cmdline = 'console=ttyS0,115200 panic=1'
13+
test_flag = "UGRD TEST SUCCESSFUL"
14+
test_timeout = 15
15+
16+
[masks]
17+
pack = "make_cpio"
18+
19+
[imports.build_pre]
20+
"ugrd.fs.test_image" = ["init_test_vars"]
21+
22+
[imports.pack]
23+
"ugrd.fs.test_image" = ["make_test_image"]
24+
25+
[custom_parameters]
26+
shebang = "str" # Add the shebang property, because the test mode disables the base module
27+
test_kernel = "Path" # Define the kernel to use for the test image
28+
test_cmdline = "str" # Define the kernel command line for the test image
29+
test_image_size = "int" # Define the size of the test image in MiB
30+
test_memory = "str" # Define the amount of memory to use for the test image (passed to qemu)
31+
test_cpu = "str" # Define the CPU to use for the test image (passed to qemu)
32+
qemu_arch = "str" # Define the qemu arch (added to the qemu-system- command)
33+
mounts = "dict" # We only need the mounts dict, not the whole module
34+
test_flag = "str" # Define the success flag used to determine if the test was successful
35+
test_timeout = "int" # Define the timeout for the test
36+

src/ugrd/generator_helpers.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from typing import Union
22
from pathlib import Path
3-
from subprocess import run, CompletedProcess
3+
from subprocess import run, CompletedProcess, TimeoutExpired
44

55
from zenlib.util import pretty_print
66

7-
__version__ = "1.0.0"
7+
__version__ = "1.1.0"
88
__author__ = "desultory"
99

1010

@@ -142,10 +142,15 @@ def _symlink(self, source: Union[Path, str], target: Union[Path, str]) -> None:
142142
self.logger.debug("Creating symlink: %s -> %s" % (target, source))
143143
symlink(source, target)
144144

145-
def _run(self, args: list[str]) -> CompletedProcess:
145+
def _run(self, args: list[str], timeout=15) -> CompletedProcess:
146146
""" Runs a command, returns the CompletedProcess object """
147-
self.logger.debug("Running command: %s" % ' '.join(args))
148-
cmd = run(args, capture_output=True)
147+
cmd_args = [str(arg) for arg in args]
148+
self.logger.debug("Running command: %s" % ' '.join(cmd_args))
149+
try:
150+
cmd = run(cmd_args, capture_output=True, timeout=timeout)
151+
except TimeoutExpired as e:
152+
raise RuntimeError("[%ds] Command timed out: %s" % (timeout, [str(arg) for arg in cmd_args])) from e
153+
149154
if cmd.returncode != 0:
150155
self.logger.error("Failed to run command: %s" % cmd.args)
151156
self.logger.error("Command output: %s" % cmd.stdout.decode())

src/ugrd/initramfs_dict.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

22
__author__ = "desultory"
3-
__version__ = "1.5.2"
3+
__version__ = "1.7.0"
44

55
from tomllib import load, TOMLDecodeError
66
from pathlib import Path
@@ -39,11 +39,16 @@ def __init__(self, NO_BASE=False, *args, **kwargs):
3939
super().__setitem__(parameter, default_type())
4040
if not NO_BASE:
4141
self['modules'] = 'ugrd.base.base'
42+
else:
43+
self['modules'] = 'ugrd.base.core'
4244

4345
def import_args(self, args: dict) -> None:
4446
""" Imports data from an argument dict. """
4547
for arg, value in args.items():
4648
self.logger.info("Importing argument '%s' with value: %s" % (arg, value))
49+
if arg == 'modules':
50+
for module in value.split(','):
51+
self[arg] = module
4752
self[arg] = value
4853

4954
def __setitem__(self, key: str, value) -> None:
@@ -119,6 +124,7 @@ def _process_custom_parameters(self, parameter_name: str, parameter_type: type)
119124
120125
If the parameter is in the processing queue, process the queued values.
121126
"""
127+
from pycpio import PyCPIO
122128
self['custom_parameters'][parameter_name] = eval(parameter_type)
123129
self.logger.debug("Registered custom parameter '%s' with type: %s" % (parameter_name, parameter_type))
124130

@@ -133,6 +139,8 @@ def _process_custom_parameters(self, parameter_name: str, parameter_type: type)
133139
super().__setitem__(parameter_name, 0)
134140
case "float":
135141
super().__setitem__(parameter_name, 0.0)
142+
case "PyCPIO":
143+
super().__setitem__(parameter_name, PyCPIO(logger=self.logger, _log_init=False, _log_bump=10))
136144
case _: # For strings and things, don't init them so they are None
137145
self.logger.debug("Leaving '%s' as None" % parameter_name)
138146

src/ugrd/initramfs_generator.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,16 @@
55

66
from ugrd.initramfs_dict import InitramfsConfigDict
77

8-
from importlib.metadata import version
9-
108
from .generator_helpers import GeneratorHelpers
119

12-
__version__ = version(__package__)
1310
__author__ = "desultory"
1411

1512

1613
@loggify
1714
class InitramfsGenerator(GeneratorHelpers):
1815
def __init__(self, config='/etc/ugrd/config.toml', *args, **kwargs):
1916
self.config_filename = config
20-
self.config_dict = InitramfsConfigDict(logger=self.logger)
17+
self.config_dict = InitramfsConfigDict(NO_BASE=kwargs.pop('NO_BASE', False), logger=self.logger)
2118

2219
# Used for functions that are added to the bash source file
2320
self.included_functions = {}
@@ -28,8 +25,11 @@ def __init__(self, config='/etc/ugrd/config.toml', *args, **kwargs):
2825
# init_pre and init_final are run as part of generate_initramfs_main
2926
self.init_types = ['init_debug', 'init_early', 'init_main', 'init_late', 'init_premount', 'init_mount', 'init_mount_late', 'init_cleanup']
3027

31-
self.load_config()
3228
self.config_dict.import_args(kwargs)
29+
if config:
30+
self.load_config()
31+
else:
32+
self.logger.warning("No config file specified, using the base config")
3333
self.config_dict.validate()
3434

3535
def load_config(self) -> None:
@@ -127,11 +127,14 @@ def run_init_hook(self, level: str) -> list[str]:
127127
return out
128128
else:
129129
self.logger.debug("No output for init level: %s" % level)
130+
return []
130131

131132
def generate_profile(self) -> None:
132133
""" Generates the bash profile file based on self.included_functions. """
133-
out = [self['shebang'],
134-
f"#\n# Generated by UGRD v{__version__}"]
134+
from importlib.metadata import version
135+
ver = version(__package__) or 9999 # Version won't be found unless the package is installed
136+
out = [self['shebang'].split(' ')[0], # Don't add arguments to the shebang (for the profile)
137+
f"#\n# Generated by UGRD v{ver}\n#"]
135138

136139
# Add the library paths
137140
library_paths = ":".join(self['library_paths'])
@@ -172,7 +175,7 @@ def generate_init(self) -> None:
172175
self.run_hook('functions', force_include=True)
173176

174177
init.extend(self.run_init_hook('init_pre'))
175-
init += [f'einfo "Starting UGRD v{__version__}"']
178+
init += [self.banner]
176179

177180
if self['imports'].get('custom_init') and self.get('_custom_init_file'):
178181
init += ["\n# !!custom_init"]
@@ -191,7 +194,7 @@ def generate_init(self) -> None:
191194
self._write('/etc/profile', self.generate_profile(), 0o755)
192195
self.logger.info("Included functions: %s" % ', '.join(list(self.included_functions.keys())))
193196
if self['imports'].get('custom_init'):
194-
custom_init.insert(2, f"einfo 'Starting custom init, UGRD v{__version__}'")
197+
custom_init.insert(2, self.banner)
195198

196199
if self.get('_custom_init_file'):
197200
self._write(self['_custom_init_file'], custom_init, 0o755)

0 commit comments

Comments
 (0)