mirror of
https://git.in.rschanz.org/ryan77627/guix.git
synced 2024-11-07 07:26:13 -05:00
Add (guix inferior) and (guix scripts repl).
* guix/inferior.scm, guix/scripts/repl.scm, tests/inferior.scm: New files. * Makefile.am (MODULES): Add 'guix/scripts/repl.scm' and 'guix/inferior.scm'. (SCM_TESTS): Add 'tests/inferior.scm'. * doc/guix.texi (Invoking guix repl): New node.
This commit is contained in:
parent
bc6e291ef0
commit
2ca299caf6
5 changed files with 521 additions and 0 deletions
|
@ -85,6 +85,7 @@ MODULES = \
|
|||
guix/nar.scm \
|
||||
guix/derivations.scm \
|
||||
guix/grafts.scm \
|
||||
guix/inferior.scm \
|
||||
guix/gnu-maintenance.scm \
|
||||
guix/self.scm \
|
||||
guix/upstream.scm \
|
||||
|
@ -200,6 +201,7 @@ MODULES = \
|
|||
guix/scripts/substitute.scm \
|
||||
guix/scripts/authenticate.scm \
|
||||
guix/scripts/refresh.scm \
|
||||
guix/scripts/repl.scm \
|
||||
guix/scripts/system.scm \
|
||||
guix/scripts/system/search.scm \
|
||||
guix/scripts/lint.scm \
|
||||
|
@ -357,6 +359,7 @@ SCM_TESTS = \
|
|||
tests/profiles.scm \
|
||||
tests/search-paths.scm \
|
||||
tests/syscalls.scm \
|
||||
tests/inferior.scm \
|
||||
tests/gremlin.scm \
|
||||
tests/bournish.scm \
|
||||
tests/lint.scm \
|
||||
|
|
|
@ -166,6 +166,7 @@ Programming Interface
|
|||
* Derivations:: Low-level interface to package derivations.
|
||||
* The Store Monad:: Purely functional interface to the store.
|
||||
* G-Expressions:: Manipulating build expressions.
|
||||
* Invoking guix repl:: Fiddling with Guix interactively.
|
||||
|
||||
Defining Packages
|
||||
|
||||
|
@ -3267,6 +3268,7 @@ package definitions.
|
|||
* Derivations:: Low-level interface to package derivations.
|
||||
* The Store Monad:: Purely functional interface to the store.
|
||||
* G-Expressions:: Manipulating build expressions.
|
||||
* Invoking guix repl:: Fiddling with Guix interactively.
|
||||
@end menu
|
||||
|
||||
@node Defining Packages
|
||||
|
@ -5544,6 +5546,57 @@ corresponding to @var{obj} for @var{system}, cross-compiling for
|
|||
has an associated gexp compiler, such as a @code{<package>}.
|
||||
@end deffn
|
||||
|
||||
@node Invoking guix repl
|
||||
@section Invoking @command{guix repl}
|
||||
|
||||
@cindex REPL, read-eval-print loop
|
||||
The @command{guix repl} command spawns a Guile @dfn{read-eval-print loop}
|
||||
(REPL) for interactive programming (@pxref{Using Guile Interactively,,, guile,
|
||||
GNU Guile Reference Manual}). Compared to just launching the @command{guile}
|
||||
command, @command{guix repl} guarantees that all the Guix modules and all its
|
||||
dependencies are available in the search path. You can use it this way:
|
||||
|
||||
@example
|
||||
$ guix repl
|
||||
scheme@@(guile-user)> ,use (gnu packages base)
|
||||
scheme@@(guile-user)> coreutils
|
||||
$1 = #<package coreutils@@8.29 gnu/packages/base.scm:327 3e28300>
|
||||
@end example
|
||||
|
||||
@cindex inferiors
|
||||
In addition, @command{guix repl} implements a simple machine-readable REPL
|
||||
protocol for use by @code{(guix inferior)}, a facility to interact with
|
||||
@dfn{inferiors}, separate processes running a potentially different revision
|
||||
of Guix.
|
||||
|
||||
The available options are as follows:
|
||||
|
||||
@table @code
|
||||
@item --type=@var{type}
|
||||
@itemx -t @var{type}
|
||||
Start a REPL of the given @var{TYPE}, which can be one of the following:
|
||||
|
||||
@table @code
|
||||
@item guile
|
||||
This is default, and it spawns a standard full-featured Guile REPL.
|
||||
@item machine
|
||||
Spawn a REPL that uses the machine-readable protocol. This is the protocol
|
||||
that the @code{(guix inferior)} module speaks.
|
||||
@end table
|
||||
|
||||
@item --listen=@var{endpoint}
|
||||
By default, @command{guix repl} reads from standard input and writes to
|
||||
standard output. When this option is passed, it will instead listen for
|
||||
connections on @var{endpoint}. Here are examples of valid options:
|
||||
|
||||
@table @code
|
||||
@item --listen=tcp:37146
|
||||
Accept connections on localhost on port 37146.
|
||||
|
||||
@item --listen=unix:/tmp/socket
|
||||
Accept connections on the Unix-domain socket @file{/tmp/socket}.
|
||||
@end table
|
||||
@end table
|
||||
|
||||
@c *********************************************************************
|
||||
@node Utilities
|
||||
|
|
197
guix/inferior.scm
Normal file
197
guix/inferior.scm
Normal file
|
@ -0,0 +1,197 @@
|
|||
;;; GNU Guix --- Functional package management for GNU
|
||||
;;; Copyright © 2018 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 (guix inferior)
|
||||
#:use-module (srfi srfi-9)
|
||||
#:use-module (srfi srfi-9 gnu)
|
||||
#:use-module (ice-9 match)
|
||||
#:use-module (ice-9 popen)
|
||||
#:export (inferior?
|
||||
open-inferior
|
||||
close-inferior
|
||||
inferior-eval
|
||||
inferior-object?
|
||||
|
||||
inferior-package?
|
||||
inferior-package-name
|
||||
inferior-package-version
|
||||
|
||||
inferior-packages
|
||||
inferior-package-synopsis
|
||||
inferior-package-description))
|
||||
|
||||
;;; Commentary:
|
||||
;;;
|
||||
;;; This module provides a way to spawn Guix "inferior" processes and to talk
|
||||
;;; to them. It allows us, from one instance of Guix, to interact with
|
||||
;;; another instance of Guix coming from a different commit.
|
||||
;;;
|
||||
;;; Code:
|
||||
|
||||
;; Inferior Guix process.
|
||||
(define-record-type <inferior>
|
||||
(inferior pid socket version)
|
||||
inferior?
|
||||
(pid inferior-pid)
|
||||
(socket inferior-socket)
|
||||
(version inferior-version)) ;REPL protocol version
|
||||
|
||||
(define (inferior-pipe directory command)
|
||||
"Return an input/output pipe on the Guix instance in DIRECTORY. This runs
|
||||
'DIRECTORY/COMMAND repl' if it exists, or falls back to some other method if
|
||||
it's an old Guix."
|
||||
(let ((pipe (with-error-to-port (%make-void-port "w")
|
||||
(lambda ()
|
||||
(open-pipe* OPEN_BOTH
|
||||
(string-append directory "/" command)
|
||||
"repl" "-t" "machine")))))
|
||||
(if (eof-object? (peek-char pipe))
|
||||
(begin
|
||||
(close-pipe pipe)
|
||||
|
||||
;; Older versions of Guix didn't have a 'guix repl' command, so
|
||||
;; emulate it.
|
||||
(open-pipe* OPEN_BOTH "guile"
|
||||
"-L" (string-append directory "/share/guile/site/"
|
||||
(effective-version))
|
||||
"-C" (string-append directory "/share/guile/site/"
|
||||
(effective-version))
|
||||
"-C" (string-append directory "/lib/guile/"
|
||||
(effective-version) "/site-ccache")
|
||||
"-c"
|
||||
(object->string
|
||||
`(begin
|
||||
(primitive-load ,(search-path %load-path
|
||||
"guix/scripts/repl.scm"))
|
||||
((@ (guix scripts repl) machine-repl))))))
|
||||
pipe)))
|
||||
|
||||
(define* (open-inferior directory #:key (command "bin/guix"))
|
||||
"Open the inferior Guix in DIRECTORY, running 'DIRECTORY/COMMAND repl' or
|
||||
equivalent. Return #f if the inferior could not be launched."
|
||||
(define pipe
|
||||
(inferior-pipe directory command))
|
||||
|
||||
(setvbuf pipe _IOLBF)
|
||||
(match (read pipe)
|
||||
(('repl-version 0 rest ...)
|
||||
(let ((result (inferior 'pipe pipe (cons 0 rest))))
|
||||
(inferior-eval '(use-modules (guix)) result)
|
||||
(inferior-eval '(use-modules (gnu)) result)
|
||||
(inferior-eval '(define %package-table (make-hash-table))
|
||||
result)
|
||||
result))
|
||||
(_
|
||||
#f)))
|
||||
|
||||
(define (close-inferior inferior)
|
||||
"Close INFERIOR."
|
||||
(close-pipe (inferior-socket inferior)))
|
||||
|
||||
;; Non-self-quoting object of the inferior.
|
||||
(define-record-type <inferior-object>
|
||||
(inferior-object address appearance)
|
||||
inferior-object?
|
||||
(address inferior-object-address)
|
||||
(appearance inferior-object-appearance))
|
||||
|
||||
(define (write-inferior-object object port)
|
||||
(match object
|
||||
(($ <inferior-object> _ appearance)
|
||||
(format port "#<inferior-object ~a>" appearance))))
|
||||
|
||||
(set-record-type-printer! <inferior-object> write-inferior-object)
|
||||
|
||||
(define (inferior-eval exp inferior)
|
||||
"Evaluate EXP in INFERIOR."
|
||||
(define sexp->object
|
||||
(match-lambda
|
||||
(('value value)
|
||||
value)
|
||||
(('non-self-quoting address string)
|
||||
(inferior-object address string))))
|
||||
|
||||
(write exp (inferior-socket inferior))
|
||||
(newline (inferior-socket inferior))
|
||||
(match (read (inferior-socket inferior))
|
||||
(('values objects ...)
|
||||
(apply values (map sexp->object objects)))
|
||||
(('exception key objects ...)
|
||||
(apply throw key (map sexp->object objects)))))
|
||||
|
||||
|
||||
;;;
|
||||
;;; Inferior packages.
|
||||
;;;
|
||||
|
||||
(define-record-type <inferior-package>
|
||||
(inferior-package inferior name version id)
|
||||
inferior-package?
|
||||
(inferior inferior-package-inferior)
|
||||
(name inferior-package-name)
|
||||
(version inferior-package-version)
|
||||
(id inferior-package-id))
|
||||
|
||||
(define (write-inferior-package package port)
|
||||
(match package
|
||||
(($ <inferior-package> _ name version)
|
||||
(format port "#<inferior-package ~a@~a ~a>"
|
||||
name version
|
||||
(number->string (object-address package) 16)))))
|
||||
|
||||
(set-record-type-printer! <inferior-package> write-inferior-package)
|
||||
|
||||
(define (inferior-packages inferior)
|
||||
"Return the list of packages known to INFERIOR."
|
||||
(let ((result (inferior-eval
|
||||
'(fold-packages (lambda (package result)
|
||||
(let ((id (object-address package)))
|
||||
(hashv-set! %package-table id package)
|
||||
(cons (list (package-name package)
|
||||
(package-version package)
|
||||
id)
|
||||
result)))
|
||||
'())
|
||||
inferior)))
|
||||
(map (match-lambda
|
||||
((name version id)
|
||||
(inferior-package inferior name version id)))
|
||||
result)))
|
||||
|
||||
(define (inferior-package-field package getter)
|
||||
"Return the field of PACKAGE, an inferior package, accessed with GETTER."
|
||||
(let ((inferior (inferior-package-inferior package))
|
||||
(id (inferior-package-id package)))
|
||||
(inferior-eval `(,getter (hashv-ref %package-table ,id))
|
||||
inferior)))
|
||||
|
||||
(define* (inferior-package-synopsis package #:key (translate? #t))
|
||||
"Return the Texinfo synopsis of PACKAGE, an inferior package. When
|
||||
TRANSLATE? is true, translate it to the current locale's language."
|
||||
(inferior-package-field package
|
||||
(if translate?
|
||||
'(compose (@ (guix ui) P_) package-synopsis)
|
||||
'package-synopsis)))
|
||||
|
||||
(define* (inferior-package-description package #:key (translate? #t))
|
||||
"Return the Texinfo description of PACKAGE, an inferior package. When
|
||||
TRANSLATE? is true, translate it to the current locale's language."
|
||||
(inferior-package-field package
|
||||
(if translate?
|
||||
'(compose (@ (guix ui) P_) package-description)
|
||||
'package-description)))
|
199
guix/scripts/repl.scm
Normal file
199
guix/scripts/repl.scm
Normal file
|
@ -0,0 +1,199 @@
|
|||
;;; GNU Guix --- Functional package management for GNU
|
||||
;;; Copyright © 2018 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 (guix scripts repl)
|
||||
#:use-module (guix ui)
|
||||
#:use-module (guix scripts)
|
||||
#:use-module (guix utils)
|
||||
#:use-module (guix packages)
|
||||
#:use-module (gnu packages)
|
||||
#:use-module (srfi srfi-1)
|
||||
#:use-module (srfi srfi-37)
|
||||
#:use-module (ice-9 match)
|
||||
#:use-module (rnrs bytevectors)
|
||||
#:autoload (system repl repl) (start-repl)
|
||||
#:autoload (system repl server)
|
||||
(make-tcp-server-socket make-unix-domain-server-socket)
|
||||
#:export (machine-repl
|
||||
guix-repl))
|
||||
|
||||
;;; Commentary:
|
||||
;;;
|
||||
;;; This command provides a Guile REPL
|
||||
|
||||
(define %default-options
|
||||
`((type . guile)))
|
||||
|
||||
(define %options
|
||||
(list (option '(#\h "help") #f #f
|
||||
(lambda args
|
||||
(show-help)
|
||||
(exit 0)))
|
||||
(option '(#\V "version") #f #f
|
||||
(lambda args
|
||||
(show-version-and-exit "guix repl")))
|
||||
(option '(#\t "type") #t #f
|
||||
(lambda (opt name arg result)
|
||||
(alist-cons 'type (string->symbol arg) result)))
|
||||
(option '("listen") #t #f
|
||||
(lambda (opt name arg result)
|
||||
(alist-cons 'listen arg result)))))
|
||||
|
||||
|
||||
(define (show-help)
|
||||
(display (G_ "Usage: guix repl [OPTIONS...]
|
||||
Start a Guile REPL in the Guix execution environment.\n"))
|
||||
(display (G_ "
|
||||
-t, --type=TYPE start a REPL of the given TYPE"))
|
||||
(newline)
|
||||
(display (G_ "
|
||||
-h, --help display this help and exit"))
|
||||
(display (G_ "
|
||||
-V, --version display version information and exit"))
|
||||
(newline)
|
||||
(show-bug-report-information))
|
||||
|
||||
(define (self-quoting? x)
|
||||
"Return #t if X is self-quoting."
|
||||
(letrec-syntax ((one-of (syntax-rules ()
|
||||
((_) #f)
|
||||
((_ pred rest ...)
|
||||
(or (pred x)
|
||||
(one-of rest ...))))))
|
||||
(one-of symbol? string? pair? null? vector?
|
||||
bytevector? number? boolean?)))
|
||||
|
||||
(define user-module
|
||||
;; Module where we execute user code.
|
||||
(let ((module (resolve-module '(guix-user) #f #f #:ensure #t)))
|
||||
(beautify-user-module! module)
|
||||
module))
|
||||
|
||||
(define* (machine-repl #:optional
|
||||
(input (current-input-port))
|
||||
(output (current-output-port)))
|
||||
"Run a machine-usable REPL over ports INPUT and OUTPUT.
|
||||
|
||||
The protocol of this REPL is meant to be machine-readable and provides proper
|
||||
support to represent multiple-value returns, exceptions, objects that lack a
|
||||
read syntax, and so on. As such it is more convenient and robust than parsing
|
||||
Guile's REPL prompt."
|
||||
(define (value->sexp value)
|
||||
(if (self-quoting? value)
|
||||
`(value ,value)
|
||||
`(non-self-quoting ,(object-address value)
|
||||
,(object->string value))))
|
||||
|
||||
(write `(repl-version 0 0) output)
|
||||
(newline output)
|
||||
(force-output output)
|
||||
|
||||
(let loop ()
|
||||
(match (read input)
|
||||
((? eof-object?) #t)
|
||||
(exp
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(let ((results (call-with-values
|
||||
(lambda ()
|
||||
|
||||
(primitive-eval exp))
|
||||
list)))
|
||||
(write `(values ,@(map value->sexp results))
|
||||
output)
|
||||
(newline output)
|
||||
(force-output output)))
|
||||
(lambda (key . args)
|
||||
(write `(exception ,key ,@(map value->sexp args)))
|
||||
(newline output)
|
||||
(force-output output)))
|
||||
(loop)))))
|
||||
|
||||
(define (call-with-connection spec thunk)
|
||||
"Dynamically-bind the current input and output ports according to SPEC and
|
||||
call THUNK."
|
||||
(if (not spec)
|
||||
(thunk)
|
||||
|
||||
;; Note: the "PROTO:" prefix in SPEC is here so that we can eventually
|
||||
;; parse things like "fd:123" in a non-ambiguous way.
|
||||
(match (string-index spec #\:)
|
||||
(#f
|
||||
(leave (G_ "~A: invalid listen specification~%") spec))
|
||||
(index
|
||||
(let ((protocol (string-take spec index))
|
||||
(address (string-drop spec (+ index 1))))
|
||||
(define socket
|
||||
(match protocol
|
||||
("tcp"
|
||||
(make-tcp-server-socket #:port (string->number address)))
|
||||
("unix"
|
||||
(make-unix-domain-server-socket #:path address))
|
||||
(_
|
||||
(leave (G_ "~A: unsupported protocol family~%")
|
||||
protocol))))
|
||||
|
||||
(listen socket 10)
|
||||
(let loop ()
|
||||
(match (accept socket)
|
||||
((connection . address)
|
||||
(if (= AF_UNIX (sockaddr:fam address))
|
||||
(info (G_ "accepted connection~%"))
|
||||
(info (G_ "accepted connection from ~a~%")
|
||||
(inet-ntop (sockaddr:fam address)
|
||||
(sockaddr:addr address))))
|
||||
(dynamic-wind
|
||||
(const #t)
|
||||
(lambda ()
|
||||
(parameterize ((current-input-port connection)
|
||||
(current-output-port connection))
|
||||
(thunk)))
|
||||
(lambda ()
|
||||
(false-if-exception (close-port connection))
|
||||
(info (G_ "connection closed~%"))))))
|
||||
(loop)))))))
|
||||
|
||||
|
||||
(define (guix-repl . args)
|
||||
(define opts
|
||||
;; Return the list of package names.
|
||||
(args-fold* args %options
|
||||
(lambda (opt name arg result)
|
||||
(leave (G_ "~A: unrecognized option~%") name))
|
||||
(lambda (arg result)
|
||||
(leave (G_ "~A: extraneous argument~%") arg))
|
||||
%default-options))
|
||||
|
||||
(with-error-handling
|
||||
(let ((type (assoc-ref opts 'type)))
|
||||
(call-with-connection (assoc-ref opts 'listen)
|
||||
(lambda ()
|
||||
(case type
|
||||
((guile)
|
||||
(save-module-excursion
|
||||
(lambda ()
|
||||
(set-current-module user-module)
|
||||
(start-repl))))
|
||||
((machine)
|
||||
(machine-repl))
|
||||
(else
|
||||
(leave (G_ "~a: unknown type of REPL~%") type))))))))
|
||||
|
||||
;; Local Variables:
|
||||
;; eval: (put 'call-with-connection 'scheme-indent-function 1)
|
||||
;; End:
|
69
tests/inferior.scm
Normal file
69
tests/inferior.scm
Normal file
|
@ -0,0 +1,69 @@
|
|||
;;; GNU Guix --- Functional package management for GNU
|
||||
;;; Copyright © 2018 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-inferior)
|
||||
#:use-module (guix inferior)
|
||||
#:use-module (guix packages)
|
||||
#:use-module (gnu packages)
|
||||
#:use-module (srfi srfi-1)
|
||||
#:use-module (srfi srfi-64))
|
||||
|
||||
(define %top-srcdir
|
||||
(dirname (search-path %load-path "guix.scm")))
|
||||
|
||||
(define %top-builddir
|
||||
(dirname (search-path %load-compiled-path "guix.go")))
|
||||
|
||||
|
||||
(test-begin "inferior")
|
||||
|
||||
(test-equal "open-inferior"
|
||||
'(42 #t)
|
||||
(let ((inferior (open-inferior %top-builddir
|
||||
#:command "scripts/guix")))
|
||||
(and (inferior? inferior)
|
||||
(let ((a (inferior-eval '(apply * '(6 7)) inferior))
|
||||
(b (inferior-eval '(@ (gnu packages base) coreutils)
|
||||
inferior)))
|
||||
(close-inferior inferior)
|
||||
(list a (inferior-object? b))))))
|
||||
|
||||
(test-equal "inferior-packages"
|
||||
(take (sort (fold-packages (lambda (package lst)
|
||||
(alist-cons (package-name package)
|
||||
(package-version package)
|
||||
lst))
|
||||
'())
|
||||
(lambda (x y)
|
||||
(string<? (car x) (car y))))
|
||||
10)
|
||||
(let* ((inferior (open-inferior %top-builddir
|
||||
#:command "scripts/guix"))
|
||||
(packages (inferior-packages inferior)))
|
||||
(and (every string? (map inferior-package-synopsis packages))
|
||||
(begin
|
||||
(close-inferior inferior)
|
||||
(take (sort (map (lambda (package)
|
||||
(cons (inferior-package-name package)
|
||||
(inferior-package-version package)))
|
||||
packages)
|
||||
(lambda (x y)
|
||||
(string<? (car x) (car y))))
|
||||
10)))))
|
||||
|
||||
(test-end "inferior")
|
Loading…
Reference in a new issue