1{ pkgs, lib, ... }: 2{ 3 name = "gnome-extensions"; 4 meta.maintainers = [ ]; 5 6 node.pkgsReadOnly = false; 7 8 nodes.machine = 9 { pkgs, ... }: 10 { 11 imports = [ ./common/user-account.nix ]; 12 13 # Install all extensions 14 environment.systemPackages = lib.filter (e: e ? extensionUuid) ( 15 lib.attrValues pkgs.gnomeExtensions 16 ); 17 18 # Some extensions are broken, but that's kind of the point of a testing VM 19 nixpkgs.config.allowBroken = true; 20 # There are some aliases which throw exceptions; ignore them. 21 # Also prevent duplicate extensions under different names. 22 nixpkgs.config.allowAliases = false; 23 24 # Configure GDM 25 services.xserver.enable = true; 26 services.xserver.displayManager.gdm = { 27 enable = true; 28 debug = true; 29 wayland = true; 30 }; 31 services.displayManager.autoLogin = { 32 enable = true; 33 user = "alice"; 34 }; 35 36 # Configure Gnome 37 services.desktopManager.gnome.enable = true; 38 services.desktopManager.gnome.debug = true; 39 40 systemd.user.services = { 41 "org.gnome.Shell@wayland" = { 42 serviceConfig = { 43 ExecStart = [ 44 # Clear the list before overriding it. 45 "" 46 # Eval API is now internal so Shell needs to run in unsafe mode. 47 # TODO: improve test driver so that it supports openqa-like manipulation 48 # that would allow us to drop this mess. 49 "${pkgs.gnome-shell}/bin/gnome-shell --unsafe-mode" 50 ]; 51 }; 52 }; 53 }; 54 55 }; 56 57 testScript = 58 { nodes, ... }: 59 let 60 # Keep line widths somewhat manageable 61 user = nodes.machine.users.users.alice; 62 uid = toString user.uid; 63 bus = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${uid}/bus"; 64 # Run a command in the appropriate user environment 65 run = command: "su - ${user.name} -c '${bus} ${command}'"; 66 67 # Call javascript in gnome shell, returns a tuple (success, output), where 68 # `success` is true if the dbus call was successful and output is what the 69 # javascript evaluates to. 70 eval = 71 command: 72 run "gdbus call --session -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval ${command}"; 73 74 # False when startup is done 75 startingUp = eval "Main.layoutManager._startingUp"; 76 77 # Extensions to keep always enabled together 78 # Those are extensions that are usually always on for many users, and that we expect to work 79 # well together with most others without conflicts 80 alwaysOnExtensions = map (name: pkgs.gnomeExtensions.${name}.extensionUuid) [ 81 "applications-menu" 82 "user-themes" 83 ]; 84 85 # Extensions to enable and disable individually 86 # Extensions like dash-to-dock and dash-to-panel cannot be enabled at the same time. 87 testExtensions = map (name: pkgs.gnomeExtensions.${name}.extensionUuid) [ 88 "appindicator" 89 "dash-to-dock" 90 "dash-to-panel" 91 "ddterm" 92 "gsconnect" 93 "system-monitor-next" 94 "desktop-icons-ng-ding" 95 "workspace-indicator" 96 "vitals" 97 ]; 98 in 99 '' 100 with subtest("Login to GNOME with GDM"): 101 # wait for gdm to start 102 machine.wait_for_unit("display-manager.service") 103 # wait for the wayland server 104 machine.wait_for_file("/run/user/${uid}/wayland-0") 105 # wait for alice to be logged in 106 machine.wait_for_unit("default.target", "${user.name}") 107 # check that logging in has given the user ownership of devices 108 assert "alice" in machine.succeed("getfacl -p /dev/snd/timer") 109 110 with subtest("Wait for GNOME Shell"): 111 # correct output should be (true, 'false') 112 machine.wait_until_succeeds( 113 "${startingUp} | grep -q 'true,..false'" 114 ) 115 116 # Close the Activities view so that Shell can correctly track the focused window. 117 machine.send_key("esc") 118 # # Disable extension version validation (only use for manual testing) 119 # machine.succeed( 120 # "${run "gsettings set org.gnome.shell disable-extension-version-validation true"}" 121 # ) 122 123 def getState(extension): 124 return machine.succeed( 125 f"${run "gnome-extensions info {extension}"} | grep '^ State: .*$'" 126 ) 127 128 # Assert that some extension is in a specific state 129 def checkState(target, extension): 130 state = getState(extension) 131 assert target in state, f"{state} instead of {target}" 132 133 def checkExtension(extension, disable): 134 with subtest(f"Enable extension '{extension}'"): 135 # Check that the extension is properly initialized; skip out of date ones 136 state = machine.succeed( 137 f"${run "gnome-extensions info {extension}"} | grep '^ State: .*$'" 138 ) 139 if "OUT OF DATE" in state: 140 machine.log(f"Extension {extension} will be skipped because out of date") 141 return 142 143 assert "INITIALIZED" in state, f"{state} instead of INITIALIZED" 144 145 # Enable and optionally disable 146 147 machine.succeed(f"${run "gnome-extensions enable {extension}"}") 148 wait_time = 5 149 while getState(extension) == "ACTIVATING" and (wait_time := wait_time - 1) > 0: 150 machine.log(f"Extension {extension} is still activating, waiting {wait_time} more seconds") 151 machine.sleep(1) 152 checkState("ACTIVE", extension) 153 154 if disable: 155 machine.succeed(f"${run "gnome-extensions disable {extension}"}") 156 checkState("INACTIVE", extension) 157 '' 158 + lib.concatLines (map (e: ''checkExtension("${e}", False)'') alwaysOnExtensions) 159 + lib.concatLines (map (e: ''checkExtension("${e}", True)'') testExtensions); 160}