gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[gnunet-scheme] 03/03: concurrency/lost-and-found: New module.


From: gnunet
Subject: [gnunet-scheme] 03/03: concurrency/lost-and-found: New module.
Date: Thu, 10 Feb 2022 18:47:54 +0100

This is an automated email from the git hooks/post-receive script.

maxime-devos pushed a commit to branch master
in repository gnunet-scheme.

commit 7f6e421e9085ffc72815cde41404f484dd803ab6
Author: Maxime Devos <maximedevos@telenet.be>
AuthorDate: Thu Feb 10 17:31:38 2022 +0000

    concurrency/lost-and-found: New module.
    
    It will be used by (gnu gnunet dht client), and later (gnu gnunet
    cadet client).
    
    * gnu/gnunet/concurrency/lost-and-found.scm: New module.
    * tests/lost-and-found.scm: New test.
    * Makefile.am (modules, SCM_TESTS): Register new files.
---
 Makefile.am                               |   2 +
 gnu/gnunet/concurrency/lost-and-found.scm | 237 +++++++++++++++++++++++
 tests/lost-and-found.scm                  | 312 ++++++++++++++++++++++++++++++
 3 files changed, 551 insertions(+)

diff --git a/Makefile.am b/Makefile.am
index 00120f2..b42b6cc 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -47,6 +47,7 @@ modules = \
   \
   gnu/gnunet/concurrency/update.scm \
   gnu/gnunet/concurrency/repeated-condition.scm \
+  gnu/gnunet/concurrency/lost-and-found.scm \
   \
   gnu/gnunet/mq/envelope.scm \
   gnu/gnunet/mq/error-reporting.scm \
@@ -194,6 +195,7 @@ SCM_TESTS = \
   tests/crypto.scm \
   tests/distributed-hash-table.scm \
   tests/form.scm \
+  tests/lost-and-found.scm \
   tests/netstruct.scm \
   tests/time.scm \
   tests/tokeniser.scm
diff --git a/gnu/gnunet/concurrency/lost-and-found.scm 
b/gnu/gnunet/concurrency/lost-and-found.scm
new file mode 100644
index 0000000..8975240
--- /dev/null
+++ b/gnu/gnunet/concurrency/lost-and-found.scm
@@ -0,0 +1,237 @@
+;; This file is part of Scheme-GNUnet
+;; Copyright © 2022 GNUnet e.V.
+;;
+;; Scheme-GNUnet is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU Affero General Public License as published
+;; by the Free Software Foundation, either version 3 of the License,
+;; or (at your option) any later version.
+;;
+;; Scheme-GNUnet 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
+;; Affero General Public License for more details.
+;;
+;; You should have received a copy of the GNU Affero General Public License
+;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
+;;
+;; SPDX-License-Identifier: AGPL-3.0-or-later
+
+;; Author: Maxime Devos
+(define-library (gnu gnunet concurrency lost-and-found)
+  (export make-lost-and-found lost-and-found? collect-lost-and-found-operation
+         make-losable <losable> losable?
+         ;; exported for tests
+         (rename (add-found! #{ add-found!}#)))
+  (import (only (rnrs base)
+               begin let define lambda quote if cond eq? assert cons list)
+         (only (rnrs control)
+               when unless)
+         (only (rnrs records syntactic)
+               define-record-type)
+         (only (guile)
+               make-guardian add-hook! after-gc-hook object-address)
+         (only (ice-9 format)
+               format)
+         (only (srfi srfi-9 gnu)
+               set-record-type-printer!)
+         (only (ice-9 atomic)
+               make-atomic-box atomic-box-ref)
+         (only (fibers conditions)
+               make-condition condition? signal-condition! wait-operation)
+         (only (fibers operations)
+               wrap-operation make-base-operation)
+         ;; TODO: move elsewhere
+         (only (gnu gnunet mq envelope)
+               %%bind-atomic-boxen))
+  (begin
+    (define-record-type (<lost-and-found> make-lost-and-found lost-and-found?)
+      ;; Atomic box of [condition | (found found* ...)].
+      ;; When there is nothing found, the condition is unsignalled.
+      ;;
+      ;; To register something lost, it is added to the list (if any),
+      ;; otherwise the condition is replaced by the lost object, then
+      ;; the condition is signalled.
+      (fields (immutable contents-box lost-and-found-contents-box))
+      (protocol (lambda (%make)
+                 (lambda ()
+                   (%make (make-atomic-box (make-condition)))))))
+
+    (set-record-type-printer!
+     <lost-and-found>
+     (lambda (record port)
+       (format port "#<lost-and-found ~x ~a>"
+              (object-address record)
+              (if (condition?
+                   (atomic-box-ref (lost-and-found-contents-box record)))
+                  "empty"
+                  "non-empty"))))
+
+    ;; TODO: concurrency this operation, not reusable
+    (define (collect-lost-and-found-operation lost-and-found)
+      "Make an operation that will complete when something lost has been
+found and return the newly found objects as a list.  If this operation is
+performed multiple times concurrently on the same lost and found, spurious
+wakeups where the empty list is returned are possible."
+      (%%bind-atomic-boxen
+       ((value (lost-and-found-contents-box lost-and-found) swap!))
+       (let ((old value)
+            (new-condition (make-condition)))
+        (define (loop old)
+          ;; The mutation replacing 'old' by 'value' is detected by
+          ;; the tests "new lost between making the operation and performing
+          ;; it".
+          (define new-old (swap! old new-condition))
+          ;; If a condition, a concurrent
+          ;; 'collect-lost-and-found-operation' has took the found
+          ;; objects, return a spurious empty list.
+          ;;
+          ;; The mutations ‘inverse the condition’, ‘remove this clause’,
+          ;; and ‘return new-old’ are detected by the test "concurrent
+          ;; collecting (light)".
+          ;;
+          ;; The mutation ‘replace ‘new-old’ by ‘old’ or ‘value’’ is detected
+          ;; by "new lost between making the operation and performing it (2)".
+          ;;
+          ;; TODO: detect switching the first two clauses.
+          (cond ((condition? new-old) '())
+                ;; eq? and not a condition --> succesfully replaced a
+                ;; list of found objects with 'new-condition', return
+                ;; the list.
+                ;;
+                ;; The mutations ‘remove this clause’, ‘always return the
+                ;; empty list’, ‘inverse the condition’, ‘replace new-old or
+                ;; old by value’ are detected by the test "unreachable + gc ->
+                ;; moved into lost and found".
+                ((eq? old new-old) old)
+                ;; not eq? --> a race happened, retry
+                ;;
+                ;; The mutations ‘removing this clause’,
+                ;; ‘returning the empty list’ and ‘calling loop twice’ are
+                ;; detected by tests "new lost between making the operation and
+                ;; performing it".
+                (#true (loop new-old))))
+        ;; The mutation ‘use value instead of old’ is detected ‘losing and
+        ;; collecting concurrently’ (somewhat irreproducible).
+        (if (condition? old)
+            (wrap-operation
+             ;; The mutation ‘don't wait for anything’ is detected by
+             ;; the test "block while nothing to collect".
+             ;; The mutation ‘use value instead of old’ is detected by
+             ;; the test "losing and collecting concurrently".
+             (wait-operation old)
+             ;; The mutations 'always return the empty list' and 'call loop
+             ;; twice' are detected by test "new lost between making the
+             ;; operation and performing it (2)".
+             ;;
+             ;; The mutation ‘replace old by value’ _survives_ but seems
+             ;; benign.
+             (lambda () (loop old)))
+            ;; 'collect-lost' added something before we started waiting,
+            ;; return it when asked for (unless a race interferes).
+            (make-base-operation
+             #false ; wrap
+             ;; Try (always succeeds).
+             ;; The mutations ‘always return the empty list’ and
+             ;; 'call loop twice' are rejected by test
+             ;; "unreachable + gc -> moved into lost and found".
+             ;;
+             ;; The mutation ‘replace old by value’ _survives_ but seems
+             ;; benign.
+             (lambda () (lambda () (loop old)))
+             ;; There is no block, only try -- try always succeeds.
+             "do not call me, try always returns!")))))
+
+    (define (add-found! lost-and-found lost)
+      "Add an object @var{lost} to @var{lost-and-found}."
+      (%%bind-atomic-boxen
+       ((value (lost-and-found-contents-box lost-and-found) swap!))
+       (let loop ((old value))
+        ;; The mutations ‘simply run the first branch’, ‘simply run
+        ;; the second branch’, ‘run both branches’ and ‘invert the
+        ;; branch condition’ are detected by test "unreachable + gc ->
+        ;; moved into lost and found".
+        ;;
+        ;; TODO: maybe detect replacing ‘old’ by ‘value’.
+        (if (condition? old)
+            ;; Replace the condition by a list containing lost,
+            ;; then notify the condition.  This ordering is important,
+            ;; otherwise 'collect-lost-and-found-soperation' could
+            ;; be unnecessarily in the ‘spuriously return the empty list’
+            ;; case, even when there aren't multiple concurrent
+            ;; 'collect-lost-and-found-operation' operations.
+            ;;
+            ;; (Though in practice, this would not seem to be a problem,
+            ;; since 'collect-lost-and-found' is called in a loop anyway.)
+            (let ((new-old (swap! old (list lost))))
+              ;; The mutations ‘invert the branch condition’ and ‘do both
+              ;; branches (in order or out-of-order)’ are detected by the test
+              ;; "unreachable + gc -> moved into lost and found".
+              ;;
+              ;; The mutation ‘simply do the second branch’ is detected by
+              ;; test "new lost between making the operation and performing it
+              ;; (2)" (timeout).
+              ;;
+              ;; The mutation ‘simply do the first branch’ is dected by the
+              ;; test "losing and collecting concurrently" (not 100%
+              ;; reproducible).
+              (if (eq? new-old old)
+                  ;; The mutation ‘don't do anything’ is detected by test
+                  ;; "new lost between making the operation and performing it
+                  ;; (2)" (by timeout).
+                  (signal-condition! old)
+                  ;; Race was lost, try again!
+                  ;;
+                  ;; The mutation ‘don't do anything’ is detected by the test
+                  ;; "losing and collecting concurrently".
+                  ;;
+                  ;; The mutation ‘use old instead of new-old’ is detected by
+                  ;; the test "losing and collecting concurrently" (infinite
+                  ;; loop).
+                  ;;
+                  ;; The mutation ‘use value instead of new-value’ is
+                  ;; _survives_ and seems benign, although possibly suboptimal
+                  ;; performance-wise.
+                  (loop new-old)))
+            ;; There is already a list of lost objects, extend it.
+            ;; The mutation ‘replace the first old by value’ causes
+            ;; "concurrent losing" to fail. TODO: replacing the second ‘old’
+            ;; is currently undetected.
+            (let ((new-old (swap! old (cons lost old))))
+              ;; The mutations ‘don't do anything’, ‘invert the condition’,
+              ;; ‘replace old by value in the condition’
+              ;; cause the test "concurrent losing" to fail.
+              ;;
+              ;; The mutations ‘always run’ and ‘replace new-old by value in
+              ;; the condition’ cause an infinite loop (presumambly with
+              ;; unbounded memory!). The mutation ‘run loop twice’ seems to
+              ;; cause an OOM or at least very high memory usage.
+              (unless (eq? new-old old)
+                ;; Race was lost, try again!
+                ;;
+                ;; The mutation ‘replace new-old by old’ causes "concurrent
+                ;; losing" to busy hang.  The mutation ‘replace new-old by
+                ;; value’ survives and seems benign, although perhaps
+                ;; suboptimal performance-wise.
+                (loop new-old)))))))
+
+    (define *guard* (make-guardian))
+
+    (define-record-type (<losable> make-losable losable?)
+      (fields (immutable lost-and-found losable-lost-and-found))
+      (sealed #false)
+      (protocol (lambda (%make)
+                 (lambda (lost-and-found)
+                   (assert (lost-and-found? lost-and-found))
+                   (let ((object (%make lost-and-found)))
+                     (*guard* object)
+                     object)))))
+
+    (define (collect-lost)
+      (define object (*guard*))
+      (when object
+       (add-found! (losable-lost-and-found object) object)
+       ;; Absence detected by test
+       ;; "unreachable + gc -> moved into lost and found"
+       (collect-lost)))
+
+    (add-hook! after-gc-hook (lambda () (collect-lost)))))
diff --git a/tests/lost-and-found.scm b/tests/lost-and-found.scm
new file mode 100644
index 0000000..5ff08b2
--- /dev/null
+++ b/tests/lost-and-found.scm
@@ -0,0 +1,312 @@
+;; This file is part of scheme-GNUnet, a partial Scheme port of GNUnet.
+;; Copyright © 2022 GNUnet e.V.
+;;
+;; Scheme-GNUnet is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU Affero General Public License as published
+;; by the Free Software Foundation, either version 3 of the License,
+;; or (at your option) any later version.
+;;
+;; Scheme-GNUnet 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
+;; Affero General Public License for more details.
+;;
+;; You should have received a copy of the GNU Affero General Public License
+;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
+;;
+;; SPDX-License-Identifier: AGPL-3.0-or-later
+
+;; Author: Maxime Devos
+(define-module (test-lost-and-found))
+(import (ice-9 match)
+        (srfi srfi-1)
+       (gnu gnunet concurrency lost-and-found)
+       (rnrs base)
+       (srfi srfi-64)
+       (fibers conditions)
+       (fibers operations)
+       (fibers channels)
+       (rnrs records syntactic)
+       (fibers)
+       (fibers timers)) ; sleep
+
+(test-begin "lost-and-found")
+
+(define-record-type (<losable+datum> make-losable+datum losable+datum?)
+  (parent <losable>)
+  (fields (immutable datum losable-datum))
+  ;; TODO: why is this necessary?
+  (protocol (lambda (%make)
+             (lambda (lost-and-found foo)
+               ((%make lost-and-found) foo)))))
+
+(define (lose lost-and-found start to/exclusive)
+  "Lose integers from the range [start to/exclusive)."
+  (when (< start to/exclusive)
+    (make-losable+datum lost-and-found start)
+    (lose lost-and-found (+ 1 start) to/exclusive)))
+
+(define (collect-operation lost-and-found)
+  "Make an operation returning the list of found integers
+(make sure to gc before performing the operation!)."
+  (wrap-operation
+   (collect-lost-and-found-operation lost-and-found)
+   (lambda (list)
+     (map losable-datum list))))
+
+(define (collect lost-and-found)
+  "Return a list of found integers (make sure to gc first!)."
+  (perform-operation (collect-operation lost-and-found)))
+
+(define (verify collected from to/exclusive)
+  (define count (- to/exclusive from))
+  (define present (make-bitvector count #false))
+  (for-each (lambda (i)
+             (assert (not (bitvector-bit-set? present (- i from))))
+             (bitvector-set-bit! present i))
+           collected)
+  ;; Presumably due to boehmgc being conservative, this number
+  ;; of elements collected tends can be off by one or two.
+  ;; Allow being 5 elements off.
+  (define fraction (/ (bitvector-count present) (- count 5.)))
+  (pk 'f (+ 0.0 fraction))
+  (assert (>= fraction 1))
+  #true
+  #;
+  (receive (collected\expected ∩)
+      (lset-diff+intersection! = collected (iota count from))
+    (assert (null? collected\expected))
+    ;; Presumably due to boehmgc being conservative, this number
+    ;; of elements collected tends can be off by one or two.
+    ;; Allow being 5 elements off.
+    (let ((fraction (/ (length ∩) (- count 5))))
+      (pk 'f (+ 0.0 fraction))
+      (assert (>= fraction 1))
+      #true)))
+
+(define %count 1000)
+(test-assert "unreachable + gc -> moved into lost and found"
+  (let ((lost-and-found (make-lost-and-found)))
+    (lose lost-and-found 0 %count)
+    (gc)
+    (verify (collect lost-and-found) 0 %count)))
+
+(test-assert "new lost between making the operation and performing it (1)"
+  (let ((lost-and-found (make-lost-and-found)))
+    (lose lost-and-found 0 %count)
+    (gc)
+    (define operation (collect-operation lost-and-found))
+    (lose lost-and-found %count (* 2 %count))
+    (gc)
+    (verify (perform-operation operation) 0 (* 2 %count))))
+
+(test-assert "new lost between making the operation and performing it (2)"
+  (let ((lost-and-found (make-lost-and-found)))
+    (lose lost-and-found 0 %count)
+    ;; <- no gc!
+    (define operation (collect-operation lost-and-found))
+    (lose lost-and-found %count (* 2 %count))
+    (gc)
+    (verify (perform-operation operation) 0 (* 2 %count))))
+
+(test-assert "concurrent collecting (light)"
+  (let ((lost-and-found (make-lost-and-found)))
+    (lose lost-and-found 0 %count)
+    (gc)
+    (define operation1 (collect-operation lost-and-found))
+    (define operation2 (collect-operation lost-and-found))
+    (define result1 (perform-operation operation1))
+    ;; Technically, this is allowed to hang (since everything is
+    ;; collected by result1), but due to implementation details,
+    ;; it doesn't.
+    (define result2 (perform-operation operation2))
+    (verify result1 0 %count)
+    (verify (append result1 result2) 0 %count)))
+
+
+;; TODO: copied from (tests update)
+;; TODO: 1e-4 is not sufficient here, 1e-3 is required to make tests
+;; fail (CPU-dependent?).
+(define expected-blocking-operation
+  (wrap-operation (sleep-operation 1e-3) (lambda () 'blocking)))
+
+(test-eq "block while nothing to collect"
+  'blocking
+  (perform-operation
+   (choice-operation (collect-operation (make-lost-and-found))
+                    expected-blocking-operation)))
+
+(test-assert "delaying performing the operation, some concurrency"
+  (let* ((lost-and-found (make-lost-and-found))
+        ;; 'lost-and-found' currently has a condition, so the
+        ;; (if (condition? old) ...) case should happen here
+        (operation (collect-operation lost-and-found)))
+    ;; Trigger and replace the original condition.
+    (lose lost-and-found 0 %count)
+    (gc)
+    (collect lost-and-found)
+    ;; Run the original operation.
+    (define result
+      (perform-operation
+       (choice-operation operation expected-blocking-operation)))
+    ;; The lost objects were already collected, so blocking is fine.
+    ;; There's a form of concurrency, so a spurious empty list is
+    ;; also allowed.
+    (memq result '(blocking ()))))
+
+(define add-found! #{ add-found!}#)
+
+;; There is no rule against the GC hook being called from within the GC hook,
+;; or the GC hook being called in parallel from another thread running the
+;; GC hook, in case a lot of garbage was generated before the original
+;; invocation of the GC hook was able to finish.
+;;
+;; This seems a bit difficult to reliably trigger, so cheat by manually adding
+;; running 'add-found!' concurrently.
+
+(define (lose* lost-and-found start to/exclusive)
+  "Lose integers from the range [start to/exclusive), bypassing the GC and not
+wrap things in a <losable+datum>."
+  (when (< start to/exclusive)
+    (add-found! lost-and-found start)
+    (lose* lost-and-found (+ 1 start) to/exclusive)))
+
+(define (collect* lost-and-found)
+  "Return a list of found integers (no need to GC, since the GC and guardian 
was
+bypassed by calling @code{add-found!} directly)."
+  (perform-operation (collect-lost-and-found-operation lost-and-found)))
+
+;; In the current implementation of Guile, while to a degree GC is 
parellelised,
+;; gc hooks are serialised (or maybe not, since ‘this hook is run  
+(test-assert "concurrent losing"
+  (run-fibers
+   (lambda ()
+     (define %count/fiber 100000)
+     (define fibers 8)
+     (define start (make-condition))
+     (define done-channel (make-channel))
+     (define lost-and-found (make-lost-and-found))
+     (define (lose/async from to/exclusive)
+       (spawn-fiber
+       (lambda ()
+         (wait start)
+         (lose* lost-and-found from to/exclusive)
+         (put-message done-channel 'done))))
+     (let loop ((i 0))
+       (when (< i fibers)
+        (lose/async (* i %count/fiber) (* (+ 1 i) %count/fiber))
+        (loop (+ i 1))))
+     (signal-condition! start)
+     (let loop ((i 0))
+       (when (< i fibers)
+        (get-message done-channel)
+        (loop (+ i 1))))
+     (verify (collect* lost-and-found) 0 (* %count/fiber fibers)))
+   #:install-suspendable-ports? #false ; unnecessary
+   #:hz 10000))
+
+(test-assert "losing and collecting concurrently"
+  (run-fibers
+   (lambda ()
+     ;; 100000 does not suffice for testing the first
+     ;; '(loop new-old)' in 'add-found!'.
+     (define %count/loser 1000000)
+     (define %losers 8)
+     (define %collectors 8)
+     (define start (make-condition))
+     (define done-losing (make-condition))
+     (define done-channel/losers (make-channel))
+     (define done-channel/collectors (make-channel))
+     (define done-losing-operation
+       (wrap-operation
+       (wait-operation done-losing)
+       (lambda () 'done)))
+     (define lost-and-found (make-lost-and-found))
+     (define (lose/async from to/exclusive)
+       (spawn-fiber
+       (lambda ()
+         (wait start)
+         (lose* lost-and-found from to/exclusive)
+         (put-message done-channel/losers 'done))))
+     ;; vector of list of list of collected objects
+     (define collected (make-vector %collectors))
+     (define (collect/async i)
+       (spawn-fiber
+       (lambda ()
+         (wait start)
+         (let loop ((list-of-list-of-results '()))
+           (define r
+             (perform-operation
+              (choice-operation
+               (collect-lost-and-found-operation lost-and-found)
+               done-losing-operation)))
+           (if (eq? r 'done)
+               (begin
+                 (vector-set! collected i list-of-list-of-results)
+                 (put-message done-channel/collectors 'done))
+               (loop (cons r list-of-list-of-results)))))))
+     ;; Start fibers collecting integers.
+     (let loop ((i 0))
+       (when (< i %collectors)
+        (collect/async i)
+        (loop (+ i 1))))
+     ;; Start fibers losing integers
+     (let loop ((i 0))
+       (when (< i %losers)
+        (lose/async (* i %count/loser) (* (+ 1 i) %count/loser))
+        (loop (+ i 1))))
+     ;; Try starting the collectors and losers start at the same time, to
+     ;; maximise concurrency.
+     (signal-condition! start)
+     (let loop ((i 0))
+       (when (< i %losers)
+        (get-message done-channel/losers)
+        (loop (+ i 1))))
+     (signal-condition! done-losing)
+     ;; Wait for 'collected' to be initialised.
+     (let loop ((i 0))
+       (when (< i %collectors)
+        (get-message done-channel/collectors)
+        (loop (+ i 1))))
+     ;; Do like 'verify' does, without the - 5 because the GC
+     ;; was bypassed.
+     (define results (make-bitvector (* %count/loser %losers)))
+     (define (register-result! i)
+       (assert (not (bitvector-bit-set? results i)))
+       (bitvector-set-bit! results i))
+     (let loop ((i 0))
+       (when (< i %collectors)
+        (for-each
+         (lambda (list)
+           (for-each register-result! list))
+         (vector-ref collected i))
+        (loop (+ i 1))))
+     (define fraction (/ (bitvector-count results) (bitvector-length results)))
+     (pk 'f (+ 0.0 fraction))
+     (assert (>= fraction 1)))
+   #:install-suspendable-ports? #false ; unnecessary
+   #:hz 10000))
+
+(test-assert "lost-and-found as a string (empty)"
+  (let* ((l (make-lost-and-found))
+        (expected (format #f "#<lost-and-found ~x empty>"
+                          (object-address l)))
+        (found (object->string l)))
+    (string=? expected found)))
+
+;; It is important to _not_ print the objects inside the lost-and-found,
+;; because <losable> objects keep a lost-and-found in their fields and hence
+;; printing these objects would lead to infinite recursion.
+(test-assert "lost-and-found as a string (non-empty)"
+  (let* ((l (make-lost-and-found))
+        (expected (format #f "#<lost-and-found ~x non-empty>"
+                          (object-address l))))
+    (add-found! l (make-losable l))
+    (define found (object->string l))
+    (string=? expected found)))
+
+;; The exception is better raised during the construction of the
+;; <losable> than during the after-gc hook.
+(test-error "make-losable without lost-and-found" (make-losable 'bogus))
+
+(test-end "lost-and-found")

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]