tests.scm 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2020 Ludovic Courtès <ludo@gnu.org>
  3. ;;; Copyright © 2020 Mathieu Othacehe <m.othacehe@gmail.com>
  4. ;;;
  5. ;;; This file is part of GNU Guix.
  6. ;;;
  7. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  8. ;;; under the terms of the GNU General Public License as published by
  9. ;;; the Free Software Foundation; either version 3 of the License, or (at
  10. ;;; your option) any later version.
  11. ;;;
  12. ;;; GNU Guix is distributed in the hope that it will be useful, but
  13. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  14. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. ;;; GNU General Public License for more details.
  16. ;;;
  17. ;;; You should have received a copy of the GNU General Public License
  18. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  19. (define-module (gnu installer tests)
  20. #:use-module (srfi srfi-1)
  21. #:use-module (srfi srfi-34)
  22. #:use-module (srfi srfi-35)
  23. #:use-module (ice-9 match)
  24. #:use-module (ice-9 regex)
  25. #:use-module (ice-9 pretty-print)
  26. #:export (&pattern-not-matched
  27. pattern-not-matched?
  28. %installer-socket-file
  29. open-installer-socket
  30. converse
  31. conversation-log-port
  32. choose-locale+keyboard
  33. enter-host-name+passwords
  34. choose-services
  35. choose-partitioning
  36. start-installation
  37. complete-installation
  38. edit-configuration-file))
  39. ;;; Commentary:
  40. ;;;
  41. ;;; This module provides tools to test the guided "graphical" installer in a
  42. ;;; non-interactive fashion. The core of it is 'converse': it allows you to
  43. ;;; state Expect-style dialogues, which happen over the Unix-domain socket the
  44. ;;; installer listens to. Higher-level procedures such as
  45. ;;; 'choose-locale+keyboard' are provided to perform specific parts of the
  46. ;;; dialogue.
  47. ;;;
  48. ;;; Code:
  49. (define %installer-socket-file
  50. ;; Socket the installer listens to.
  51. "/var/guix/installer-socket")
  52. (define* (open-installer-socket #:optional (file %installer-socket-file))
  53. "Return a socket connected to the installer."
  54. (let ((sock (socket AF_UNIX SOCK_STREAM 0)))
  55. (connect sock AF_UNIX file)
  56. sock))
  57. (define-condition-type &pattern-not-matched &error
  58. pattern-not-matched?
  59. (pattern pattern-not-matched-pattern)
  60. (sexp pattern-not-matched-sexp))
  61. (define (pattern-error pattern sexp)
  62. (raise (condition
  63. (&pattern-not-matched
  64. (pattern pattern) (sexp sexp)))))
  65. (define conversation-log-port
  66. ;; Port where debugging info is logged
  67. (make-parameter (current-error-port)))
  68. (define (converse-debug pattern)
  69. (format (conversation-log-port)
  70. "conversation expecting pattern ~s~%"
  71. pattern))
  72. (define-syntax converse
  73. (lambda (s)
  74. "Convert over PORT: read sexps from there, match them against each
  75. PATTERN, and send the corresponding REPLY. Raise to '&pattern-not-matched'
  76. when one of the PATTERNs is not matched."
  77. ;; XXX: Strings that appear in PATTERNs must be in the language the
  78. ;; installer is running in. In the future, we should add support to allow
  79. ;; writing English strings in PATTERNs and have the pattern matcher
  80. ;; automatically translate them.
  81. ;; Here we emulate 'pmatch' syntax on top of 'match'. This is ridiculous
  82. ;; but that's because 'pmatch' compares objects with 'eq?', making it
  83. ;; pretty useless, and it doesn't support ellipses and such.
  84. (define (quote-pattern s)
  85. ;; Rewrite the pattern S from pmatch style (a ,b) to match style like
  86. ;; ('a b).
  87. (with-ellipsis :::
  88. (syntax-case s (unquote _ ...)
  89. ((unquote id) #'id)
  90. (_ #'_)
  91. (... #'...)
  92. (id
  93. (identifier? #'id)
  94. #''id)
  95. ((lst :::) (map quote-pattern #'(lst :::)))
  96. (pattern #'pattern))))
  97. (define (match-pattern s)
  98. ;; Match one pattern without a guard.
  99. (syntax-case s ()
  100. ((port (pattern reply) continuation)
  101. (with-syntax ((pattern (quote-pattern #'pattern)))
  102. #'(let ((pat 'pattern))
  103. (converse-debug pat)
  104. (match (read port)
  105. (pattern
  106. (let ((data (call-with-values (lambda () reply)
  107. list)))
  108. (for-each (lambda (obj)
  109. (write obj port)
  110. (newline port))
  111. data)
  112. (force-output port)
  113. (continuation port)))
  114. (sexp
  115. (pattern-error pat sexp))))))))
  116. (syntax-case s ()
  117. ((_ port (pattern reply) rest ...)
  118. (match-pattern #'(port (pattern reply)
  119. (lambda (port)
  120. (converse port rest ...)))))
  121. ((_ port (pattern guard reply) rest ...)
  122. #`(let ((skip? (not guard))
  123. (next (lambda (p)
  124. (converse p rest ...))))
  125. (if skip?
  126. (next port)
  127. #,(match-pattern #'(port (pattern reply) next)))))
  128. ((_ port)
  129. #t))))
  130. (define* (choose-locale+keyboard port
  131. #:key
  132. (language "English")
  133. (location "Hong Kong")
  134. (timezone '("Europe" "Zagreb"))
  135. (keyboard
  136. '("English (US)"
  137. "English (intl., with AltGr dead keys)")))
  138. "Converse over PORT with the guided installer to choose the specified
  139. LANGUAGE, LOCATION, TIMEZONE, and KEYBOARD."
  140. (converse port
  141. ((list-selection (title "Locale language")
  142. (multiple-choices? #f)
  143. (items _))
  144. language)
  145. ((list-selection (title "Locale location")
  146. (multiple-choices? #f)
  147. (items _))
  148. location)
  149. ((menu (title "GNU Guix install")
  150. (text _)
  151. (items (,guided _ ...))) ;"Guided graphical installation"
  152. guided)
  153. ((list-selection (title "Timezone")
  154. (multiple-choices? #f)
  155. (items _))
  156. (first timezone))
  157. ((list-selection (title "Timezone")
  158. (multiple-choices? #f)
  159. (items _))
  160. (second timezone))
  161. ((list-selection (title "Layout")
  162. (multiple-choices? #f)
  163. (items _))
  164. (first keyboard))
  165. ((list-selection (title "Variant")
  166. (multiple-choices? #f)
  167. (items _))
  168. (second keyboard))))
  169. (define* (enter-host-name+passwords port
  170. #:key
  171. (host-name "guix")
  172. (root-password "foo")
  173. (users '(("alice" "pass1")
  174. ("bob" "pass2")
  175. ("charlie" "pass3"))))
  176. "Converse over PORT with the guided installer to choose HOST-NAME,
  177. ROOT-PASSWORD, and USERS."
  178. (converse port
  179. ((input (title "Hostname") (text _) (default _))
  180. host-name)
  181. ((input (title "System administrator password") (text _) (default _))
  182. root-password)
  183. ((input (title "Password confirmation required") (text _) (default _))
  184. root-password)
  185. ((add-users)
  186. (match users
  187. (((names passwords) ...)
  188. (map (lambda (name password)
  189. `(user (name ,name) (real-name ,(string-titlecase name))
  190. (home-directory ,(string-append "/home/" name))
  191. (password ,password)))
  192. names passwords))))))
  193. (define* (choose-services port
  194. #:key
  195. (choose-desktop-environment? (const #f))
  196. (choose-network-service?
  197. (lambda (service)
  198. (or (string-contains service "SSH")
  199. (string-contains service "NSS"))))
  200. (choose-network-management-tool?
  201. (lambda (service)
  202. (string-contains service "DHCP"))))
  203. "Converse over PORT to choose networking services."
  204. (define desktop-environments '())
  205. (converse port
  206. ((checkbox-list (title "Desktop environment") (text _)
  207. (items ,services))
  208. (let ((desktops (filter choose-desktop-environment? services)))
  209. (set! desktop-environments desktops)
  210. desktops))
  211. ((checkbox-list (title "Network service") (text _)
  212. (items ,services))
  213. (filter choose-network-service? services))
  214. ;; The "Network management" dialog shows up only when no desktop
  215. ;; environments have been selected, hence the guard.
  216. ((list-selection (title "Network management")
  217. (multiple-choices? #f)
  218. (items ,services))
  219. (null? desktop-environments)
  220. (find choose-network-management-tool? services))))
  221. (define (edit-configuration-file file)
  222. "Edit FILE, an operating system configuration file generated by the
  223. installer, by adding a marionette service such that the installed OS is
  224. instrumented for further testing."
  225. (define (read-expressions port)
  226. (let loop ((result '()))
  227. (match (read port)
  228. ((? eof-object?)
  229. (reverse result))
  230. (exp
  231. (loop (cons exp result))))))
  232. (define (edit exp)
  233. (match exp
  234. (('operating-system _ ...)
  235. `(marionette-operating-system ,exp
  236. #:imported-modules
  237. '((gnu services herd)
  238. (guix build utils)
  239. (guix combinators))))
  240. (_
  241. exp)))
  242. (let ((content (call-with-input-file file read-expressions)))
  243. (call-with-output-file file
  244. (lambda (port)
  245. (format port "\
  246. ;; Operating system configuration edited for automated testing.~%~%")
  247. (pretty-print '(use-modules (gnu tests)) port)
  248. (for-each (lambda (exp)
  249. (pretty-print (edit exp) port)
  250. (newline port))
  251. content)))
  252. #t))
  253. (define* (choose-partitioning port
  254. #:key
  255. (encrypted? #t)
  256. (uefi-support? #f)
  257. (passphrase "thepassphrase")
  258. (edit-configuration-file
  259. edit-configuration-file))
  260. "Converse over PORT to choose the partitioning method. When ENCRYPTED? is
  261. true, choose full-disk encryption with PASSPHRASE as the LUKS passphrase.
  262. When UEFI-SUPPORT? is true, assume that we are running the installation tests
  263. on an UEFI capable machine.
  264. This conversation stops when the user partitions have been formatted, right
  265. before the installer generates the configuration file and shows it in a dialog
  266. box. "
  267. (converse port
  268. ((list-selection (title "Partitioning method")
  269. (multiple-choices? #f)
  270. (items (,not-encrypted ,encrypted _ ...)))
  271. (if encrypted?
  272. encrypted
  273. not-encrypted))
  274. ((list-selection (title "Disk") (multiple-choices? #f)
  275. (items (,disks ...)))
  276. ;; When running the installation from an ISO image, the CD/DVD drive
  277. ;; shows up in the list. Avoid it.
  278. (find (lambda (disk)
  279. (not (or (string-contains disk "DVD")
  280. (string-contains disk "CD-ROM"))))
  281. disks))
  282. ;; The "Partition table" dialog pops up only if there's not already a
  283. ;; partition table and if the system does not support UEFI.
  284. ((list-selection (title "Partition table")
  285. (multiple-choices? #f)
  286. (items _))
  287. ;; When UEFI is supported, the partition is forced to GPT by the
  288. ;; installer.
  289. (not uefi-support?)
  290. "gpt")
  291. ((list-selection (title "Partition scheme")
  292. (multiple-choices? #f)
  293. (items (,one-partition _ ...)))
  294. one-partition)
  295. ((list-selection (title "Guided partitioning")
  296. (multiple-choices? #f)
  297. (items (,disk _ ...)))
  298. disk)
  299. ((input (title "Password required")
  300. (text _) (default #f))
  301. encrypted? ;only when ENCRYPTED?
  302. passphrase)
  303. ((input (title "Password confirmation required")
  304. (text _) (default #f))
  305. encrypted?
  306. passphrase)
  307. ((confirmation (title "Format disk?") (text _))
  308. #t)
  309. ((info (title "Preparing partitions") _ ...)
  310. (values)) ;nothing to return
  311. ((starting-final-step)
  312. ;; Do not return anything. The reply will be sent by
  313. ;; 'conclude-installation' and in the meantime the installer just waits
  314. ;; for us, giving us a chance to do things such as changing partition
  315. ;; UUIDs before it generates the configuration file.
  316. (values))))
  317. (define (start-installation port)
  318. "Start the installation by checking over PORT that we get the generated
  319. configuration file, accepting it and starting the installation, and then
  320. receiving the pause message once the 'guix system init' process has
  321. completed."
  322. ;; Assume the previous message received was 'starting-final-step'; here we
  323. ;; send the reply to that message, which lets the installer continue.
  324. (write #t port)
  325. (newline port)
  326. (force-output port)
  327. (converse port
  328. ((file-dialog (title "Configuration file")
  329. (text _)
  330. (file ,configuration-file))
  331. (edit-configuration-file configuration-file))
  332. ((pause) ;"Press Enter to continue."
  333. (values))))
  334. (define (complete-installation port)
  335. "Complete the installation by replying to the installer pause message and
  336. waiting for the installation-complete message."
  337. ;; Assume the previous message received was 'pause'; here we send the reply
  338. ;; to that message, which lets the installer continue.
  339. (write #t port)
  340. (newline port)
  341. (force-output port)
  342. (converse port
  343. ((installation-complete)
  344. (values))))
  345. ;;; Local Variables:
  346. ;;; eval: (put 'converse 'scheme-indent-function 1)
  347. ;;; eval: (put 'with-ellipsis 'scheme-indent-function 1)
  348. ;;; End: