From ccb1a8c437fa40760899ae95f9d3f10ed7c8b41b Mon Sep 17 00:00:00 2001 From: Ludovic Courtès Date: Wed, 19 Feb 2020 23:25:58 +0100 Subject: tests: install: Add "gui-installed-os". * gnu/installer/tests.scm: New file. * gnu/local.mk (INSTALLER_MODULES): Add it. * gnu/tests/install.scm (run-install): Add #:gui-test. Add (gnu installer tests) to the marionette imported modules. Honor GUI-TEST. Check whether SCRIPT is true. (%root-password, %syslog-conf): New variable. (operating-system-with-console-syslog, gui-test-program) (guided-installation-test): New procedures. (%extra-packages, installation-os-for-gui-tests) (%test-gui-installed-os): New variable. --- gnu/installer/tests.scm | 340 ++++++++++++++++++++++++++++++++++++++++++++++++ gnu/local.mk | 3 +- gnu/tests/install.scm | 202 ++++++++++++++++++++++++++-- 3 files changed, 536 insertions(+), 9 deletions(-) create mode 100644 gnu/installer/tests.scm diff --git a/gnu/installer/tests.scm b/gnu/installer/tests.scm new file mode 100644 index 0000000000..6f5393e3ab --- /dev/null +++ b/gnu/installer/tests.scm @@ -0,0 +1,340 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2020 Ludovic Courtès +;;; +;;; This file is part of GNU Guix. +;;; +;;; GNU Guix is free software; you can redistribute it and/or modify it +;;; under the terms of the GNU General Public License as published by +;;; the Free Software Foundation; either version 3 of the License, or (at +;;; your option) any later version. +;;; +;;; GNU Guix is distributed in the hope that it will be useful, but +;;; WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;; GNU General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with GNU Guix. If not, see . + +(define-module (gnu installer tests) + #:use-module (srfi srfi-1) + #:use-module (srfi srfi-34) + #:use-module (srfi srfi-35) + #:use-module (ice-9 match) + #:use-module (ice-9 regex) + #:use-module (ice-9 pretty-print) + #:export (&pattern-not-matched + pattern-not-matched? + + %installer-socket-file + open-installer-socket + + converse + conversation-log-port + + choose-locale+keyboard + enter-host-name+passwords + choose-services + choose-partitioning + conclude-installation + + edit-configuration-file)) + +;;; Commentary: +;;; +;;; This module provides tools to test the guided "graphical" installer in a +;;; non-interactive fashion. The core of it is 'converse': it allows you to +;;; state Expect-style dialogues, which happen over the Unix-domain socket the +;;; installer listens to. Higher-level procedures such as +;;; 'choose-locale+keyboard' are provided to perform specific parts of the +;;; dialogue. +;;; +;;; Code: + +(define %installer-socket-file + ;; Socket the installer listens to. + "/var/guix/installer-socket") + +(define* (open-installer-socket #:optional (file %installer-socket-file)) + "Return a socket connected to the installer." + (let ((sock (socket AF_UNIX SOCK_STREAM 0))) + (connect sock AF_UNIX file) + sock)) + +(define-condition-type &pattern-not-matched &error + pattern-not-matched? + (pattern pattern-not-matched-pattern) + (sexp pattern-not-matched-sexp)) + +(define (pattern-error pattern sexp) + (raise (condition + (&pattern-not-matched + (pattern pattern) (sexp sexp))))) + +(define conversation-log-port + ;; Port where debugging info is logged + (make-parameter (current-error-port))) + +(define (converse-debug pattern) + (format (conversation-log-port) + "conversation expecting pattern ~s~%" + pattern)) + +(define-syntax converse + (lambda (s) + "Convert over PORT: read sexps from there, match them against each +PATTERN, and send the corresponding REPLY. Raise to '&pattern-not-matched' +when one of the PATTERNs is not matched." + + ;; XXX: Strings that appear in PATTERNs must be in the language the + ;; installer is running in. In the future, we should add support to allow + ;; writing English strings in PATTERNs and have the pattern matcher + ;; automatically translate them. + + ;; Here we emulate 'pmatch' syntax on top of 'match'. This is ridiculous + ;; but that's because 'pmatch' compares objects with 'eq?', making it + ;; pretty useless, and it doesn't support ellipses and such. + + (define (quote-pattern s) + ;; Rewrite the pattern S from pmatch style (a ,b) to match style like + ;; ('a b). + (with-ellipsis ::: + (syntax-case s (unquote _ ...) + ((unquote id) #'id) + (_ #'_) + (... #'...) + (id + (identifier? #'id) + #''id) + ((lst :::) (map quote-pattern #'(lst :::))) + (pattern #'pattern)))) + + (define (match-pattern s) + ;; Match one pattern without a guard. + (syntax-case s () + ((port (pattern reply) continuation) + (with-syntax ((pattern (quote-pattern #'pattern))) + #'(let ((pat 'pattern)) + (converse-debug pat) + (match (read port) + (pattern + (let ((data (call-with-values (lambda () reply) + list))) + (for-each (lambda (obj) + (write obj port) + (newline port)) + data) + (force-output port) + (continuation port))) + (sexp + (pattern-error pat sexp)))))))) + + (syntax-case s () + ((_ port (pattern reply) rest ...) + (match-pattern #'(port (pattern reply) + (lambda (port) + (converse port rest ...))))) + ((_ port (pattern guard reply) rest ...) + #`(let ((skip? (not guard)) + (next (lambda (p) + (converse p rest ...)))) + (if skip? + (next port) + #,(match-pattern #'(port (pattern reply) next))))) + ((_ port) + #t)))) + +(define* (choose-locale+keyboard port + #:key + (language "English") + (location "Hong Kong") + (timezone '("Europe" "Zagreb")) + (keyboard + '("English (US)" + "English (intl., with AltGr dead keys)"))) + "Converse over PORT with the guided installer to choose the specified +LANGUAGE, LOCATION, TIMEZONE, and KEYBOARD." + (converse port + ((list-selection (title "Locale language") + (multiple-choices? #f) + (items _)) + language) + ((list-selection (title "Locale location") + (multiple-choices? #f) + (items _)) + location) + ((menu (title "GNU Guix install") + (text _) + (items (,guided _ ...))) ;"Guided graphical installation" + guided) + ((list-selection (title "Timezone") + (multiple-choices? #f) + (items _)) + (first timezone)) + ((list-selection (title "Timezone") + (multiple-choices? #f) + (items _)) + (second timezone)) + ((list-selection (title "Layout") + (multiple-choices? #f) + (items _)) + (first keyboard)) + ((list-selection (title "Variant") + (multiple-choices? #f) + (items _)) + (second keyboard)))) + +(define* (enter-host-name+passwords port + #:key + (host-name "guix") + (root-password "foo") + (users '(("alice" "pass1") + ("bob" "pass2") + ("charlie" "pass3")))) + "Converse over PORT with the guided installer to choose HOST-NAME, +ROOT-PASSWORD, and USERS." + (converse port + ((input (title "Hostname") (text _) (default _)) + host-name) + ((input (title "System administrator password") (text _) (default _)) + root-password) + ((input (title "Password confirmation required") (text _) (default _)) + root-password) + ((add-users) + (match users + (((names passwords) ...) + (map (lambda (name password) + `(user (name ,name) (real-name ,(string-titlecase name)) + (home-directory ,(string-append "/home/" name)) + (password ,password))) + names passwords)))))) + +(define* (choose-services port + #:key + (desktop-environments '("GNOME")) + (choose-network-service? + (lambda (service) + (or (string-contains service "SSH") + (string-contains service "NSS")))) + (choose-network-management-tool? + (lambda (service) + (string-contains service "DHCP")))) + "Converse over PORT to choose networking services." + (converse port + ((checkbox-list (title "Desktop environment") (text _) + (items _)) + desktop-environments) + ((checkbox-list (title "Network service") (text _) + (items ,services)) + (filter choose-network-service? services)) + + ;; The "Network management" dialog shows up only when no desktop + ;; environments have been selected, hence the guard. + ((list-selection (title "Network management") + (multiple-choices? #f) + (items ,services)) + (null? desktop-environments) + (find choose-network-management-tool? services)))) + +(define (edit-configuration-file file) + "Edit FILE, an operating system configuration file generated by the +installer, by adding a marionette service such that the installed OS is +instrumented for further testing." + (define (read-expressions port) + (let loop ((result '())) + (match (read port) + ((? eof-object?) + (reverse result)) + (exp + (loop (cons exp result)))))) + + (define (edit exp) + (match exp + (('operating-system _ ...) + `(marionette-operating-system ,exp + #:imported-modules + '((gnu services herd) + (guix build utils) + (guix combinators)))) + (_ + exp))) + + (let ((content (call-with-input-file file read-expressions))) + (call-with-output-file file + (lambda (port) + (format port "\ +;; Operating system configuration edited for automated testing.~%~%") + + (pretty-print '(use-modules (gnu tests)) port) + (for-each (lambda (exp) + (pretty-print (edit exp) port) + (newline port)) + content))) + + #t)) + +(define* (choose-partitioning port + #:key + (encrypted? #t) + (passphrase "thepassphrase") + (edit-configuration-file + edit-configuration-file)) + "Converse over PORT to choose the partitioning method. When ENCRYPTED? is +true, choose full-disk encryption with PASSPHRASE as the LUKS passphrase. +This conversation goes past the final dialog box that shows the configuration +file, actually starting the installation process." + (converse port + ((list-selection (title "Partitioning method") + (multiple-choices? #f) + (items (,not-encrypted ,encrypted _ ...))) + (if encrypted? + encrypted + not-encrypted)) + ((list-selection (title "Disk") (multiple-choices? #f) + (items (,disk _ ...))) + disk) + + ;; The "Partition table" dialog pops up only if there's not already a + ;; partition table. + ((list-selection (title "Partition table") + (multiple-choices? #f) + (items _)) + "gpt") + ((list-selection (title "Partition scheme") + (multiple-choices? #f) + (items (,one-partition _ ...))) + one-partition) + ((list-selection (title "Guided partitioning") + (multiple-choices? #f) + (items (,disk _ ...))) + disk) + ((input (title "Password required") + (text _) (default #f)) + encrypted? ;only when ENCRYPTED? + passphrase) + ((input (title "Password confirmation required") + (text _) (default #f)) + encrypted? + passphrase) + ((confirmation (title "Format disk?") (text _)) + #t) + ((info (title "Preparing partitions") _ ...) + (values)) ;nothing to return + ((file-dialog (title "Configuration file") + (text _) + (file ,configuration-file)) + (edit-configuration-file configuration-file)))) + +(define (conclude-installation port) + "Conclude the installation by checking over PORT that we get the final +messages once the 'guix system init' process has completed." + (converse port + ((pause) ;"Press Enter to continue." + #t) + ((installation-complete) ;congratulations! + (values)))) + +;;; Local Variables: +;;; eval: (put 'converse 'scheme-indent-function 1) +;;; eval: (put 'with-ellipsis 'scheme-indent-function 1) +;;; End: diff --git a/gnu/local.mk b/gnu/local.mk index 22c11afd38..f6c56ceb28 100644 --- a/gnu/local.mk +++ b/gnu/local.mk @@ -1,5 +1,5 @@ # GNU Guix --- Functional package management for GNU -# Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 Ludovic Courtès +# Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Ludovic Courtès # Copyright © 2013, 2014, 2015, 2016, 2017, 2018, 2019 Andreas Enge # Copyright © 2016 Mathieu Lirzin # Copyright © 2013, 2014, 2015, 2016, 2017, 2018, 2019 Mark H Weaver @@ -656,6 +656,7 @@ INSTALLER_MODULES = \ %D%/installer/record.scm \ %D%/installer/services.scm \ %D%/installer/steps.scm \ + %D%/installer/tests.scm \ %D%/installer/timezone.scm \ %D%/installer/user.scm \ %D%/installer/utils.scm \ diff --git a/gnu/tests/install.scm b/gnu/tests/install.scm index 335efbd468..8480c95fd6 100644 --- a/gnu/tests/install.scm +++ b/gnu/tests/install.scm @@ -26,10 +26,14 @@ (define-module (gnu tests install) #:use-module (gnu system install) #:use-module (gnu system vm) #:use-module ((gnu build vm) #:select (qemu-command)) + #:use-module (gnu packages admin) #:use-module (gnu packages bootloaders) + #:use-module (gnu packages cryptsetup) + #:use-module (gnu packages linux) #:use-module (gnu packages ocr) #:use-module (gnu packages package-management) #:use-module (gnu packages virtualization) + #:use-module (gnu services networking) #:use-module (guix store) #:use-module (guix monads) #:use-module (guix packages) @@ -44,7 +48,9 @@ (define-module (gnu tests install) %test-raid-root-os %test-encrypted-root-os %test-btrfs-root-os - %test-jfs-root-os)) + %test-jfs-root-os + + %test-gui-installed-os)) ;;; Commentary: ;;; @@ -179,6 +185,7 @@ (define %extlinux-gpt-installation-script (define* (run-install target-os target-os-source #:key (script %simple-installation-script) + (gui-test #f) (packages '()) (os (marionette-operating-system (operating-system @@ -191,6 +198,7 @@ (define* (run-install target-os target-os-source packages)) (kernel-arguments '("console=ttyS0"))) #:imported-modules '((gnu services herd) + (gnu installer tests) (guix combinators)))) (installation-disk-image-file-system-type "ext4") (target-size (* 2200 MiB))) @@ -256,13 +264,21 @@ (define marionette (start 'term-tty1)) marionette) - (marionette-eval '(call-with-output-file "/etc/target-config.scm" - (lambda (port) - (write '#$target-os-source port))) - marionette) - - (exit (marionette-eval '(zero? (system #$script)) - marionette))))) + (when #$(->bool script) + (marionette-eval '(call-with-output-file "/etc/target-config.scm" + (lambda (port) + (write '#$target-os-source port))) + marionette) + (exit (marionette-eval '(zero? (system #$script)) + marionette))) + + (when #$(->bool gui-test) + (wait-for-unix-socket "/var/guix/installer-socket" + marionette) + (format #t "installer socket ready~%") + (force-output) + (exit #$(and gui-test + (gui-test #~marionette))))))) (gexp->derivation "installation" install))) @@ -890,4 +906,174 @@ (define %test-jfs-root-os (command (qemu-command/writable-image image))) (run-basic-test %jfs-root-os command "jfs-root-os"))))) + +;;; +;;; Installation through the graphical interface. +;;; + +(define %syslog-conf + ;; Syslog configuration that dumps to /dev/console, so we can see the + ;; installer's messages during the test. + (computed-file "syslog.conf" + #~(begin + (copy-file #$%default-syslog.conf #$output) + (chmod #$output #o644) + (let ((port (open-file #$output "a"))) + (display "\n*.info /dev/console\n" port) + #t)))) + +(define (operating-system-with-console-syslog os) + "Return OS with a syslog service that writes to /dev/console." + (operating-system + (inherit os) + (services (modify-services (operating-system-user-services os) + (syslog-service-type config + => + (syslog-configuration + (inherit config) + (config-file %syslog-conf))))))) + +(define %root-password "foo") + +(define* (gui-test-program marionette #:key (encrypted? #f)) + #~(let () + (define (screenshot file) + (marionette-control (string-append "screendump " file) + #$marionette)) + + (setvbuf (current-output-port) 'none) + (setvbuf (current-error-port) 'none) + + (marionette-eval '(use-modules (gnu installer tests)) + #$marionette) + + ;; Arrange so that 'converse' prints debugging output to the console. + (marionette-eval '(let ((console (open-output-file "/dev/console"))) + (setvbuf console 'none) + (conversation-log-port console)) + #$marionette) + + ;; Tell the installer to not wait for the Connman "online" status. + (marionette-eval '(call-with-output-file "/tmp/installer-assume-online" + (const #t)) + #$marionette) + + ;; Run 'guix system init' with '--no-grafts', to cope with the lack of + ;; network access. + (marionette-eval '(call-with-output-file + "/tmp/installer-system-init-options" + (lambda (port) + (write '("--no-grafts" "--no-substitutes") + port))) + #$marionette) + + (marionette-eval '(define installer-socket + (open-installer-socket)) + #$marionette) + (screenshot "installer-start.ppm") + + (marionette-eval '(choose-locale+keyboard installer-socket) + #$marionette) + (screenshot "installer-locale.ppm") + + ;; Choose the host name that the "basic" test expects. + (marionette-eval '(enter-host-name+passwords installer-socket + #:host-name "liberigilo" + #:root-password + #$%root-password + #:users + '(("alice" "pass1") + ("bob" "pass2"))) + #$marionette) + (screenshot "installer-services.ppm") + + (marionette-eval '(choose-services installer-socket + #:desktop-environments '() + #:choose-network-service? + (const #f)) + #$marionette) + (screenshot "installer-partitioning.ppm") + + (marionette-eval '(choose-partitioning installer-socket + #:encrypted? #$encrypted? + #:passphrase #$%luks-passphrase) + #$marionette) + (screenshot "installer-run.ppm") + + (marionette-eval '(conclude-installation installer-socket) + #$marionette) + + (sync) + #t)) + +(define %extra-packages + ;; Packages needed when installing with an encrypted root. + (list isc-dhcp + lvm2-static cryptsetup-static e2fsck/static + loadkeys-static)) + +(define installation-os-for-gui-tests + ;; Operating system that contains all of %EXTRA-PACKAGES, needed for the + ;; target OS, as well as syslog output redirected to the console so we can + ;; see what the installer is up to. + (marionette-operating-system + (operating-system + (inherit (operating-system-with-console-syslog + (operating-system-add-packages + (operating-system-with-current-guix + installation-os) + %extra-packages))) + (kernel-arguments '("console=ttyS0"))) + #:imported-modules '((gnu services herd) + (gnu installer tests) + (guix combinators)))) + +(define* (guided-installation-test name #:key encrypted?) + (define os + (operating-system + (inherit %minimal-os) + (users (append (list (user-account + (name "alice") + (comment "Bob's sister") + (group "users") + (supplementary-groups + '("wheel" "audio" "video"))) + (user-account + (name "bob") + (comment "Alice's brother") + (group "users") + (supplementary-groups + '("wheel" "audio" "video")))) + %base-user-accounts)) + (swap-devices '("/dev/vdb2")) + (services (cons (service dhcp-client-service-type) + (operating-system-user-services %minimal-os))))) + + (system-test + (name name) + (description + "Install an OS using the graphical installer and test it.") + (value + (mlet* %store-monad ((image (run-install os '(this is unused) + #:script #f + #:os installation-os-for-gui-tests + #:gui-test + (lambda (marionette) + (gui-test-program + marionette + #:encrypted? encrypted?)))) + (command (qemu-command/writable-image image))) + (run-basic-test os command name + #:initialization (and encrypted? enter-luks-passphrase) + #:root-password %root-password))))) + +(define %test-gui-installed-os + (guided-installation-test "gui-installed-os" + #:encrypted? #f)) + +;; (define %test-gui-installed-os +;; ;; FIXME: Fails due to . +;; (guided-installation-test "gui-installed-os-encrypted" +;; #:encrypted? #t)) + ;;; install.scm ends here -- cgit v1.2.3