From ef3c2dbdc44889689eb56956c4fb70e14935cfde Mon Sep 17 00:00:00 2001 From: Lulu Date: Mon, 19 Aug 2024 20:13:48 +0200 Subject: [PATCH] Bootstrap testing - Add a unit test for the baseline configuration - All mocks are extracted into `conftest.py` - Add fixture files for `hardware-configuration.nix` and `kbd-model-map` - Add a nix flake for running and developing the tests - Add instructions to README.md - Configure github actions to run tests on all pull requests --- .github/workflows/test.yml | 13 +++ .gitignore | 3 +- README.md | 11 +- flake.lock | 27 +++++ flake.nix | 34 ++++++ testing/__init__.py | 0 testing/conftest.py | 162 +++++++++++++++++++++++++++++ testing/hardware-configuration.nix | 31 ++++++ testing/kbd-model-map | 72 +++++++++++++ testing/test_baseline.py | 143 +++++++++++++++++++++++++ 10 files changed, 494 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 testing/__init__.py create mode 100644 testing/conftest.py create mode 100644 testing/hardware-configuration.nix create mode 100644 testing/kbd-model-map create mode 100644 testing/test_baseline.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..029d32a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,13 @@ +name: "Test the nixos Python job module" +on: + pull_request: + push: +jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@v27 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + - run: nix run diff --git a/.gitignore b/.gitignore index c5cd571..bdd0802 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ build/ *.kdev4 *.qmlc -.vscode/ \ No newline at end of file +.vscode/ +**/__pycache__/ diff --git a/README.md b/README.md index b1d7bb2..a93d842 100644 --- a/README.md +++ b/README.md @@ -29,4 +29,13 @@ Images stored in [config/images](config/images) are licensed under [CC-BY-SA-4.0 Images [gfx-landing-declarative.png](branding/nixos/gfx-landing-declarative.png), [gfx-landing-reliable.png](branding/nixos/gfx-landing-reliable.png), and [gfx-landing-reproducible.png](branding/nixos/gfx-landing-reproducible.png) are licensed under [CC-BY-SA-4.0](LICENSES/CC-BY-SA-4.0.txt) -Images [nix-snowflake.svg](branding/nixos/nix-snowflake.svg) and [white.png](branding/nixos/white.png) are licensed under [CC-BY-4.0](LICENSES/CC-BY-4.0.txt) \ No newline at end of file +Images [nix-snowflake.svg](branding/nixos/nix-snowflake.svg) and [white.png](branding/nixos/white.png) are licensed under [CC-BY-4.0](LICENSES/CC-BY-4.0.txt) + +## Tests + +- The `nixos` Python job module is has unit tests in [testing/](https://github.com/NixOS/calamares-nixos-extensions/tree/calamares/testing). + +These tests can be executed with the command: +```sh +$ nix run . +``` diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..d1c19fb --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1723991338, + "narHash": "sha256-Grh5PF0+gootJfOJFenTTxDTYPidA3V28dqJ/WV7iis=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "8a3354191c0d7144db9756a74755672387b702ba", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..c564aa6 --- /dev/null +++ b/flake.nix @@ -0,0 +1,34 @@ +{ + description = "Testing calamares-nixos-extensions"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = { nixpkgs, ... }: + let + system = "x86_64-linux"; + + pkgs = import nixpkgs { + inherit system; + }; + + packages = [ + (pkgs.python3.withPackages (pp: with pp; [ pytest pytest-mock ])) + ]; + in + { + packages.${system}.default = pkgs.writeShellApplication { + name = "test-nixos-install"; + runtimeInputs = packages; + text = '' + #!${pkgs.stdenv.shell} + pytest -vv testing + ''; + }; + + devShells.${system}.default = pkgs.mkShell { + inherit packages; + }; + }; +} diff --git a/testing/__init__.py b/testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testing/conftest.py b/testing/conftest.py new file mode 100644 index 0000000..7ae72d9 --- /dev/null +++ b/testing/conftest.py @@ -0,0 +1,162 @@ +import os +import sys + +import pytest + + +@pytest.fixture +def mock_translation_gettext(mocker): + return mocker.Mock( + "gettext.translation().gettext", + # Return the translation key as the translation + side_effect=lambda t: t, + ) + + +@pytest.fixture +def mock_gettext_translation(mocker, mock_translation_gettext): + mock_translation_object = mocker.Mock("gettext.translation()") + mock_translation_object.gettext = mock_translation_gettext + + return mocker.Mock("gettext.translation", return_value=mock_translation_object) + + +@pytest.fixture +def globalstorage(): + return { + "rootMountPoint": "/mnt/root", + "firmwareType": "efi", + "partitions": [], + "keyboardLayout": "us", + "username": "username", + "fullname": "fullname", + } + + +@pytest.fixture +def mock_check_output(mocker): + return mocker.Mock(name="subprocess.check_output") + + +@pytest.fixture +def mock_getoutput(mocker): + return mocker.Mock( + name="subprocess.getoutput", + # subprocess.getoutput() is only called to get the output of `nixos-version` so it is hard-coded here. + return_value="24.05.20240815.c3d4ac7 (Uakari)", + ) + + +@pytest.fixture +def mock_Popen(mocker): + mock_Popen_inst = mocker.Mock("Popen()") + mock_Popen_inst.stdout = mocker.Mock("Popen().stdout") + mock_Popen_inst.stdout.readline = mocker.Mock( + "Popen().stdout.readline", + # Make Popen print nothing (empty bytes) to stdout + return_value=b"", + ) + mock_Popen_inst.wait = mocker.Mock( + "Popen().wait", + # Make Popen().wait() give a returncode of 0 + return_value=0, + ) + return mocker.Mock(name="subprocess.Popen", return_value=mock_Popen_inst) + + +@pytest.fixture +def mock_libcalamares(mocker, globalstorage): + mock_libcalamares = mocker.Mock("libcalamares") + + mock_libcalamares.globalstorage = mocker.Mock("libcalamares.globalstorage") + mock_libcalamares.globalstorage.value = mocker.Mock( + "libcalamares.globalstorage.value" + ) + mock_libcalamares.globalstorage.value.side_effect = lambda k: globalstorage.get(k) + + mock_libcalamares.utils = mocker.Mock("libcalamares.utils") + mock_libcalamares.utils.gettext = mocker.Mock("libcalamares.utils.gettext") + mock_libcalamares.utils.gettext_path = mocker.Mock( + "libcalamares.utils.gettext_path" + ) + mock_libcalamares.utils.gettext_languages = mocker.Mock( + "libcalamares.utils.gettext_languages" + ) + mock_libcalamares.utils.warning = mocker.Mock("libcalamares.utils.warning") + mock_libcalamares.utils.debug = mocker.Mock("libcalamares.utils.debug") + mock_libcalamares.utils.host_env_process_output = mocker.Mock( + "libcalamares.utils.host_env_process_output" + ) + + mock_libcalamares.job = mocker.Mock("libcalamares.job") + mock_libcalamares.job.setprogress = mocker.Mock("libcalamares.job.setprogress") + + return mock_libcalamares + + +@pytest.fixture +def mock_open_hwconf(mocker): + return mocker.Mock('open("hardware-configuration.nix")') + + +@pytest.fixture +def mock_open_kbdmodelmap(mocker): + return mocker.Mock('open("kbd-model-map")') + + +@pytest.fixture +def mock_open(mocker, mock_open_hwconf, mock_open_kbdmodelmap): + testing_dir = os.path.dirname(__file__) + + hwconf_txt = "" + with open(os.path.join(testing_dir, "hardware-configuration.nix"), "r") as hwconf: + hwconf_txt = hwconf.read() + + kbdmodelmap_txt = "" + with open(os.path.join(testing_dir, "kbd-model-map"), "r") as kbdmodelmap: + kbdmodelmap_txt = kbdmodelmap.read() + + mock_open = mocker.Mock("open") + + def fake_open(*args): + file, mode, *_ = args + + assert mode == "r", "open() called without the 'r' mode" + + if file.endswith("hardware-configuration.nix"): + return mocker.mock_open(mock=mock_open_hwconf, read_data=hwconf_txt)(*args) + elif file.endswith("kbd-model-map"): + return mocker.mock_open( + mock=mock_open_kbdmodelmap, read_data=kbdmodelmap_txt + )(*args) + else: + raise AssertionError(f"open() called with unexpected file '{file}'") + + mock_open.side_effect = fake_open + + return mock_open + + +@pytest.fixture +def run( + mocker, + mock_gettext_translation, + mock_libcalamares, + mock_check_output, + mock_getoutput, + mock_Popen, + mock_open, +): + sys.modules["libcalamares"] = mock_libcalamares + + mocker.patch("gettext.translation", mock_gettext_translation) + + mocker.patch("subprocess.check_output", mock_check_output) + mocker.patch("subprocess.getoutput", mock_getoutput) + mocker.patch("subprocess.Popen", mock_Popen) + + mocker.patch("builtins.open", mock_open) + + from modules.nixos.main import run + + return run diff --git a/testing/hardware-configuration.nix b/testing/hardware-configuration.nix new file mode 100644 index 0000000..42d8f21 --- /dev/null +++ b/testing/hardware-configuration.nix @@ -0,0 +1,31 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = + [ (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "usbhid" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-amd" ]; + boot.extraModulePackages = [ ]; + + swapDevices = [ ]; + + # Enables DHCP on each ethernet and wireless interface. In case of scripted networking + # (the default) this is the recommended approach. When using systemd-networkd it's + # still possible to use this option, but it's recommended to use it in conjunction + # with explicit per-interface declarations with `networking.interfaces..useDHCP`. + networking.useDHCP = lib.mkDefault true; + # networking.interfaces.docker0.useDHCP = lib.mkDefault true; + # networking.interfaces.veth1a64ca3.useDHCP = lib.mkDefault true; + # networking.interfaces.vethb5290db.useDHCP = lib.mkDefault true; + # networking.interfaces.vethf60304e.useDHCP = lib.mkDefault true; + # networking.interfaces.wlp2s0.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/testing/kbd-model-map b/testing/kbd-model-map new file mode 100644 index 0000000..279d1a3 --- /dev/null +++ b/testing/kbd-model-map @@ -0,0 +1,72 @@ +# Originally generated from system-config-keyboard's model list. +# consolelayout xlayout xmodel xvariant xoptions +sg ch pc105 de_nodeadkeys terminate:ctrl_alt_bksp +nl nl pc105 - terminate:ctrl_alt_bksp +mk-utf mk,us pc105 - terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll +trq tr pc105 - terminate:ctrl_alt_bksp +uk gb pc105 - terminate:ctrl_alt_bksp +is-latin1 is pc105 - terminate:ctrl_alt_bksp +de de pc105 - terminate:ctrl_alt_bksp +la-latin1 latam pc105 - terminate:ctrl_alt_bksp +us us pc105+inet - terminate:ctrl_alt_bksp +ko kr pc105 - terminate:ctrl_alt_bksp +ro-std ro pc105 std terminate:ctrl_alt_bksp +de-latin1 de pc105 - terminate:ctrl_alt_bksp +slovene si pc105 - terminate:ctrl_alt_bksp +hu hu pc105 - terminate:ctrl_alt_bksp +jp106 jp jp106 - terminate:ctrl_alt_bksp +croat hr pc105 - terminate:ctrl_alt_bksp +it2 it pc105 - terminate:ctrl_alt_bksp +hu101 hu pc105 qwerty terminate:ctrl_alt_bksp +sr-latin rs pc105 latin terminate:ctrl_alt_bksp +fi fi pc105 - terminate:ctrl_alt_bksp +fr_CH ch pc105 fr terminate:ctrl_alt_bksp +dk-latin1 dk pc105 - terminate:ctrl_alt_bksp +fr fr pc105 - terminate:ctrl_alt_bksp +it it pc105 - terminate:ctrl_alt_bksp +ua-utf ua,us pc105 - terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll +fr-latin1 fr pc105 - terminate:ctrl_alt_bksp +sg-latin1 ch pc105 de_nodeadkeys terminate:ctrl_alt_bksp +be-latin1 be pc105 - terminate:ctrl_alt_bksp +dk dk pc105 - terminate:ctrl_alt_bksp +fr-pc fr pc105 - terminate:ctrl_alt_bksp +bg_pho-utf8 bg,us pc105 ,phonetic terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll +it-ibm it pc105 - terminate:ctrl_alt_bksp +cz-us-qwertz cz,us pc105 - terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll +cz-qwerty cz,us pc105 qwerty, terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll +br-abnt2 br abnt2 - terminate:ctrl_alt_bksp +ro ro pc105 - terminate:ctrl_alt_bksp +us-acentos us pc105 intl terminate:ctrl_alt_bksp +pt-latin1 pt pc105 - terminate:ctrl_alt_bksp +ro-std-cedilla ro pc105 std_cedilla terminate:ctrl_alt_bksp +tj_alt-UTF8 tj pc105 - terminate:ctrl_alt_bksp +de-latin1-nodeadkeys de pc105 nodeadkeys terminate:ctrl_alt_bksp +no no pc105 - terminate:ctrl_alt_bksp +bg_bds-utf8 bg,us pc105 - terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll +dvorak us pc105 dvorak terminate:ctrl_alt_bksp +dvorak us pc105 dvorak-alt-intl terminate:ctrl_alt_bksp +ru ru,us pc105 - terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll +cz-lat2 cz pc105 qwerty terminate:ctrl_alt_bksp +pl2 pl pc105 - terminate:ctrl_alt_bksp +es es pc105 - terminate:ctrl_alt_bksp +ro-cedilla ro pc105 cedilla terminate:ctrl_alt_bksp +ie ie pc105 - terminate:ctrl_alt_bksp +et ee pc105 - terminate:ctrl_alt_bksp +sk-qwerty sk pc105 qwerty terminate:ctrl_alt_bksp +sk-qwertz sk pc105 - terminate:ctrl_alt_bksp +fr-latin9 fr pc105 latin9 terminate:ctrl_alt_bksp +fr_CH-latin1 ch pc105 fr terminate:ctrl_alt_bksp +cf ca pc105 - terminate:ctrl_alt_bksp +sv-latin1 se pc105 - terminate:ctrl_alt_bksp +sr-cy rs pc105 - terminate:ctrl_alt_bksp +gr gr,us pc105 - terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll +by by,us pc105 - terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll +il il pc105 - terminate:ctrl_alt_bksp +kazakh kz,us pc105 - terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll +lt.baltic lt pc105 - terminate:ctrl_alt_bksp +lt.l4 lt pc105 - terminate:ctrl_alt_bksp +lt lt pc105 - terminate:ctrl_alt_bksp +khmer kh,us pc105 - terminate:ctrl_alt_bksp +es-dvorak es microsoftpro dvorak terminate:ctrl_alt_bksp +lv lv pc105 apostrophe terminate:ctrl_alt_bksp +lv-tilde lv pc105 tilde terminate:ctrl_alt_bksp diff --git a/testing/test_baseline.py b/testing/test_baseline.py new file mode 100644 index 0000000..86bd29d --- /dev/null +++ b/testing/test_baseline.py @@ -0,0 +1,143 @@ +import subprocess + + +BASELINE_CFG = """# Edit this configuration file to define what should be installed on +# your system. Help is available in the configuration.nix(5) man page +# and in the NixOS manual (accessible by running ‘nixos-help’). + +{ config, pkgs, ... }: + +{ + imports = + [ # Include the results of the hardware scan. + ./hardware-configuration.nix + ]; + + # Bootloader. + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + + networking.hostName = "nixos"; # Define your hostname. + # networking.wireless.enable = true; # Enables wireless support via wpa_supplicant. + + # Configure network proxy if necessary + # networking.proxy.default = "http://user:password@proxy:port/"; + # networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain"; + + # Enable networking + networking.networkmanager.enable = true; + + # Define a user account. Don't forget to set a password with ‘passwd’. + users.users.username = { + isNormalUser = true; + description = "fullname"; + extraGroups = [ "networkmanager" "wheel" ]; + packages = with pkgs; [ + # thunderbird + ]; + }; + + # Install firefox. + programs.firefox.enable = true; + + # List packages installed in system profile. To search, run: + # $ nix search wget + environment.systemPackages = with pkgs; [ + # vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default. + # wget + ]; + + # Some programs need SUID wrappers, can be configured further or are + # started in user sessions. + # programs.mtr.enable = true; + # programs.gnupg.agent = { + # enable = true; + # enableSSHSupport = true; + # }; + + # List services that you want to enable: + + # Enable the OpenSSH daemon. + # services.openssh.enable = true; + + # Open ports in the firewall. + # networking.firewall.allowedTCPPorts = [ ... ]; + # networking.firewall.allowedUDPPorts = [ ... ]; + # Or disable the firewall altogether. + # networking.firewall.enable = false; + + # This value determines the NixOS release from which the default + # settings for stateful data, like file locations and database versions + # on your system were taken. It‘s perfectly fine and recommended to leave + # this value at the release version of the first install of this system. + # Before changing this value read the documentation for this option + # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html). + system.stateVersion = "24.05"; # Did you read the comment? + +} +""" + + +def test_baseline( + mocker, + run, + mock_gettext_translation, + mock_libcalamares, + mock_getoutput, + mock_check_output, + mock_open_hwconf, + mock_Popen, +): + result = run() + + assert result is None, "nixos-install failed." + + mock_gettext_translation.assert_called_once_with( + "calamares-python", localedir=mocker.ANY, languages=mocker.ANY, fallback=True + ) + + # libcalamares.job.setprogress(0.1) + assert mock_libcalamares.job.setprogress.mock_calls[0] == mocker.call(0.1) + + # libcalamares.job.setprogress(0.18) + assert mock_libcalamares.job.setprogress.mock_calls[1] == mocker.call(0.18) + + # version = ".".join(subprocess.getoutput( + # ["nixos-version"]).split(".")[:2])[:5] + assert mock_getoutput.mock_calls[0] == mocker.call(["nixos-version"]) + + # The baseline configuration should not raise any warnings. + mock_libcalamares.utils.warning.assert_not_called() + + # libcalamares.job.setprogress(0.25) + assert mock_libcalamares.job.setprogress.mock_calls[2] == mocker.call(0.25) + + # subprocess.check_output( + # ["pkexec", "nixos-generate-config", "--root", root_mount_point], stderr=subprocess.STDOUT) + assert mock_check_output.mock_calls[0] == mocker.call( + ["pkexec", "nixos-generate-config", "--root", "/mnt/root"], + stderr=subprocess.STDOUT, + ) + + # hf = open(root_mount_point + "/etc/nixos/hardware-configuration.nix", "r") + mock_open_hwconf.assert_called_once_with( + "/mnt/root/etc/nixos/hardware-configuration.nix", "r" + ) + + # libcalamares.utils.host_env_process_output( + # ["cp", "/dev/stdin", config], None, cfg) + mock_libcalamares.utils.host_env_process_output.assert_called_once_with( + ["cp", "/dev/stdin", "/mnt/root/etc/nixos/configuration.nix"], None, mocker.ANY + ) + cfg = mock_libcalamares.utils.host_env_process_output.call_args[0][2] + assert cfg == BASELINE_CFG + + # libcalamares.job.setprogress(0.3) + assert mock_libcalamares.job.setprogress.mock_calls[3] == mocker.call(0.3) + + # proc = subprocess.Popen(["pkexec", "nixos-install", "--no-root-passwd", "--root", root_mount_point], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + mock_Popen.assert_called_once_with( + ["pkexec", "nixos-install", "--no-root-passwd", "--root", "/mnt/root"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + )