Subject: [bug#36555] [PATCH v5 3/3] tests: Add reconfigure system test.

From: [email-masked] (Jakob L. Kreuze)
Date: Mon, 22 Jul 2019 14:57:52 -0400

* gnu/tests/reconfigure.scm: New file.
* gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
---
 gnu/local.mk              |   1 +
 gnu/tests/reconfigure.scm | 262 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 263 insertions(+)
 create mode 100644 gnu/tests/reconfigure.scm

diff --git a/gnu/local.mk b/gnu/local.mk
index 0e17af953..b334d0572 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -592,6 +592,7 @@ GNU_SYSTEM_MODULES =				\
   %D%/tests/mail.scm				\
   %D%/tests/messaging.scm			\
   %D%/tests/networking.scm			\
+  %D%/tests/reconfigure.scm			\
   %D%/tests/rsync.scm				\
   %D%/tests/security-token.scm			\
   %D%/tests/singularity.scm			\
diff --git a/gnu/tests/reconfigure.scm b/gnu/tests/reconfigure.scm
new file mode 100644
index 000000000..3a2f0a2e5
--- /dev/null
+++ b/gnu/tests/reconfigure.scm
@@ -0,0 +1,262 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2019 Jakob L. Kreuze <[email-masked]>
+;;;
+;;; 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 <http://www.gnu.org/licenses/>.
+
+(define-module (gnu tests reconfigure)
+  #:use-module (gnu bootloader)
+  #:use-module (gnu services shepherd)
+  #:use-module (gnu system vm)
+  #:use-module (gnu system)
+  #:use-module (gnu tests)
+  #:use-module (guix derivations)
+  #:use-module (guix gexp)
+  #:use-module (guix monads)
+  #:use-module (guix scripts system reconfigure)
+  #:use-module (guix store)
+  #:export (%test-switch-to-system
+            %test-upgrade-services
+            %test-install-bootloader))
+
+;;; Commentary:
+;;;
+;;; Test in-place system reconfiguration: advancing the system generation on a
+;;; running instance of the Guix System.
+;;;
+;;; Code:
+
+(define* (run-switch-to-system-test)
+  "Run a test of an OS running SWITCH-SYSTEM-PROGRAM, which creates a new
+generation of the system profile."
+  (define os
+    (marionette-operating-system
+     (simple-operating-system)
+     #:imported-modules '((gnu services herd)
+                          (guix combinators))))
+
+  (define vm (virtual-machine os))
+
+  (define (test script)
+    (with-imported-modules '((gnu build marionette))
+      #~(begin
+          (use-modules (gnu build marionette)
+                       (srfi srfi-64))
+
+          (define marionette
+            (make-marionette (list #$vm)))
+
+          ;; Return the names of the generation symlinks on MARIONETTE.
+          (define (system-generations marionette)
+            (marionette-eval
+             '(begin
+                (use-modules (ice-9 ftw)
+                             (srfi srfi-1))
+                (let* ((profile-dir "/var/guix/profiles/")
+                       (entries (map first (cddr (file-system-tree profile-dir)))))
+                  (remove (lambda (entry)
+                            (member entry '("per-user" "system")))
+                          entries)))
+             marionette))
+
+          (mkdir #$output)
+          (chdir #$output)
+
+          (test-begin "switch-to-system")
+
+          (let ((generations-prior (system-generations marionette)))
+            (test-assert "script successfully evaluated"
+              (marionette-eval
+               '(primitive-load #$script)
+               marionette))
+
+            (test-equal "script created new generation"
+              (length (system-generations marionette))
+              (1+ (length generations-prior))))
+
+          (test-end)
+          (exit (= (test-runner-fail-count (test-runner-current)) 0)))))
+
+  (gexp->derivation "switch-to-system" (test (switch-system-program os))))
+
+(define* (run-upgrade-services-test)
+  "Run a test of an OS running UPGRADE-SERVICES-PROGRAM, which upgrades the
+Shepherd (PID 1) by unloading obsolete services and loading new services."
+  (define os
+    (marionette-operating-system
+     (simple-operating-system)
+     #:imported-modules '((gnu services herd)
+                          (guix combinators))))
+
+  (define vm (virtual-machine os))
+
+  (define dummy-service
+    ;; Shepherd service that does nothing, for the sole purpose of ensuring
+    ;; that it is properly installed and started by the script.
+    (shepherd-service (provision '(dummy))
+                      (start #~(const #t))
+                      (stop #~(const #t))
+                      (respawn? #f)))
+
+  ;; Return the Shepherd service file for SERVICE, after ensuring that it
+  ;; exists in the store.
+  (define (ensure-service-file service)
+    (let ((file (shepherd-service-file service)))
+      (mlet* %store-monad ((store-object (lower-object file))
+                           (_ (built-derivations (list store-object))))
+        (return file))))
+
+  (define (test enable-dummy disable-dummy)
+    (with-imported-modules '((gnu build marionette))
+      #~(begin
+          (use-modules (gnu build marionette)
+                       (srfi srfi-64))
+
+          (define marionette
+            (make-marionette (list #$vm)))
+
+          ;; Return the names of the running services on MARIONETTE.
+          (define (running-services marionette)
+            (marionette-eval
+             '(begin
+                (use-modules (gnu services herd))
+                (map live-service-canonical-name (current-services)))
+             marionette))
+
+          (mkdir #$output)
+          (chdir #$output)
+
+          (test-begin "upgrade-services")
+
+          (let ((services-prior (running-services marionette)))
+            (test-assert "script successfully evaluated"
+              (marionette-eval
+               '(primitive-load #$enable-dummy)
+               marionette))
+
+            (test-assert "script started new service"
+              (and (not (memq 'dummy services-prior))
+                   (memq 'dummy (running-services marionette))))
+
+            (test-assert "script successfully evaluated"
+              (marionette-eval
+               '(primitive-load #$disable-dummy)
+               marionette))
+
+            (test-assert "script stopped obsolete service"
+              (not (memq 'dummy (running-services marionette)))))
+
+          (test-end)
+          (exit (= (test-runner-fail-count (test-runner-current)) 0)))))
+
+  (mlet* %store-monad ((file (ensure-service-file dummy-service)))
+    (let ((enable (upgrade-services-program (list file) '(dummy) '() '()))
+          (disable (upgrade-services-program '() '() '(dummy) '())))
+      (gexp->derivation "upgrade-services" (test enable disable)))))
+
+(define* (run-install-bootloader-test)
+  "Run a test of an OS running INSTALL-BOOTLOADER-PROGRAM, which installs a
+bootloader's configuration file."
+  (define os
+    (marionette-operating-system
+     (simple-operating-system)
+     #:imported-modules '((gnu services herd)
+                          (guix combinators))))
+
+  (define vm (virtual-machine os))
+
+  (define (test script)
+    (with-imported-modules '((gnu build marionette))
+      #~(begin
+          (use-modules (gnu build marionette)
+                       (ice-9 regex)
+                       (srfi srfi-1)
+                       (srfi srfi-64))
+
+          (define marionette
+            (make-marionette (list #$vm)))
+
+          ;; Return the system generation paths that have GRUB menu entries.
+          (define (generations-in-grub-cfg marionette)
+            (let ((grub-cfg (marionette-eval
+                             '(begin
+                                (call-with-input-file "/boot/grub/grub.cfg"
+                                  (lambda (port)
+                                    (get-string-all port))))
+                             marionette)))
+              (map (lambda (parameter)
+                     (second (string-split (match:substring parameter) #\=)))
+                   (list-matches "system=[^ ]*" grub-cfg))))
+
+          (mkdir #$output)
+          (chdir #$output)
+
+          (test-begin "install-bootloader")
+
+          (test-assert "no prior menu entry for system generation"
+            (not (member #$os (generations-in-grub-cfg marionette))))
+
+          (test-assert "script successfully evaluated"
+            (marionette-eval
+             '(primitive-load #$script)
+             marionette))
+
+          (test-assert "menu entry created for system generation"
+            (member #$os (generations-in-grub-cfg marionette)))
+
+          (test-end)
+          (exit (= (test-runner-fail-count (test-runner-current)) 0)))))
+
+  (let* ((bootloader ((compose bootloader-configuration-bootloader
+                               operating-system-bootloader)
+                      os))
+         ;; The typical use-case for 'install-bootloader-program' is to read
+         ;; the boot parameters for the existing menu entries on the system,
+         ;; parse them with 'boot-parameters->menu-entry', and pass the
+         ;; results to 'operating-system-bootcfg'. However, to obtain boot
+         ;; parameters, we would need to start the marionette, which we should
+         ;; ideally avoid doing outside of the 'test' G-Expression. Thus, we
+         ;; generate a bootloader configuration for the script as if there
+         ;; were no existing menu entries. In the grand scheme of things, this
+         ;; matters little -- these tests should not make assertions about the
+         ;; behavior of 'operating-system-bootcfg'.
+         (bootcfg (operating-system-bootcfg os '()))
+         (bootcfg-file (bootloader-configuration-file bootloader)))
+    (gexp->derivation
+     "install-bootloader"
+     ;; Due to the read-only nature of the virtual machines used in the system
+     ;; test suite, the bootloader installer script is omitted. 'grub-install'
+     ;; would attempt to write directly to the virtual disk if the
+     ;; installation script were run.
+     (test (install-bootloader-program #f #f bootcfg bootcfg-file #f "/")))))
+
+(define %test-switch-to-system
+  (system-test
+   (name "switch-to-system")
+   (description "Create a new generation of the system profile.")
+   (value (run-switch-to-system-test))))
+
+(define %test-upgrade-services
+  (system-test
+   (name "upgrade-services")
+   (description "Upgrade the Shepherd by unloading obsolete services and
+loading new services.")
+   (value (run-upgrade-services-test))))
+
+(define %test-install-bootloader
+  (system-test
+   (name "install-bootloader")
+   (description "Install a bootloader and its configuration file.")
+   (value (run-install-bootloader-test))))
-- 
2.22.0