tests.scm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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. conclude-installation
  37. edit-configuration-file))
  38. ;;; Commentary:
  39. ;;;
  40. ;;; This module provides tools to test the guided "graphical" installer in a
  41. ;;; non-interactive fashion. The core of it is 'converse': it allows you to
  42. ;;; state Expect-style dialogues, which happen over the Unix-domain socket the
  43. ;;; installer listens to. Higher-level procedures such as
  44. ;;; 'choose-locale+keyboard' are provided to perform specific parts of the
  45. ;;; dialogue.
  46. ;;;
  47. ;;; Code:
  48. (define %installer-socket-file
  49. ;; Socket the installer listens to.
  50. "/var/guix/installer-socket")
  51. (define* (open-installer-socket #:optional (file %installer-socket-file))
  52. "Return a socket connected to the installer."
  53. (let ((sock (socket AF_UNIX SOCK_STREAM 0)))
  54. (connect sock AF_UNIX file)
  55. sock))
  56. (define-condition-type &pattern-not-matched &error
  57. pattern-not-matched?
  58. (pattern pattern-not-matched-pattern)
  59. (sexp pattern-not-matched-sexp))
  60. (define (pattern-error pattern sexp)
  61. (raise (condition
  62. (&pattern-not-matched
  63. (pattern pattern) (sexp sexp)))))
  64. (define conversation-log-port
  65. ;; Port where debugging info is logged
  66. (make-parameter (current-error-port)))
  67. (define (converse-debug pattern)
  68. (format (conversation-log-port)
  69. "conversation expecting pattern ~s~%"
  70. pattern))
  71. (define-syntax converse
  72. (lambda (s)
  73. "Convert over PORT: read sexps from there, match them against each
  74. PATTERN, and send the corresponding REPLY. Raise to '&pattern-not-matched'
  75. when one of the PATTERNs is not matched."
  76. ;; XXX: Strings that appear in PATTERNs must be in the language the
  77. ;; installer is running in. In the future, we should add support to allow
  78. ;; writing English strings in PATTERNs and have the pattern matcher
  79. ;; automatically translate them.
  80. ;; Here we emulate 'pmatch' syntax on top of 'match'. This is ridiculous
  81. ;; but that's because 'pmatch' compares objects with 'eq?', making it
  82. ;; pretty useless, and it doesn't support ellipses and such.
  83. (define (quote-pattern s)
  84. ;; Rewrite the pattern S from pmatch style (a ,b) to match style like
  85. ;; ('a b).
  86. (with-ellipsis :::
  87. (syntax-case s (unquote _ ...)
  88. ((unquote id) #'id)
  89. (_ #'_)
  90. (... #'...)
  91. (id
  92. (identifier? #'id)
  93. #''id)
  94. ((lst :::) (map quote-pattern #'(lst :::)))
  95. (pattern #'pattern))))
  96. (define (match-pattern s)
  97. ;; Match one pattern without a guard.
  98. (syntax-case s ()
  99. ((port (pattern reply) continuation)
  100. (with-syntax ((pattern (quote-pattern #'pattern)))
  101. #'(let ((pat 'pattern))
  102. (converse-debug pat)
  103. (match (read port)
  104. (pattern
  105. (let ((data (call-with-values (lambda () reply)
  106. list)))
  107. (for-each (lambda (obj)
  108. (write obj port)
  109. (newline port))
  110. data)
  111. (force-output port)
  112. (continuation port)))
  113. (sexp
  114. (pattern-error pat sexp))))))))
  115. (syntax-case s ()
  116. ((_ port (pattern reply) rest ...)
  117. (match-pattern #'(port (pattern reply)
  118. (lambda (port)
  119. (converse port rest ...)))))
  120. ((_ port (pattern guard reply) rest ...)
  121. #`(let ((skip? (not guard))
  122. (next (lambda (p)
  123. (converse p rest ...))))
  124. (if skip?
  125. (next port)
  126. #,(match-pattern #'(port (pattern reply) next)))))
  127. ((_ port)
  128. #t))))
  129. (define* (choose-locale+keyboard port
  130. #:key
  131. (language "English")
  132. (location "Hong Kong")
  133. (timezone '("Europe" "Zagreb"))
  134. (keyboard
  135. '("English (US)"
  136. "English (intl., with AltGr dead keys)")))
  137. "Converse over PORT with the guided installer to choose the specified
  138. LANGUAGE, LOCATION, TIMEZONE, and KEYBOARD."
  139. (converse port
  140. ((list-selection (title "Locale language")
  141. (multiple-choices? #f)
  142. (items _))
  143. language)
  144. ((list-selection (title "Locale location")
  145. (multiple-choices? #f)
  146. (items _))
  147. location)
  148. ((menu (title "GNU Guix install")
  149. (text _)
  150. (items (,guided _ ...))) ;"Guided graphical installation"
  151. guided)
  152. ((list-selection (title "Timezone")
  153. (multiple-choices? #f)
  154. (items _))
  155. (first timezone))
  156. ((list-selection (title "Timezone")
  157. (multiple-choices? #f)
  158. (items _))
  159. (second timezone))
  160. ((list-selection (title "Layout")
  161. (multiple-choices? #f)
  162. (items _))
  163. (first keyboard))
  164. ((list-selection (title "Variant")
  165. (multiple-choices? #f)
  166. (items _))
  167. (second keyboard))))
  168. (define* (enter-host-name+passwords port
  169. #:key
  170. (host-name "guix")
  171. (root-password "foo")
  172. (users '(("alice" "pass1")
  173. ("bob" "pass2")
  174. ("charlie" "pass3"))))
  175. "Converse over PORT with the guided installer to choose HOST-NAME,
  176. ROOT-PASSWORD, and USERS."
  177. (converse port
  178. ((input (title "Hostname") (text _) (default _))
  179. host-name)
  180. ((input (title "System administrator password") (text _) (default _))
  181. root-password)
  182. ((input (title "Password confirmation required") (text _) (default _))
  183. root-password)
  184. ((add-users)
  185. (match users
  186. (((names passwords) ...)
  187. (map (lambda (name password)
  188. `(user (name ,name) (real-name ,(string-titlecase name))
  189. (home-directory ,(string-append "/home/" name))
  190. (password ,password)))
  191. names passwords))))))
  192. (define* (choose-services port
  193. #:key
  194. (choose-desktop-environment? (const #f))
  195. (choose-network-service?
  196. (lambda (service)
  197. (or (string-contains service "SSH")
  198. (string-contains service "NSS"))))
  199. (choose-network-management-tool?
  200. (lambda (service)
  201. (string-contains service "DHCP"))))
  202. "Converse over PORT to choose networking services."
  203. (define desktop-environments '())
  204. (converse port
  205. ((checkbox-list (title "Desktop environment") (text _)
  206. (items ,services))
  207. (let ((desktops (filter choose-desktop-environment? services)))
  208. (set! desktop-environments desktops)
  209. desktops))
  210. ((checkbox-list (title "Network service") (text _)
  211. (items ,services))
  212. (filter choose-network-service? services))
  213. ;; The "Network management" dialog shows up only when no desktop
  214. ;; environments have been selected, hence the guard.
  215. ((list-selection (title "Network management")
  216. (multiple-choices? #f)
  217. (items ,services))
  218. (null? desktop-environments)
  219. (find choose-network-management-tool? services))))
  220. (define (edit-configuration-file file)
  221. "Edit FILE, an operating system configuration file generated by the
  222. installer, by adding a marionette service such that the installed OS is
  223. instrumented for further testing."
  224. (define (read-expressions port)
  225. (let loop ((result '()))
  226. (match (read port)
  227. ((? eof-object?)
  228. (reverse result))
  229. (exp
  230. (loop (cons exp result))))))
  231. (define (edit exp)
  232. (match exp
  233. (('operating-system _ ...)
  234. `(marionette-operating-system ,exp
  235. #:imported-modules
  236. '((gnu services herd)
  237. (guix build utils)
  238. (guix combinators))))
  239. (_
  240. exp)))
  241. (let ((content (call-with-input-file file read-expressions)))
  242. (call-with-output-file file
  243. (lambda (port)
  244. (format port "\
  245. ;; Operating system configuration edited for automated testing.~%~%")
  246. (pretty-print '(use-modules (gnu tests)) port)
  247. (for-each (lambda (exp)
  248. (pretty-print (edit exp) port)
  249. (newline port))
  250. content)))
  251. #t))
  252. (define* (choose-partitioning port
  253. #:key
  254. (encrypted? #t)
  255. (passphrase "thepassphrase")
  256. (edit-configuration-file
  257. edit-configuration-file))
  258. "Converse over PORT to choose the partitioning method. When ENCRYPTED? is
  259. true, choose full-disk encryption with PASSPHRASE as the LUKS passphrase.
  260. This conversation goes past the final dialog box that shows the configuration
  261. file, actually starting the installation process."
  262. (converse port
  263. ((list-selection (title "Partitioning method")
  264. (multiple-choices? #f)
  265. (items (,not-encrypted ,encrypted _ ...)))
  266. (if encrypted?
  267. encrypted
  268. not-encrypted))
  269. ((list-selection (title "Disk") (multiple-choices? #f)
  270. (items (,disks ...)))
  271. ;; When running the installation from an ISO image, the CD/DVD drive
  272. ;; shows up in the list. Avoid it.
  273. (find (lambda (disk)
  274. (not (or (string-contains disk "DVD")
  275. (string-contains disk "CD-ROM"))))
  276. disks))
  277. ;; The "Partition table" dialog pops up only if there's not already a
  278. ;; partition table.
  279. ((list-selection (title "Partition table")
  280. (multiple-choices? #f)
  281. (items _))
  282. "gpt")
  283. ((list-selection (title "Partition scheme")
  284. (multiple-choices? #f)
  285. (items (,one-partition _ ...)))
  286. one-partition)
  287. ((list-selection (title "Guided partitioning")
  288. (multiple-choices? #f)
  289. (items (,disk _ ...)))
  290. disk)
  291. ((input (title "Password required")
  292. (text _) (default #f))
  293. encrypted? ;only when ENCRYPTED?
  294. passphrase)
  295. ((input (title "Password confirmation required")
  296. (text _) (default #f))
  297. encrypted?
  298. passphrase)
  299. ((confirmation (title "Format disk?") (text _))
  300. #t)
  301. ((info (title "Preparing partitions") _ ...)
  302. (values)) ;nothing to return
  303. ((file-dialog (title "Configuration file")
  304. (text _)
  305. (file ,configuration-file))
  306. (edit-configuration-file configuration-file))))
  307. (define (conclude-installation port)
  308. "Conclude the installation by checking over PORT that we get the final
  309. messages once the 'guix system init' process has completed."
  310. (converse port
  311. ((pause) ;"Press Enter to continue."
  312. #t)
  313. ((installation-complete) ;congratulations!
  314. (values))))
  315. ;;; Local Variables:
  316. ;;; eval: (put 'converse 'scheme-indent-function 1)
  317. ;;; eval: (put 'with-ellipsis 'scheme-indent-function 1)
  318. ;;; End: