tests.scm 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  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. (choose-misc-service?
  204. (lambda (service)
  205. (string-contains service "NTP")))
  206. (choose-other-service? (const #f)))
  207. "Converse over PORT to choose services."
  208. (define desktop-environments '())
  209. (converse port
  210. ((checkbox-list (title "Desktop environment") (text _)
  211. (items ,services))
  212. (let ((desktops (filter choose-desktop-environment? services)))
  213. (set! desktop-environments desktops)
  214. desktops))
  215. ((checkbox-list (title "Network service") (text _)
  216. (items ,services))
  217. (filter choose-network-service? services))
  218. ;; The "Network management" dialog shows up only when no desktop
  219. ;; environments have been selected, hence the guard.
  220. ((list-selection (title "Network management")
  221. (multiple-choices? #f)
  222. (items ,services))
  223. (null? desktop-environments)
  224. (find choose-network-management-tool? services))
  225. ((checkbox-list (title "Console services") (text _)
  226. (items ,services))
  227. (null? desktop-environments)
  228. (filter choose-misc-service? services))
  229. ((checkbox-list (title "Printing and document services") (text _)
  230. (items ,services))
  231. (filter choose-other-service? services))))
  232. (define (edit-configuration-file file)
  233. "Edit FILE, an operating system configuration file generated by the
  234. installer, by adding a marionette service such that the installed OS is
  235. instrumented for further testing."
  236. (define (read-expressions port)
  237. (let loop ((result '()))
  238. (match (read port)
  239. ((? eof-object?)
  240. (reverse result))
  241. (exp
  242. (loop (cons exp result))))))
  243. (define (edit exp)
  244. (match exp
  245. (('operating-system _ ...)
  246. `(marionette-operating-system ,exp
  247. #:imported-modules
  248. '((gnu services herd)
  249. (guix build utils)
  250. (guix combinators))))
  251. (_
  252. exp)))
  253. (let ((content (call-with-input-file file read-expressions)))
  254. ;; XXX: Remove the file before re-writing it, to be sure there are no
  255. ;; leftovers. We shouldn't have to do that as CALL-WITH-OUTPUT-FILE uses
  256. ;; the O_TRUNC flag by default.
  257. (delete-file file)
  258. (call-with-output-file file
  259. (lambda (port)
  260. (format port "\
  261. ;; Operating system configuration edited for automated testing.~%~%")
  262. (pretty-print '(use-modules (gnu tests)) port)
  263. (for-each (lambda (exp)
  264. (pretty-print (edit exp) port)
  265. (newline port))
  266. content)))
  267. #t))
  268. (define* (choose-partitioning port
  269. #:key
  270. (encrypted? #t)
  271. (uefi-support? #f)
  272. (passphrase "thepassphrase")
  273. (edit-configuration-file
  274. edit-configuration-file))
  275. "Converse over PORT to choose the partitioning method. When ENCRYPTED? is
  276. true, choose full-disk encryption with PASSPHRASE as the LUKS passphrase.
  277. When UEFI-SUPPORT? is true, assume that we are running the installation tests
  278. on an UEFI capable machine.
  279. This conversation stops when the user partitions have been formatted, right
  280. before the installer generates the configuration file and shows it in a dialog
  281. box. "
  282. (converse port
  283. ((list-selection (title "Partitioning method")
  284. (multiple-choices? #f)
  285. (items (,not-encrypted ,encrypted _ ...)))
  286. (if encrypted?
  287. encrypted
  288. not-encrypted))
  289. ((list-selection (title "Disk") (multiple-choices? #f)
  290. (items (,disks ...)))
  291. ;; When running the installation from an ISO image, the CD/DVD drive
  292. ;; shows up in the list. Avoid it.
  293. (find (lambda (disk)
  294. (not (or (string-contains disk "DVD")
  295. (string-contains disk "CD-ROM"))))
  296. disks))
  297. ;; The "Partition table" dialog pops up only if there's not already a
  298. ;; partition table and if the system does not support UEFI.
  299. ((list-selection (title "Partition table")
  300. (multiple-choices? #f)
  301. (items _))
  302. ;; When UEFI is supported, the partition is forced to GPT by the
  303. ;; installer.
  304. (not uefi-support?)
  305. "gpt")
  306. ((list-selection (title "Partition scheme")
  307. (multiple-choices? #f)
  308. (items (,one-partition _ ...)))
  309. one-partition)
  310. ((list-selection (title "Guided partitioning")
  311. (multiple-choices? #f)
  312. (items (,disk _ ...)))
  313. disk)
  314. ((input (title "Password required")
  315. (text _) (default #f))
  316. encrypted? ;only when ENCRYPTED?
  317. passphrase)
  318. ((input (title "Password confirmation required")
  319. (text _) (default #f))
  320. encrypted?
  321. passphrase)
  322. ((confirmation (title "Format disk?") (text _))
  323. #t)
  324. ((info (title "Preparing partitions") _ ...)
  325. (values)) ;nothing to return
  326. ((starting-final-step)
  327. ;; Do not return anything. The reply will be sent by
  328. ;; 'conclude-installation' and in the meantime the installer just waits
  329. ;; for us, giving us a chance to do things such as changing partition
  330. ;; UUIDs before it generates the configuration file.
  331. (values))))
  332. (define (start-installation port)
  333. "Start the installation by checking over PORT that we get the generated
  334. configuration file, accepting it and starting the installation, and then
  335. receiving the pause message once the 'guix system init' process has
  336. completed."
  337. ;; Assume the previous message received was 'starting-final-step'; here we
  338. ;; send the reply to that message, which lets the installer continue.
  339. (write #t port)
  340. (newline port)
  341. (force-output port)
  342. (converse port
  343. ((file-dialog (title "Configuration file")
  344. (text _)
  345. (file ,configuration-file))
  346. (edit-configuration-file configuration-file))
  347. ((pause) ;"Press Enter to continue."
  348. (values))))
  349. (define (complete-installation port)
  350. "Complete the installation by replying to the installer pause message and
  351. waiting for the installation-complete message."
  352. ;; Assume the previous message received was 'pause'; here we send the reply
  353. ;; to that message, which lets the installer continue.
  354. (write #t port)
  355. (newline port)
  356. (force-output port)
  357. (converse port
  358. ((installation-complete)
  359. (values))))
  360. ;;; Local Variables:
  361. ;;; eval: (put 'converse 'scheme-indent-function 1)
  362. ;;; eval: (put 'with-ellipsis 'scheme-indent-function 1)
  363. ;;; End: