mirror of
https://github.com/NixOS/nixpkgs.git
synced 2026-07-06 17:13:24 -05:00
nixosTests/cosmic: move core testing logic into a dedicated script
This commit is contained in:
parent
4ae53d27e5
commit
0ef6027265
3 changed files with 239 additions and 61 deletions
|
|
@ -7,6 +7,30 @@
|
|||
...
|
||||
}:
|
||||
|
||||
let
|
||||
user = config.nodes.machine.users.users.alice;
|
||||
logFilePath = "/home/${user.name}/${testName}";
|
||||
# Use `writeShellScriptBin` instead of `writeShellScript` so that the
|
||||
# process name in the journald log appears as 'cosmicTest[$pid]'
|
||||
cosmicTest = config.node.pkgs.writeShellScriptBin "cosmicTest" ''
|
||||
exec ${lib.getExe config.node.pkgs.python3Minimal} ${./test-script.py} \
|
||||
--log-file-path ${logFilePath} \
|
||||
--cosmic-reader-pdf ${config.node.pkgs.empty-pdf} \
|
||||
--polkit-agent-helper-path ${config.node.pkgs.polkit.out}/lib/polkit-1/polkit-agent-helper-1 \
|
||||
--root-user-password ${user.password} \
|
||||
--ydotool-drv-store-path ${config.node.pkgs.ydotool}
|
||||
'';
|
||||
cosmicTestDesktop = config.node.pkgs.makeDesktopItem {
|
||||
name = "cosmicTest";
|
||||
desktopName = "COSMIC NixOS VM test (${testName})";
|
||||
exec = "cosmicTest";
|
||||
};
|
||||
cosmicTestAutostartItem = config.node.pkgs.makeAutostartItem {
|
||||
name = "cosmicTest";
|
||||
package = cosmicTestDesktop;
|
||||
};
|
||||
in
|
||||
|
||||
{
|
||||
name = testName;
|
||||
|
||||
|
|
@ -30,10 +54,13 @@
|
|||
|
||||
services.displayManager.autoLogin = lib.mkIf enableAutologin {
|
||||
enable = true;
|
||||
user = "alice";
|
||||
user = user.name;
|
||||
};
|
||||
|
||||
environment.systemPackages = with config.node.pkgs; [
|
||||
cosmicTest
|
||||
cosmicTestAutostartItem
|
||||
|
||||
# These two packages are used to check if a window was opened
|
||||
# under the COSMIC session or not. Kinda important.
|
||||
# TODO: Move the check from the test module to
|
||||
|
|
@ -58,27 +85,9 @@
|
|||
|
||||
testScript =
|
||||
{ nodes, ... }:
|
||||
let
|
||||
cfg = nodes.machine;
|
||||
user = cfg.users.users.alice;
|
||||
DISPLAY = lib.strings.optionalString enableXWayland (
|
||||
if enableAutologin then "DISPLAY=:0" else "DISPLAY=:1"
|
||||
);
|
||||
emptyPDF = config.node.pkgs.stdenvNoCC.mkDerivation {
|
||||
name = "empty-pdf";
|
||||
dontUnpack = true;
|
||||
nativeBuildInputs = [ config.node.pkgs.imagemagick ];
|
||||
buildPhase = ''
|
||||
magick xc:none -page Letter empty.pdf
|
||||
'';
|
||||
installPhase = ''
|
||||
mkdir $out
|
||||
mv empty.pdf $out/empty.pdf
|
||||
'';
|
||||
};
|
||||
in
|
||||
''
|
||||
#testName: ${testName}
|
||||
import sys
|
||||
''
|
||||
+ (
|
||||
if enableAutologin then
|
||||
|
|
@ -91,7 +100,7 @@
|
|||
from time import sleep
|
||||
|
||||
machine.wait_for_unit("graphical.target", timeout=120)
|
||||
machine.wait_until_succeeds("pgrep --uid ${toString cfg.users.users.cosmic-greeter.name} --full cosmic-greeter", timeout=30)
|
||||
machine.wait_until_succeeds("pgrep --uid ${config.nodes.machine.users.users.cosmic-greeter.name} --full cosmic-greeter", timeout=30)
|
||||
# Sleep for 10 seconds for ensuring that `greetd` loads the
|
||||
# password prompt for the login screen properly.
|
||||
sleep(10)
|
||||
|
|
@ -101,47 +110,48 @@
|
|||
''
|
||||
)
|
||||
+ ''
|
||||
# _One_ of the final processes to start as part of the
|
||||
# `cosmic-session` target is the Workspaces applet. So, wait
|
||||
# for it to start. The process existing means that COSMIC
|
||||
# now handles any opened windows from now on.
|
||||
machine.wait_until_succeeds("pgrep --uid ${toString user.uid} --full 'cosmic-panel-button com.system76.CosmicWorkspaces'", timeout=30)
|
||||
with subtest("xdg autostart support in cosmic"):
|
||||
# When checking the status of our `cosmicTest` package with:
|
||||
# `machine.wait_for_unit("app-cosmicTest@autostart.service", user="${user.name}")`
|
||||
# We are immediately greeted with the error:
|
||||
# ```
|
||||
# subtest: xdg autostart support in cosmic
|
||||
# machine: waiting for unit app-cosmicTest@autostart.service with user alice
|
||||
# machine # [ 26.497516] cosmic-comp[1352]: [EGL] 0x3008 (BAD_DISPLAY) eglCreateSync: _eglCreateSync
|
||||
# machine # [ 26.511706] su[1416]: Successful su for alice by root
|
||||
# machine # [ 26.528190] su[1416]: pam_unix(su:session): session opened for user alice(uid=1000) by (uid=0)
|
||||
# machine # Failed to connect to user scope bus via local transport: No such file or directory
|
||||
# machine # [ 26.599563] su[1416]: pam_unix(su:session): session closed for user alice
|
||||
# !!! Test "xdg autostart support in cosmic" failed with error: "retrieving systemctl property "ActiveState" for unit "app-cosmicTest@autostart.service" under user "alice" failed with exit code 1"
|
||||
# ```
|
||||
# Meaning, our session is extremely new and the D-Bus user
|
||||
# session socket does not yet exist. Instead, lets poll for
|
||||
# the log file that the test is guaranteed to write to, as
|
||||
# soon as it starts.
|
||||
machine.wait_for_file("${logFilePath}.log", timeout=120)
|
||||
|
||||
# The best way to test for Wayland and XWayland is to launch
|
||||
# the GUI applications and see the results yourself.
|
||||
with subtest("Launch applications"):
|
||||
# key: binary_name
|
||||
# value: "app-id" as reported by `lswt`
|
||||
gui_apps_to_launch = {}
|
||||
exit_code = 0
|
||||
try:
|
||||
machine.wait_for_file("${logFilePath}.done", timeout=700)
|
||||
except Exception:
|
||||
exit_code = 1
|
||||
|
||||
# We want to ensure that the first-party applications
|
||||
# start/launch properly.
|
||||
gui_apps_to_launch['cosmic-edit'] = 'com.system76.CosmicEdit'
|
||||
gui_apps_to_launch['cosmic-files'] = 'com.system76.CosmicFiles'
|
||||
gui_apps_to_launch['cosmic-player'] = 'com.system76.CosmicPlayer'
|
||||
gui_apps_to_launch['cosmic-reader'] = 'com.system76.CosmicReader'
|
||||
gui_apps_to_launch['cosmic-settings'] = 'com.system76.CosmicSettings'
|
||||
gui_apps_to_launch['cosmic-store'] = 'com.system76.CosmicStore'
|
||||
gui_apps_to_launch['cosmic-term'] = 'com.system76.CosmicTerm'
|
||||
|
||||
for gui_app, app_id in gui_apps_to_launch.items():
|
||||
# Don't fail the test if binary is absent
|
||||
if machine.execute(f"su - ${user.name} -c 'command -v {gui_app}'", timeout=5)[0] == 0:
|
||||
match gui_app:
|
||||
case 'cosmic-reader':
|
||||
opt_arg = '${emptyPDF}/empty.pdf'
|
||||
case _:
|
||||
opt_arg = ""
|
||||
|
||||
machine.succeed(f"su - ${user.name} -c 'WAYLAND_DISPLAY=wayland-1 XDG_RUNTIME_DIR=/run/user/${toString user.uid} ${DISPLAY} {gui_app} {opt_arg} >&2 &'", timeout=5)
|
||||
# Nix builds the following non-commented expression to the following:
|
||||
# `su - alice -c 'WAYLAND_DISPLAY=wayland-1 XDG_RUNTIME_DIR=/run/user/1000 lswt --json | jq ".toplevels" | grep "^ \\"app-id\\": \\"{app_id}\\"$"' `
|
||||
machine.wait_until_succeeds(f''''su - ${user.name} -c 'WAYLAND_DISPLAY=wayland-1 XDG_RUNTIME_DIR=/run/user/${toString user.uid} lswt --json | jq ".toplevels" | grep "^ \\"app-id\\": \\"{app_id}\\"$"' '''', timeout=60)
|
||||
machine.succeed(f"pkill {gui_app}", timeout=5)
|
||||
|
||||
machine.succeed("echo 'test completed succeessfully' > /${testName}", timeout=5)
|
||||
machine.copy_from_machine('/${testName}')
|
||||
# The log file is created in the very beginning of the test
|
||||
# script's execution. If we are here, it means that the
|
||||
# `wait_for_unit`'s "guard" on the test script's autostart unit
|
||||
# plus the 630 second combined timeout of other two
|
||||
# `wait_for_file`s, make it extremely likely for the log file to
|
||||
# be present.
|
||||
machine.copy_from_machine("${logFilePath}.log")
|
||||
|
||||
machine.shutdown()
|
||||
|
||||
with open(f"{machine.out_dir}/${testName}.log") as test_log_file:
|
||||
contents = test_log_file.read()
|
||||
print(contents)
|
||||
if any("Z [ERROR] [L:" in line for line in contents.splitlines()):
|
||||
exit_code = 1
|
||||
|
||||
sys.exit(exit_code)
|
||||
'';
|
||||
}
|
||||
|
|
|
|||
166
nixos/tests/cosmic/test-script.py
Normal file
166
nixos/tests/cosmic/test-script.py
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import logging
|
||||
import pathlib
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
def parse_cli_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--log-file-path",
|
||||
required=True,
|
||||
type=str,
|
||||
help="The path to the log file (without the '.log' suffix/extension)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cosmic-reader-pdf",
|
||||
required=True,
|
||||
type=str,
|
||||
help="The PDF that the `cosmic-reader` should open for testing",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def wait_for_cosmic_de_readiness() -> None:
|
||||
"""
|
||||
Wait for the COSMIC DE to be ready, before running the tests. This
|
||||
is done by waiting on the supposedly last component of the COSMIC
|
||||
DE to be "ready." That component is the notification watcher, of
|
||||
the `cosmic-applet` derivation.
|
||||
"""
|
||||
logging.info("=" * 80)
|
||||
logging.info("Waiting for COSMIC DE to complete initialization")
|
||||
|
||||
notification_watcher_wait_deadline = time.monotonic() + 360
|
||||
notification_watcher_exists = False
|
||||
while time.monotonic() < notification_watcher_wait_deadline:
|
||||
busctl_process = subprocess.run(
|
||||
["busctl", "--user", "status", "com.system76.CosmicStatusNotifierWatcher"],
|
||||
check=False,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
if busctl_process.returncode == 0:
|
||||
notification_watcher_exists = True
|
||||
break
|
||||
else:
|
||||
time.sleep(1)
|
||||
logging_msg = "The COSMIC DE is "
|
||||
if notification_watcher_exists:
|
||||
logging.info(f"{logging_msg} ready")
|
||||
else:
|
||||
logging.error(f"{logging_msg} not ready")
|
||||
return
|
||||
|
||||
|
||||
def perform_gui_application_test(cli_args: argparse.Namespace) -> None:
|
||||
"""
|
||||
1. Start one GUI application as a background process.
|
||||
2. Wait unil it has been confimred that the GUI application is
|
||||
running.
|
||||
3. Kill the background process of the GUI application.
|
||||
|
||||
Any breakage in this flow is considered a failure of the test for
|
||||
the GUI application.
|
||||
"""
|
||||
logging.info("=" * 80)
|
||||
logging.info("Performing test to launch GUI applications")
|
||||
|
||||
gui_apps_to_test = {
|
||||
"com.system76.CosmicEdit": [
|
||||
"cosmic-edit",
|
||||
],
|
||||
"com.system76.CosmicFiles": [
|
||||
"cosmic-files",
|
||||
],
|
||||
"com.system76.CosmicPlayer": [
|
||||
"cosmic-player",
|
||||
],
|
||||
"com.system76.CosmicReader": [
|
||||
"cosmic-reader",
|
||||
cli_args.cosmic_reader_pdf,
|
||||
],
|
||||
"com.system76.CosmicSettings": [
|
||||
"cosmic-settings",
|
||||
],
|
||||
"com.system76.CosmicStore": [
|
||||
"cosmic-store",
|
||||
],
|
||||
"com.system76.CosmicTerm": [
|
||||
"cosmic-term",
|
||||
],
|
||||
}
|
||||
|
||||
for gui_app_id, gui_app_command in gui_apps_to_test.items():
|
||||
logging.info(f"Running: {gui_app_command}")
|
||||
gui_app_bg_process = subprocess.Popen(
|
||||
gui_app_command,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
gui_app_bg_process_deadline = time.monotonic() + 30
|
||||
gui_app_is_running = False
|
||||
|
||||
while time.monotonic() < gui_app_bg_process_deadline and not gui_app_is_running:
|
||||
lswt_process = subprocess.run(
|
||||
[
|
||||
"lswt",
|
||||
"--custom",
|
||||
"a",
|
||||
],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
lswt_process_stdout = lswt_process.stdout.strip()
|
||||
if lswt_process_stdout:
|
||||
if gui_app_id in lswt_process_stdout.splitlines():
|
||||
gui_app_is_running = True
|
||||
time.sleep(1)
|
||||
pkill_process = subprocess.run(
|
||||
["pkill", gui_app_command[0]],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False,
|
||||
)
|
||||
|
||||
log_message = (
|
||||
f"The GUI application test for '{gui_app_command[0]}' ({gui_app_id})"
|
||||
)
|
||||
if gui_app_is_running:
|
||||
logging.info(f"{log_message} passed")
|
||||
else:
|
||||
logging.error(f"{log_message} failed")
|
||||
return
|
||||
|
||||
|
||||
def main() -> None:
|
||||
cli_args = parse_cli_args()
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format=f"%(asctime)sZ [%(levelname)s] [L:%(lineno)d] %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
logging.FileHandler(f"{cli_args.log_file_path}.log", mode="w"),
|
||||
],
|
||||
)
|
||||
logging.Formatter.converter = time.gmtime
|
||||
logging.info(f"Logging to '{cli_args.log_file_path}.log'")
|
||||
|
||||
# Wait for the DE to be ready
|
||||
wait_for_cosmic_de_readiness()
|
||||
|
||||
# tests go here
|
||||
perform_gui_application_test(cli_args)
|
||||
|
||||
pathlib.Path(f"{cli_args.log_file_path}.done").touch()
|
||||
return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -6,7 +6,9 @@
|
|||
|
||||
stdenvNoCC.mkDerivation {
|
||||
name = "empty-pdf";
|
||||
|
||||
__structuredAttrs = true;
|
||||
strictDeps = true;
|
||||
|
||||
dontUnpack = true;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue