guix/tests/services.scm
Brian Cully f66fa5f917
gnu: services: Revert to deleting and updating all matching services
This patch reverts the behavior introduced in
1819512073 which caused ‘modify-services’
clauses to only match a single instance of a service.

We will now match all service instances when doing a deletion or update, while
still raising an exception when trying to match against a service that does
not exist in the services list, or which was deleted explicitly by a ‘delete’
clause (or an update clause that returns ‘#f’ for the service).

Fixes: #64106

* gnu/services.scm (%modify-services): New procedure.
(modify-services): Use it.
(apply-clauses): Add DELETED-SERVICES argument, change to modify one service
at a time.
* tests/services.scm
("modify-services: delete then modify")
("modify-services: modify then delete")
("modify-services: delete multiple services of the same type")
("modify-services: modify multiple services of the same type"): New tests.

Signed-off-by: Maxim Cournoyer <maxim.cournoyer@gmail.com>
Modified-by: Maxim Cournoyer <maxim.cournoyer@gmail.com>
2023-08-31 23:31:50 -04:00

441 lines
18 KiB
Scheme
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2015-2019, 2022, 2023 Ludovic Courtès <ludo@gnu.org>
;;;
;;; 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 (test-services)
#:use-module (gnu services)
#:use-module (gnu services herd)
#:use-module (gnu services shepherd)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-26)
#:use-module (srfi srfi-34)
#:use-module (srfi srfi-64)
#:use-module (ice-9 match))
(test-begin "services")
(test-equal "services, default value"
'(42 123 234 error)
(let* ((t1 (service-type (name 't1) (extensions '())
(description "")))
(t2 (service-type (name 't2) (extensions '())
(description "")
(default-value 42))))
(list (service-value (service t2))
(service-value (service t2 123))
(service-value (service t1 234))
(guard (c ((missing-value-service-error? c) 'error))
(service t1)))))
(test-assert "service-back-edges"
(let* ((t1 (service-type (name 't1) (extensions '()) (description "")
(compose +) (extend *)))
(t2 (service-type (name 't2) (description "")
(extensions
(list (service-extension t1 (const '()))))
(compose +) (extend *)))
(t3 (service-type (name 't3) (description "")
(extensions
(list (service-extension t2 identity)
(service-extension t1 list)))))
(s1 (service t1 #t))
(s2 (service t2 #t))
(s3 (service t3 #t))
(e (service-back-edges (list s1 s2 s3))))
(and (lset= eq? (e s1) (list s2 s3))
(lset= eq? (e s2) (list s3))
(null? (e s3)))))
(test-equal "fold-services"
;; Make sure 'fold-services' returns the right result. The numbers come
;; from services of type T3; 'xyz 60' comes from the service of type T2,
;; where 60 = 15 × 4 = (1 + 2 + 3 + 4 + 5) × 4.
'(initial-value 5 4 3 2 1 xyz 60)
(let* ((t1 (service-type (name 't1) (extensions '()) (description "")
(compose concatenate)
(extend cons)))
(t2 (service-type (name 't2) (description "")
(extensions
(list (service-extension t1
(cut list 'xyz <>))))
(compose (cut reduce + 0 <>))
(extend *)))
(t3 (service-type (name 't3) (description "")
(extensions
(list (service-extension t2 identity)
(service-extension t1 list)))))
(r (fold-services (cons* (service t1 'initial-value)
(service t2 4)
(map (lambda (x)
(service t3 x))
(iota 5 1)))
#:target-type t1)))
(and (eq? (service-kind r) t1)
(service-value r))))
(test-assert "fold-services, ambiguity"
(let* ((t1 (service-type (name 't1) (extensions '()) (description "")
(compose concatenate)
(extend cons)))
(t2 (service-type (name 't2) (description "")
(extensions
(list (service-extension t1 list)))))
(s (service t2 42)))
(guard (c ((ambiguous-target-service-error? c)
(and (eq? (ambiguous-target-service-error-target-type c)
t1)
(eq? (ambiguous-target-service-error-service c)
s))))
(fold-services (list (service t1 'first)
(service t1 'second)
s)
#:target-type t1)
#f)))
(test-assert "fold-services, missing target"
(let* ((t1 (service-type (name 't1) (extensions '()) (description "")))
(t2 (service-type (name 't2) (description "")
(extensions
(list (service-extension t1 list)))))
(s (service t2 42)))
(guard (c ((missing-target-service-error? c)
(and (eq? (missing-target-service-error-target-type c)
t1)
(eq? (missing-target-service-error-service c)
s))))
(fold-services (list s) #:target-type t1)
#f)))
(test-assert "instantiate-missing-services"
(let* ((t1 (service-type (name 't1) (extensions '()) (description "")
(default-value 'dflt)
(compose concatenate)
(extend cons)))
(t2 (service-type (name 't2) (description "")
(extensions
(list (service-extension t1 list)))))
(s1 (service t1 'hey!))
(s2 (service t2 42)))
(and (lset= equal?
(list (service t1) s2)
(instantiate-missing-services (list s2)))
(equal? (list s1 s2)
(instantiate-missing-services (list s1 s2))))))
(test-assert "instantiate-missing-services, indirect"
(let* ((t1 (service-type (name 't1) (extensions '()) (description "")
(default-value 'dflt)
(compose concatenate)
(extend cons)))
(t2 (service-type (name 't2) (description "")
(default-value 'dflt2)
(compose concatenate)
(extend cons)
(extensions
(list (service-extension t1 list)))))
(t3 (service-type (name 't3) (description "")
(extensions
(list (service-extension t2 list)))))
(s1 (service t1))
(s2 (service t2))
(s3 (service t3 42))
(== (cut lset= equal? <...>)))
(and (== (list s1 s2 s3)
(instantiate-missing-services (list s3)))
(== (list s1 s2 s3)
(instantiate-missing-services (list s1 s3)))
(== (list s1 s2 s3)
(instantiate-missing-services (list s2 s3))))))
(test-assert "instantiate-missing-services, no default value"
(let* ((t1 (service-type (name 't1) (extensions '()) (description "")))
(t2 (service-type (name 't2) (description "")
(extensions
(list (service-extension t1 list)))))
(s (service t2 42)))
(guard (c ((missing-target-service-error? c)
(and (eq? (missing-target-service-error-target-type c)
t1)
(eq? (missing-target-service-error-service c)
s))))
(instantiate-missing-services (list s))
#f)))
(test-assert "shepherd-service-lookup-procedure"
(let* ((s1 (shepherd-service (provision '(s1 s1b)) (start #f)))
(s2 (shepherd-service (provision '(s2 s2b)) (start #f)))
(s3 (shepherd-service (provision '(s3 s3b s3c)) (start #f)))
(lookup (shepherd-service-lookup-procedure (list s1 s2 s3))))
(and (eq? (lookup 's1) (lookup 's1b) s1)
(eq? (lookup 's2) (lookup 's2b) s2)
(eq? (lookup 's3) (lookup 's3b) s3))))
(test-assert "shepherd-service-back-edges"
(let* ((s1 (shepherd-service (provision '(s1)) (start #f)))
(s2 (shepherd-service (provision '(s2))
(requirement '(s1))
(start #f)))
(s3 (shepherd-service (provision '(s3))
(requirement '(s1 s2))
(start #f)))
(e (shepherd-service-back-edges (list s1 s2 s3))))
(and (lset= eq? (e s1) (list s2 s3))
(lset= eq? (e s2) (list s3))
(null? (e s3)))))
(test-equal "shepherd-service-upgrade: nothing to do"
'(() ())
(call-with-values
(lambda ()
(shepherd-service-upgrade '() '()))
list))
(test-equal "shepherd-service-upgrade: one unchanged, one upgraded, one new"
'(() ;unload
((foo))) ;restart
(call-with-values
(lambda ()
;; Here 'foo' is replaced and must be explicitly restarted later
;; because it is still running, whereas 'bar' is upgraded right away
;; because it is not currently running. 'baz' is loaded because it's
;; a new service.
(shepherd-service-upgrade
(list (live-service '(foo) '() #f #t)
(live-service '(bar) '() #f #f)
(live-service '(root) '() #f #t)) ;essential!
(list (shepherd-service (provision '(foo))
(start #t))
(shepherd-service (provision '(bar))
(start #t))
(shepherd-service (provision '(baz))
(start #t)))))
(lambda (unload restart)
(list (map live-service-provision unload)
(map shepherd-service-provision restart)))))
(test-equal "shepherd-service-upgrade: service depended on is not unloaded"
'(((baz)) ;unload
((foo))) ;restart
(call-with-values
(lambda ()
;; Service 'bar' is not among the target services; yet, it must not be
;; unloaded because 'foo' depends on it. 'foo' gets replaced but it
;; must be restarted manually.
(shepherd-service-upgrade
(list (live-service '(foo) '(bar) #f #t)
(live-service '(bar) '() #f #t) ;still used!
(live-service '(baz) '() #f #t))
(list (shepherd-service (provision '(foo))
(start #t)))))
(lambda (unload restart)
(list (map live-service-provision unload)
(map shepherd-service-provision restart)))))
(test-equal "shepherd-service-upgrade: obsolete services that depend on each other"
'(((foo) (bar) (baz)) ;unload
()) ;restart
(call-with-values
(lambda ()
;; 'foo', 'bar', and 'baz' depend on each other, but all of them are
;; obsolete, and thus should be unloaded.
(shepherd-service-upgrade
(list (live-service '(foo) '(bar) #f #t) ;obsolete
(live-service '(bar) '(baz) #f #t) ;obsolete
(live-service '(baz) '() #f #t)) ;obsolete
(list (shepherd-service (provision '(qux))
(start #t)))))
(lambda (unload restart)
(list (map live-service-provision unload)
(map shepherd-service-provision restart)))))
(test-equal "shepherd-service-upgrade: transient service"
;; Transient service must not be unloaded:
;; <https://issues.guix.gnu.org/54812>.
'(((foo)) ;unload
((qux))) ;restart
(call-with-values
(lambda ()
(shepherd-service-upgrade
(list (live-service '(sshd-42) '() #t 42) ;transient
(live-service '(foo) '() #f #t) ;obsolete
(live-service '(qux) '() #f #t)) ;running
(list (shepherd-service (provision '(qux))
(start #t)))))
(lambda (unload restart)
(list (map live-service-provision unload)
(map shepherd-service-provision restart)))))
(test-eq "lookup-service-types"
system-service-type
(and (null? (lookup-service-types 'does-not-exist-at-all))
(match (lookup-service-types 'system)
((one) one)
(x x))))
(test-equal "modify-services: do nothing"
'(1 2 3) ;note: service order must be preserved
(let* ((t1 (service-type (name 't1)
(extensions '())
(description "")))
(t2 (service-type (name 't2)
(extensions '())
(description "")))
(t3 (service-type (name 't3)
(extensions '())
(description "")))
(services (list (service t1 1) (service t2 2) (service t3 3))))
(map service-value
(modify-services services))))
(test-equal "modify-services: delete service"
'(1 4) ;note: service order must be preserved
(let* ((t1 (service-type (name 't1)
(extensions '())
(description "")))
(t2 (service-type (name 't2)
(extensions '())
(description "")))
(t3 (service-type (name 't3)
(extensions '())
(description "")))
(t4 (service-type (name 't4)
(extensions '())
(description "")))
(services (list (service t1 1) (service t2 2)
(service t3 3) (service t4 4))))
(map service-value
(modify-services services
(delete t3)
(delete t2)))))
(test-error "modify-services: delete non-existing service"
#t
(let* ((t1 (service-type (name 't1)
(extensions '())
(description "")))
(t2 (service-type (name 't2)
(extensions '())
(description "")))
(t3 (service-type (name 't2)
(extensions '())
(description "")))
(services (list (service t1 1) (service t2 2))))
(modify-services services
(delete t3))))
(test-equal "modify-services: change value"
'(11 2 33) ;note: service order must be preserved
(let* ((t1 (service-type (name 't1)
(extensions '())
(description "")))
(t2 (service-type (name 't2)
(extensions '())
(description "")))
(t3 (service-type (name 't3)
(extensions '())
(description "")))
(services (list (service t1 1) (service t2 2) (service t3 3))))
(map service-value
(modify-services services
(t1 value => 11)
(t3 value => 33)))))
(test-error "modify-services: change value for non-existing service"
#t
(let* ((t1 (service-type (name 't1)
(extensions '())
(description "")))
(t2 (service-type (name 't2)
(extensions '())
(description "")))
(t3 (service-type (name 't3)
(extensions '())
(description "")))
(services (list (service t1 1) (service t3 3))))
(map service-value
(modify-services services
(t2 value => 22)))))
(test-error "modify-services: delete then modify"
#t
(let* ((t1 (service-type (name 't1)
(extensions '())
(description "")))
(t2 (service-type (name 't2)
(extensions '())
(description "")))
(t3 (service-type (name 't3)
(extensions '())
(description "")))
(services (list (service t1 1) (service t2 2) (service t3 3))))
(map service-value
(modify-services services
(delete t2)
(t2 value => 22)))))
(test-equal "modify-services: modify then delete"
'(2 3)
(let* ((t1 (service-type (name 't1)
(extensions '())
(description "")))
(t2 (service-type (name 't2)
(extensions '())
(description "")))
(t3 (service-type (name 't3)
(extensions '())
(description "")))
(services (list (service t1 1) (service t2 2) (service t3 3))))
(map service-value
(modify-services services
(t1 value => 11)
(delete t1)))))
(test-equal "modify-services: delete multiple services of the same type"
'(1 3)
(let* ((t1 (service-type (name 't1)
(extensions '())
(description "")))
(t2 (service-type (name 't2)
(extensions '())
(description "")))
(t3 (service-type (name 't3)
(extensions '())
(description "")))
(services (list (service t1 1) (service t2 2)
(service t2 2) (service t3 3))))
(map service-value
(modify-services services
(delete t2)))))
(test-equal "modify-services: modify multiple services of the same type"
'(1 12 13 4)
(let* ((t1 (service-type (name 't1)
(extensions '())
(description "")))
(t2 (service-type (name 't2)
(extensions '())
(description "")))
(t3 (service-type (name 't3)
(extensions '())
(description "")))
(services (list (service t1 1) (service t2 2)
(service t2 3) (service t3 4))))
(map service-value
(modify-services services
(t2 value => (+ value 10))))))
(test-end)