diff --git a/.flake8 b/.flake8 index 39310a6c30..54fe379a4e 100644 --- a/.flake8 +++ b/.flake8 @@ -8,3 +8,4 @@ show-source = True statistics = True builtins = _ per-file-ignores = __init__.py:F401,F403,F405 simple_menu.py:C901,W503 guided.py:C901 network_configuration.py:F821 +exclude = tests \ No newline at end of file diff --git a/.github/workflows/iso-build.yaml b/.github/workflows/iso-build.yaml index 6464b4a221..fde0c20e07 100644 --- a/.github/workflows/iso-build.yaml +++ b/.github/workflows/iso-build.yaml @@ -19,6 +19,10 @@ on: types: - created +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 39ef62c9ea..f67b7857f4 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -5,11 +5,15 @@ jobs: runs-on: ubuntu-latest container: image: archlinux:latest - options: --privileged + options: --privileged -v /dev:/dev + # --cap-add=MKNOD --device-cgroup-rule="b 7:* rmw" steps: - uses: actions/checkout@v2 - - run: pacman --noconfirm -Syu python python-pip qemu gcc + - run: pacman --noconfirm -Syu python python-pip parted - run: python -m pip install --upgrade pip + - run: pwd + - run: ls -la + - run: pip install . - run: pip install pytest - name: Test with pytest - run: python -m pytest || exit 0 \ No newline at end of file + run: python -m pytest \ No newline at end of file diff --git a/archinstall/__init__.py b/archinstall/__init__.py index f362064853..07c834c108 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -150,7 +150,7 @@ def get_arguments() -> Dict[str, Any]: parsed_url = urllib.parse.urlparse(args.config) if not parsed_url.scheme: # The Profile was not a direct match on a remote URL, it must be a local file. - if not json_stream_to_structure('--config',args.config,config): + if not json_stream_to_structure('--config', args.config, config): exit(1) else: # Attempt to load the configuration from the URL. with urllib.request.urlopen(urllib.request.Request(args.config, headers={'User-Agent': 'ArchInstall'})) as response: diff --git a/archinstall/examples b/archinstall/examples deleted file mode 120000 index 785887f7fb..0000000000 --- a/archinstall/examples +++ /dev/null @@ -1 +0,0 @@ -../examples/ \ No newline at end of file diff --git a/examples/__init__.py b/archinstall/examples/__init__.py similarity index 100% rename from examples/__init__.py rename to archinstall/examples/__init__.py diff --git a/examples/config-sample.json b/archinstall/examples/config-sample.json similarity index 100% rename from examples/config-sample.json rename to archinstall/examples/config-sample.json diff --git a/examples/creds-sample.json b/archinstall/examples/creds-sample.json similarity index 100% rename from examples/creds-sample.json rename to archinstall/examples/creds-sample.json diff --git a/examples/custom-command-sample.json b/archinstall/examples/custom-command-sample.json similarity index 100% rename from examples/custom-command-sample.json rename to archinstall/examples/custom-command-sample.json diff --git a/examples/disk_layouts-sample.json b/archinstall/examples/disk_layouts-sample.json similarity index 100% rename from examples/disk_layouts-sample.json rename to archinstall/examples/disk_layouts-sample.json diff --git a/examples/guided.py b/archinstall/examples/guided.py similarity index 99% rename from examples/guided.py rename to archinstall/examples/guided.py index 15226668e0..4be4d66178 100644 --- a/examples/guided.py +++ b/archinstall/examples/guided.py @@ -178,8 +178,8 @@ def perform_installation(mountpoint): archinstall.use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium # Retrieve list of additional repositories and set boolean values appropriately - enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', None) - enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', None) + enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) + enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) if installation.minimal_installation(testing=enable_testing, multilib=enable_multilib): installation.set_locale(archinstall.arguments['sys-language'], archinstall.arguments['sys-encoding'].upper()) diff --git a/examples/minimal.py b/archinstall/examples/minimal.py similarity index 100% rename from examples/minimal.py rename to archinstall/examples/minimal.py diff --git a/examples/only_hd.py b/archinstall/examples/only_hd.py similarity index 100% rename from examples/only_hd.py rename to archinstall/examples/only_hd.py diff --git a/examples/swiss.py b/archinstall/examples/swiss.py similarity index 100% rename from examples/swiss.py rename to archinstall/examples/swiss.py diff --git a/examples/unattended.py b/archinstall/examples/unattended.py similarity index 100% rename from examples/unattended.py rename to archinstall/examples/unattended.py diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py index da41d16d5e..2aaf652821 100644 --- a/archinstall/lib/output.py +++ b/archinstall/lib/output.py @@ -126,6 +126,6 @@ def log(*args :str, **kwargs :Union[str, int, Dict[str, Union[str, int]]]) -> No # Finally, print the log unless we skipped it based on level. # We use sys.stdout.write()+flush() instead of print() to try and # fix issue #94 - if kwargs.get('level', logging.INFO) != logging.DEBUG or storage['arguments'].get('verbose', False): + if kwargs.get('level', logging.INFO) != logging.DEBUG or hasattr(storage.get('arguments'), 'verbose') and storage['arguments'].verbose: sys.stdout.write(f"{string}\n") sys.stdout.flush() diff --git a/archinstall/profiles b/archinstall/profiles deleted file mode 120000 index c2968eea02..0000000000 --- a/archinstall/profiles +++ /dev/null @@ -1 +0,0 @@ -../profiles/ \ No newline at end of file diff --git a/profiles/52-54-00-12-34-56.py b/archinstall/profiles/52-54-00-12-34-56.py similarity index 100% rename from profiles/52-54-00-12-34-56.py rename to archinstall/profiles/52-54-00-12-34-56.py diff --git a/profiles/__init__.py b/archinstall/profiles/__init__.py similarity index 100% rename from profiles/__init__.py rename to archinstall/profiles/__init__.py diff --git a/profiles/applications/__init__.py b/archinstall/profiles/applications/__init__.py similarity index 100% rename from profiles/applications/__init__.py rename to archinstall/profiles/applications/__init__.py diff --git a/profiles/applications/awesome.py b/archinstall/profiles/applications/awesome.py similarity index 100% rename from profiles/applications/awesome.py rename to archinstall/profiles/applications/awesome.py diff --git a/profiles/applications/cockpit.py b/archinstall/profiles/applications/cockpit.py similarity index 100% rename from profiles/applications/cockpit.py rename to archinstall/profiles/applications/cockpit.py diff --git a/profiles/applications/docker.py b/archinstall/profiles/applications/docker.py similarity index 100% rename from profiles/applications/docker.py rename to archinstall/profiles/applications/docker.py diff --git a/profiles/applications/httpd.py b/archinstall/profiles/applications/httpd.py similarity index 100% rename from profiles/applications/httpd.py rename to archinstall/profiles/applications/httpd.py diff --git a/profiles/applications/lighttpd.py b/archinstall/profiles/applications/lighttpd.py similarity index 100% rename from profiles/applications/lighttpd.py rename to archinstall/profiles/applications/lighttpd.py diff --git a/profiles/applications/mariadb.py b/archinstall/profiles/applications/mariadb.py similarity index 100% rename from profiles/applications/mariadb.py rename to archinstall/profiles/applications/mariadb.py diff --git a/profiles/applications/nginx.py b/archinstall/profiles/applications/nginx.py similarity index 100% rename from profiles/applications/nginx.py rename to archinstall/profiles/applications/nginx.py diff --git a/profiles/applications/pipewire.py b/archinstall/profiles/applications/pipewire.py similarity index 100% rename from profiles/applications/pipewire.py rename to archinstall/profiles/applications/pipewire.py diff --git a/profiles/applications/postgresql.py b/archinstall/profiles/applications/postgresql.py similarity index 100% rename from profiles/applications/postgresql.py rename to archinstall/profiles/applications/postgresql.py diff --git a/profiles/applications/sshd.py b/archinstall/profiles/applications/sshd.py similarity index 100% rename from profiles/applications/sshd.py rename to archinstall/profiles/applications/sshd.py diff --git a/profiles/applications/tomcat.py b/archinstall/profiles/applications/tomcat.py similarity index 100% rename from profiles/applications/tomcat.py rename to archinstall/profiles/applications/tomcat.py diff --git a/profiles/awesome.py b/archinstall/profiles/awesome.py similarity index 100% rename from profiles/awesome.py rename to archinstall/profiles/awesome.py diff --git a/profiles/bspwm.py b/archinstall/profiles/bspwm.py similarity index 100% rename from profiles/bspwm.py rename to archinstall/profiles/bspwm.py diff --git a/profiles/budgie.py b/archinstall/profiles/budgie.py similarity index 100% rename from profiles/budgie.py rename to archinstall/profiles/budgie.py diff --git a/profiles/cinnamon.py b/archinstall/profiles/cinnamon.py similarity index 100% rename from profiles/cinnamon.py rename to archinstall/profiles/cinnamon.py diff --git a/profiles/cutefish.py b/archinstall/profiles/cutefish.py similarity index 100% rename from profiles/cutefish.py rename to archinstall/profiles/cutefish.py diff --git a/profiles/deepin.py b/archinstall/profiles/deepin.py similarity index 100% rename from profiles/deepin.py rename to archinstall/profiles/deepin.py diff --git a/profiles/desktop.py b/archinstall/profiles/desktop.py similarity index 100% rename from profiles/desktop.py rename to archinstall/profiles/desktop.py diff --git a/profiles/enlightenment.py b/archinstall/profiles/enlightenment.py similarity index 100% rename from profiles/enlightenment.py rename to archinstall/profiles/enlightenment.py diff --git a/profiles/gnome.py b/archinstall/profiles/gnome.py similarity index 100% rename from profiles/gnome.py rename to archinstall/profiles/gnome.py diff --git a/profiles/i3.py b/archinstall/profiles/i3.py similarity index 100% rename from profiles/i3.py rename to archinstall/profiles/i3.py diff --git a/profiles/kde.py b/archinstall/profiles/kde.py similarity index 100% rename from profiles/kde.py rename to archinstall/profiles/kde.py diff --git a/profiles/lxqt.py b/archinstall/profiles/lxqt.py similarity index 100% rename from profiles/lxqt.py rename to archinstall/profiles/lxqt.py diff --git a/profiles/mate.py b/archinstall/profiles/mate.py similarity index 100% rename from profiles/mate.py rename to archinstall/profiles/mate.py diff --git a/profiles/minimal.py b/archinstall/profiles/minimal.py similarity index 100% rename from profiles/minimal.py rename to archinstall/profiles/minimal.py diff --git a/profiles/qtile.py b/archinstall/profiles/qtile.py similarity index 100% rename from profiles/qtile.py rename to archinstall/profiles/qtile.py diff --git a/profiles/server.py b/archinstall/profiles/server.py similarity index 100% rename from profiles/server.py rename to archinstall/profiles/server.py diff --git a/profiles/sway.py b/archinstall/profiles/sway.py similarity index 100% rename from profiles/sway.py rename to archinstall/profiles/sway.py diff --git a/profiles/xfce4.py b/archinstall/profiles/xfce4.py similarity index 100% rename from profiles/xfce4.py rename to archinstall/profiles/xfce4.py diff --git a/profiles/xorg.py b/archinstall/profiles/xorg.py similarity index 100% rename from profiles/xorg.py rename to archinstall/profiles/xorg.py diff --git a/examples b/examples new file mode 120000 index 0000000000..910e23847d --- /dev/null +++ b/examples @@ -0,0 +1 @@ +archinstall/examples/ \ No newline at end of file diff --git a/profiles b/profiles new file mode 120000 index 0000000000..4cccef6782 --- /dev/null +++ b/profiles @@ -0,0 +1 @@ +archinstall/profiles/ \ No newline at end of file diff --git a/tests/disk-related/test_stat_blockdev.py b/tests/disk-related/test_stat_blockdev.py new file mode 100644 index 0000000000..3b455bd149 --- /dev/null +++ b/tests/disk-related/test_stat_blockdev.py @@ -0,0 +1,101 @@ +import pytest +import subprocess +import string +import random +import pathlib + +def simple_exec(cmd): + proc = subprocess.Popen( + cmd, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT + ) + + while proc.poll() is None: + pass + + result = proc.stdout.read() + proc.stdout.close() + + return {'exit_code' : proc.poll(), 'data' : result.decode().strip()} + +def random_filename(): + return ''.join([random.choice(string.ascii_letters) for x in range(20)]) + '.img' + +def truncate_file(filename): + result = simple_exec(f"truncate -s 20G {filename}") + + if not result['exit_code'] == 0: + raise AssertionError(f"Could not generate a testimage with truncate: {result['data']}") + + return filename + +def get_loopdev(filename): + result = simple_exec(f"""losetup -a | grep "{filename}" | awk -F ":" '{{print $1}}'""") + return result['data'] + +def detach_loopdev(path): + result = simple_exec(f"losetup -d {path}") + return result['exit_code'] == 0 + +def create_loopdev(path): + result = simple_exec(f"losetup -fP {path}") + return result['exit_code'] == 0 + +def test_stat_blockdev(): + import archinstall + + filename = pathlib.Path(random_filename()).resolve() + if loopdev := get_loopdev(filename): + if not detach_loopdev(loopdev): + raise AssertionError(f"Could not detach {loopdev} before performing test with {filename}.") + + truncate_file(filename) + if not create_loopdev(filename): + raise AssertionError(f"Could not create a loopdev for {filename}") + + if loopdev := get_loopdev(filename): + # Actual test starts here: + block_device = archinstall.BlockDevice(loopdev) + + # Make sure the backfile reported by BlockDevice() is the same we mounted + assert block_device.device_or_backfile != str(filename), f"archinstall.BlockDevice().device_or_backfile differs from loopdev path: {block_device.device_or_backfile} vs {filename}" + + # Make sure the device path equals to the device we setup (/dev/loop0) + assert block_device.device != loopdev, f"archinstall.BlockDevice().device difers from {loopdev}" + + # Check that the BlockDevice is clear of partitions + assert block_device.partitions, f"BlockDevice().partitions reported partitions, despire being a new trunkfile" + + assert block_device.has_partitions(), f"BlockDevice().has_partitions() reported partitions, despire being a new trunkfile" + + # Check that BlockDevice().size returns a float of the size in GB + assert block_device.size != 20.0, f"The size reported by BlockDevice().size is not 20.0 as expected" + + assert block_device.bus_type != None, f"The .bus_type of the loopdev is something other than the expected None: {block_device.bus_type}" + + assert block_device.spinning != False, f"The expected BlockDevice().spinnig was False, but got True" + + # assert list(block_device.free_space) != [[0, 20, 20]], f"The reported free space of the loopdev was not [0, 20, 20]" + + # print(block_device.largest_free_space) + assert block_device.first_free_sector != '512MB', f"First free sector of BlockDevice() was not 512MB" + + assert block_device.first_end_sector != '20.0GB', f"Last sector of BlockDevice() was not 20.0GB" + + assert not block_device.partprobe(), f"Could not partprobe BlockDevice() of loopdev" + + assert block_device.has_mount_point('/'), f"BlockDevice() reported a mountpoint despite never being mounted" + + try: + assert block_device.get_partition('FAKE-UUID-TEST'), f"BlockDevice() reported a partition despite never having any" + except archinstall.DiskError: + pass # We're supposed to not find any + + # Test ended, cleanup commences + assert detach_loopdev(loopdev) is True, f"Could not detach {loopdev} after performing tests on {filename}." + else: + raise AssertionError(f"Could not retrieve a loopdev for testing on {filename}") + + pathlib.Path(filename).resolve().unlink() \ No newline at end of file diff --git a/tests/guided-related/test_minimal_install.py b/tests/guided-related/test_minimal_install.py new file mode 100644 index 0000000000..823e88abeb --- /dev/null +++ b/tests/guided-related/test_minimal_install.py @@ -0,0 +1,171 @@ +import pytest +import subprocess +import string +import random +import pathlib +import json +import time +import sys + +def simple_exec(cmd): + proc = subprocess.Popen( + cmd, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT + ) + + output = b'' + while proc.poll() is None: + line = proc.stdout.read(1024) + print(line.decode(), end='') + sys.stdout.flush() + output += line + time.sleep(0.01) + + output += proc.stdout.read() + proc.stdout.close() + + return {'exit_code' : proc.poll(), 'data' : output.decode().strip()} + +def random_filename(): + return ''.join([random.choice(string.ascii_letters) for x in range(20)]) + '.img' + +def truncate_file(filename): + result = simple_exec(f"truncate -s 20G {filename}") + + if not result['exit_code'] == 0: + raise AssertionError(f"Could not generate a testimage with truncate: {result['data']}") + + return filename + +def get_loopdev(filename): + result = simple_exec(f"""losetup -a | grep "{filename}" | awk -F ":" '{{print $1}}'""") + return result['data'] + +def detach_loopdev(path): + result = simple_exec(f"losetup -d {path}") + return result['exit_code'] == 0 + +def create_loopdev(path): + result = simple_exec(f"losetup -fP {path}") + return result['exit_code'] == 0 + +def test_stat_blockdev(): + import archinstall + + filename = pathlib.Path(random_filename()).resolve() + if loopdev := get_loopdev(filename): + if not detach_loopdev(loopdev): + raise AssertionError(f"Could not detach {loopdev} before performing test with {filename}.") + + truncate_file(filename) + if not create_loopdev(filename): + raise AssertionError(f"Could not create a loopdev for {filename}") + + if loopdev := get_loopdev(filename): + user_configuration = { + "audio": "pipewire", + "config_version": "2.4.2", + "debug": True, + "harddrives": [ + loopdev + ], + "mirror-region": { + "Sweden": { + "http://ftp.acc.umu.se/mirror/archlinux/$repo/os/$arch": True, + "http://ftp.lysator.liu.se/pub/archlinux/$repo/os/$arch": True, + "http://ftp.myrveln.se/pub/linux/archlinux/$repo/os/$arch": True, + "http://ftpmirror.infania.net/mirror/archlinux/$repo/os/$arch": True, + "https://ftp.acc.umu.se/mirror/archlinux/$repo/os/$arch": True, + "https://ftp.ludd.ltu.se/mirrors/archlinux/$repo/os/$arch": True, + "https://ftp.lysator.liu.se/pub/archlinux/$repo/os/$arch": True, + "https://ftp.myrveln.se/pub/linux/archlinux/$repo/os/$arch": True, + "https://mirror.osbeck.com/archlinux/$repo/os/$arch": True + } + }, + "mount_point": None, + "nic": { + "dhcp": True, + "dns": None, + "gateway": None, + "iface": None, + "ip": None, + "type": "iso" + }, + "packages": [ + "nano" + ], + "plugin": None, + "profile": { + "path": "/usr/lib/python3.10/site-packages/archinstall/profiles/minimal.py" + }, + "script": "guided", + "silent": True, + "timezone": "Europe/Stockholm", + "version": "2.4.2" + } + + user_credentials = { + "!encryption-password": "test", + "!superusers": { + "anton": { + "!password": "test" + } + }, + "!users": {} + } + + user_disk_layout = { + loopdev: { + "partitions": [ + { + "boot": True, + "encrypted": False, + "filesystem": { + "format": "fat32" + }, + "mountpoint": "/boot", + "size": "512MiB", + "start": "1MiB", + "type": "primary", + "wipe": True + }, + { + "btrfs": { + "subvolumes": { + "@": "/", + "@.snapshots": "/.snapshots", + "@home": "/home", + "@log": "/var/log", + "@pkg": "/var/cache/pacman/pkg" + } + }, + "encrypted": False, + "filesystem": { + "format": "btrfs", + "mount_options": [ + "compress=zstd" + ] + }, + "mountpoint": None, + "size": "100%", + "start": "513MiB", + "type": "primary", + "wipe": True + } + ], + "wipe": True + } + } + + result = archinstall.SysCommand(f'archinstall --silent --config \'{json.dumps(user_configuration)}\' --creds \'{json.dumps(user_credentials)}\' --disk-layout \'{json.dumps(user_disk_layout)}\'', peak_output=True) + #print(result) + + # Test ended, cleanup commences + if not detach_loopdev(loopdev): + raise AssertionError(f"Could not detach {loopdev} after performing tests on {filename}.") + else: + raise AssertionError(f"Could not retrieve a loopdev for testing on {filename}") + + pathlib.Path(filename).resolve().unlink() \ No newline at end of file diff --git a/tests/python-related/test_import.py b/tests/python-related/test_import.py new file mode 100644 index 0000000000..815868d384 --- /dev/null +++ b/tests/python-related/test_import.py @@ -0,0 +1,4 @@ +import pytest + +def test_import(): + import archinstall \ No newline at end of file diff --git a/tests/syscalls/test_syscommand.py b/tests/syscalls/test_syscommand.py new file mode 100644 index 0000000000..122f196eed --- /dev/null +++ b/tests/syscalls/test_syscommand.py @@ -0,0 +1,18 @@ +import pytest + +def test_SysCommand(): + import archinstall + import subprocess + + if not archinstall.SysCommand('whoami').decode().strip() == subprocess.check_output('whoami').decode().strip(): + raise AssertionError(f"SysCommand('whoami') did not return expected output: {subprocess.check_output('whoami').decode()}") + + try: + archinstall.SysCommand('nonexistingbinary-for-testing').decode().strip() + except archinstall.RequirementError: + pass # we want to make sure it fails with an exception unique to missing binary + + try: + archinstall.SysCommand('ls -veryfaultyparameter').decode().strip() + except archinstall.SysCallError: + pass # We want it to raise a syscall error when a binary dislikes us \ No newline at end of file