123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878 |
- ;; This file is part of scheme-GNUnet, a partial Scheme port of GNUnet.
- ;; Copyright (C) 2021, 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
- (define-module (test-distributed-hash-table))
- (import (ice-9 match)
- (ice-9 binary-ports)
- (quickcheck)
- (quickcheck arbitrary)
- (quickcheck generator)
- (quickcheck property)
- (gnu gnunet dht client)
- (gnu gnunet dht network)
- (gnu gnunet dht struct)
- (gnu gnunet utils bv-slice)
- (gnu gnunet utils hat-let)
- (gnu gnunet util struct)
- (gnu gnunet netstruct syntactic)
- (only (gnu gnunet netstruct procedural) u64/big)
- (gnu gnunet hashcode struct)
- (gnu gnunet block)
- (gnu gnunet message protocols)
- (gnu gnunet mq)
- (gnu gnunet mq error-reporting)
- (gnu gnunet mq handler)
- (gnu gnunet mq-impl stream)
- (gnu extractor enum)
- (rnrs exceptions)
- (rnrs conditions)
- (rnrs base)
- (rnrs bytevectors)
- (srfi srfi-26)
- (srfi srfi-64)
- (fibers conditions)
- (fibers channels)
- (fibers operations)
- (fibers scheduler)
- (fibers timers) ; sleep
- (tests utils))
- ;; Use the @code{error} from Guile, not RnRS.
- (define error (@ (guile) error))
- ;; Copied from tests/bv-slice.scm.
- (define-syntax-rule (test-missing-caps test-case what permitted required code)
- (test-equal test-case
- (list what permitted required)
- (guard (c ((missing-capabilities? c)
- (list (missing-capabilities-what c)
- (missing-capabilities-permitted c)
- (missing-capabilities-required c))))
- code)))
- (define-syntax-rule (test-overly-large-datum test-case who canonical-type
- length)
- (test-equal test-case
- (list who canonical-type length)
- (guard (c ((overly-large-datum? c)
- (list (overly-large-datum-who c)
- (missing-capabilities-permitted c)
- (missing-capabilities-required c))))
- code)))
- ;; It's easy to accidentally swap the min and the max,
- ;; or use theoretical bounds instead of effective bounds.
- (test-begin "bound-replication-level")
- (define-syntax test-bound-equals
- (syntax-rules (->)
- ((_ (name argument -> expected) ...)
- (begin
- (test-equal name (list expected)
- (call-with-values
- (lambda ()
- (bound-replication-level argument))
- list))
- ...))))
- (test-bound-equals
- ;; Boundaries of set of fixed points
- ("effective minimum" %effective-minimum-replication-level
- -> %effective-minimum-replication-level)
- ("effective maximum" %effective-maximum-replication-level
- -> %effective-maximum-replication-level)
- ;; off by one
- ("zero" ; remove this test if %effective-minimum-replication-level becomes zero
- (begin (assert (> %effective-minimum-replication-level %minimum-replication-level))
- %effective-minimum-replication-level)
- -> %effective-minimum-replication-level)
- ("effective maximum + 1"
- (begin (assert (< %effective-maximum-replication-level %maximum-replication-level))
- (+ 1 %effective-maximum-replication-level))
- -> %effective-maximum-replication-level)
- ;; Extreme values
- ("theoretical minimum" %minimum-replication-level
- -> %effective-minimum-replication-level)
- ("theoretical maximum" %maximum-replication-level
- -> %effective-maximum-replication-level))
- (define between
- (map (cut + %effective-minimum-replication-level <>)
- (iota (- %effective-maximum-replication-level
- %effective-minimum-replication-level))))
- ;; Inner fixed points
- (test-equal "between effective extrema"
- between
- (map bound-replication-level between))
- (test-error "too large" (bound-replication-level (+ 1 %maximum-replication-level)))
- (test-error "way too large" (bound-replication-level (* #e1e20 %maximum-replication-level)))
- (test-error "too small" (bound-replication-level (- %minimum-replication-level 1)))
- (test-error "way too small" (bound-replication-level (- %minimum-replication-level #e1e20)))
- (test-error "non-numeric" (bound-replication-level 'what))
- (define (make-slice/read-write* size)
- "Like @code{make-slice/read-write*}, but fill the slice with random data."
- (define s (make-slice/read-write size))
- (let^ ((/o/ loop (i 0))
- (? (>= i size) s))
- (slice-u8-set! s i (random 256))
- (loop (+ i 1))))
- (define* (make-a-datum #:key
- (type 0)
- (key (make-slice/read-write* (sizeof /hashcode:512 '())))
- (value (make-slice/read-write 0))
- (expiration (random (expt 2 64))))
- (make-datum type key value #:expiration expiration))
- (test-assert "datum?"
- (datum? (make-a-datum)))
- (test-equal "not a datum"
- '(#false #false #false)
- (map datum? (list #false 'symbol (make-slice/read-write 0))))
- ;; For efficiency reasons, make sure the storage is reused.
- ;;
- ;; This verifies constructing a record and extracting a field from the record
- ;; end ups with the value passed to the constructor, as a readable bytevector
- ;; slice -- the writability of the original slice, if any, is removed.
- (define (slice-property-test test-case generate-slice stuff? slice->stuff stuff-slice)
- (test-assert test-case
- ;; only evaluate once, because eq? will be required
- (let* ((slice (generate-slice))
- (stuff (slice->stuff slice))
- (new-slice (stuff-slice stuff)))
- (and (stuff? stuff)
- (slice-readable? new-slice)
- (not (slice-writable? new-slice))
- (eq? (slice-bv slice) (slice-bv new-slice))
- (= (slice-length slice) (slice-length new-slice))))))
- (define-syntax-rule (datum-key-test test-case k)
- (slice-property-test test-case (lambda () k) datum?
- (lambda (s) (make-a-datum #:key s)) datum-key))
- (define-syntax-rule (datum-value-test test-case v)
- (slice-property-test test-case (lambda () v) datum?
- (lambda (s) (make-a-datum #:value s)) datum-value))
- (define-syntax-rule (datum-type-test test-case type type/integer)
- (test-equal test-case
- type/integer
- (datum-type (make-a-datum #:type type))))
- (datum-key-test "datum-key"
- (make-slice/read-write* (sizeof /hashcode:512 '())))
- (datum-key-test "datum-key, read-only is sufficient"
- (slice/read-only
- (make-slice/read-write*
- (sizeof /hashcode:512 '()))))
- (test-missing-caps
- "datum key must be readable"
- 'key
- CAP_WRITE
- CAP_READ
- (make-a-datum #:key (slice/write-only (make-slice/read-write*
- (sizeof /hashcode:512 '())))))
- ;; AFAIK a zero length value is allowed, albeit somewhat pointless?
- (datum-value-test "datum-value, length 0" (make-slice/read-write 0))
- (datum-value-test "datum-value, maximal length"
- (make-slice/read-write* %max-datum-value-length))
- (datum-value-test "datum-value" (make-slice/read-write* 900))
- (define (test-datum-overly-large test-case type type/integer length)
- (test-equal test-case
- (list 'make-datum type/integer length)
- (guard (c ((overly-large-datum? c)
- (list (condition-who c)
- (overly-large-datum-type c)
- (overly-large-datum-length c))))
- (make-a-datum #:type type #:value (make-slice/read-write* length)))))
- (test-datum-overly-large
- "datum-value, too large (1, numeric type)" 19 19
- (* 2 %max-datum-value-length))
- (test-datum-overly-large
- "datum-value, too large (2, numeric type)" 19 19
- (* 2 %max-datum-value-length))
- (test-datum-overly-large
- "datum-value, too large (1, symbolic type)" (symbol-value block-type block:revocation) 12
- (* 2 %max-datum-value-length))
- (datum-type-test "datum-key, symbolic type (1)"
- (symbol-value block-type block:consensus-element) 25)
- (datum-type-test "datum-key, symbolic type (2)"
- (symbol-value block-type block:dht:hello) 7)
- (datum-type-test "datum-key, known numeric type (1)" 7 7)
- (datum-type-test "datum-key, known numeric type (2)" 8 8)
- (datum-type-test "datum-key, unknown numeric type" 4294967295 4294967295)
- (test-error "datum-type, out-of-bounds" (make-a-datum #:type 4294967296))
- (test-error "datum-type, wrong enumeration"
- (make-a-datum #:type (symbol-value message-type msg:util:dummy)))
- (test-error "datum-type, wrong type (1)" (make-a-datum #:type 'foo))
- ;; This detected a bug!
- (test-error "datum-type, wrong type (2)" (make-a-datum #:type 1.0))
- (define (slice->bytevector s)
- (define b (make-bytevector (slice-length s)))
- (define s2 (bv-slice/read-write b))
- (slice-copy! s s2)
- b)
- (define (query->sexp z)
- (list (query-type z) (slice->bytevector (query-key z))
- (query-desired-replication-level z)))
- (define (datum->sexp z)
- (list (datum-type z)
- (slice->bytevector (datum-key z))
- (slice->bytevector (datum-value z))
- (datum-expiration z)))
- (define (insertion->sexp z)
- (list (datum->sexp (insertion->datum z))
- (insertion-desired-replication-level z)))
- (define (search-result->sexp z)
- (list (slice->bytevector (search-result-get-path z))
- (slice->bytevector (search-result-put-path z))
- (datum->sexp (search-result->datum z))))
- (define (query=? x y)
- (equal? (query->sexp x) (query->sexp y)))
- (define (datum=? x y)
- (equal? (datum->sexp x) (datum->sexp y)))
- (define (search-result=? x y)
- (equal? (search-result->sexp x) (search-result->sexp y)))
- (define (insertion=? x y)
- (equal? (insertion->sexp x) (insertion->sexp y)))
- (define (query-independent? x y)
- (slice-independent? (query-key x) (query-key y)))
- (define (datum-independent? x y)
- (and (slice-independent? (datum-key x) (datum-key y))
- (slice-independent? (datum-value x) (datum-value y))))
- (define (insertion-independent? x y)
- (datum-independent? (insertion->datum x) (insertion->datum y)))
- (define (search-result-independent? x y)
- (and (datum-independent? (search-result->datum x) (search-result->datum y))
- (slice-independent? (search-result-get-path x)
- (search-result-get-path y))
- (slice-independent? (search-result-put-path x)
- (search-result-put-path y))))
- (test-assert "copy-query: equal and independent"
- (let* ((old-key (make-slice/read-write* (sizeof /hashcode:512 '())))
- (type (random 65536))
- (desired-replication-level (+ 1 %maximum-replication-level))
- (old (make-query type old-key))
- (new (copy-query old)))
- (and (query=? old new)
- (query-independent? old new))))
- (test-assert "copy-datum: equal and independent"
- ;; A least in Guile 3.0.5, all bytevectors of length 0 are eq?,
- ;; so let the value be non-empty such that datum-independent?
- ;; can return #true.
- (let* ((old-key (make-slice/read-write* (sizeof /hashcode:512 '())))
- (old-value (make-slice/read-write* 70))
- (old (make-a-datum #:key old-key #:value old-value #:expiration 777))
- (new (copy-datum old)))
- (and (datum=? old new)
- (datum-independent? old new))))
- (define (path-length->size l)
- (* l (sizeof /dht:path-element '())))
- ;; Detected a bug: the datum was not copied
- (test-assert "copy-search-result: equal and independent"
- (let* ((old-key (make-slice/read-write* (sizeof /hashcode:512 '())))
- (old-value (make-slice/read-write* 70))
- (old-get-path (make-slice/read-write* (path-length->size 5)))
- (old-put-path (make-slice/read-write* (path-length->size 9)))
- (old-datum (make-a-datum #:value old-value #:expiration 555))
- (old (datum->search-result old-datum #:get-path old-get-path
- #:put-path old-put-path))
- (new (copy-search-result old)))
- (and (search-result=? old new)
- (search-result-independent? old new))))
- (test-assert "copy-insertion: equal and independent"
- (let* ((old-value (make-slice/read-write* 71))
- (old-datum (make-a-datum #:value old-value))
- (old
- (datum->insertion old-datum #:desired-replication-level (random 8)))
- (new (copy-insertion old)))
- (and (insertion=? old new)
- (insertion-independent? old new))))
- (define-syntax-rule (search-result-get-path-slice-test test-case k)
- (slice-property-test test-case (lambda () k) search-result?
- (lambda (s) (datum->search-result (make-a-datum)
- #:get-path s))
- search-result-get-path))
- (define-syntax-rule (search-result-put-path-slice-test test-case k)
- (slice-property-test test-case (lambda () k) search-result?
- (lambda (s) (datum->search-result (make-a-datum)
- #:put-path s))
- search-result-put-path))
- ;; These detected a bug: the capabilities were not restricted!
- ;; TODO: can there be a get path without a put path?
- (search-result-get-path-slice-test
- "search-result-get-path, slice" (make-slice/read-write* (path-length->size 7)))
- (search-result-get-path-slice-test
- "search-result-get-path, empty" (make-slice/read-write 0))
- (search-result-put-path-slice-test
- "search-result-put-path, slice" (make-slice/read-write* (path-length->size 7)))
- (search-result-put-path-slice-test
- "search-result-put-path, empty" (make-slice/read-write 0))
- (test-equal "search-result-get-path, none"
- (list #false) ; TODO: drop 'list' when SRFI-64 bug is fixed
- (list (search-result-get-path (datum->search-result (make-a-datum)))))
- (test-equal "search-result-put-path, none"
- (list #false) ; TODO: drop 'list' when SRFI-64 bug is fixed
- (list (search-result-put-path (datum->search-result (make-a-datum)))))
- (test-missing-caps
- "search-result get-path must be readable"
- 'get-path
- CAP_WRITE
- CAP_READ
- (datum->search-result
- (make-a-datum) #:get-path
- (slice/write-only (make-slice/read-write* (path-length->size 7)))))
- (test-missing-caps
- "search-result put-path must be readable"
- 'put-path
- CAP_WRITE
- CAP_READ
- (datum->search-result
- (make-a-datum) #:put-path
- (slice/write-only (make-slice/read-write* (path-length->size 7)))))
- (define (test-malformed-path test-case what keyword)
- (test-assert test-case
- (quickcheck
- (property
- ((elements $natural)
- (remainder
- (arbitrary
- (gen (choose-integer 1 (- (sizeof /dht:path-element '()) 1)))
- (xform #false))))
- (let ((size (+ remainder (* (sizeof /dht:path-element '())))))
- (equal? (guard (c ((malformed-path? c)
- (list (condition-who c)
- (malformed-path-what c)
- (malformed-path-size c))))
- (datum->search-result
- (make-a-datum) keyword
- (make-slice/read-write* size)))
- (list 'datum->search-result what size)))))))
- (test-malformed-path
- "get-path size must be a multiple of the size of a path element"
- 'get-path #:get-path)
- (test-malformed-path
- "put-path size must be a multiple of the size of a path element"
- 'put-path #:put-path)
- ;;;
- ;;; Test client<->service communication.
- ;;;
- ;;; Currently, the following operations are tested:
- ;;;
- ;;; * [x] insertion (@code{put!})
- ;;; * [x] retrieval (@code{start-get!})
- ;;; * [x] disconnecting
- ;;; * [ ] monitoring
- ;;;
- ;;; In the following contexts:
- ;;;
- ;;; * [x] nothing special
- ;;; * [ ] after a reconnection
- ;;; * [ ] requested before a reconnection, without being processed
- ;;; before the reconnection.
- ;;; * [ ] requested (and started) before a reconnection and continued
- ;;; after the reconnection
- ;;;
- ;;; Cancelling, closing the connection, parallelism and multiple
- ;;; in-progress requests are currently untested (TBD and implemented!).
- (define i (datum->insertion (make-a-datum) #:desired-replication-level 7))
- (define (no-error-handler . e)
- (pk 'e e)
- (error "no error handler"))
- ;; TODO: would be nice to turn this in a real service
- ;; (gnu gnunet dht service).
- (define* (simulate-dht-service #:optional (explode (make-condition)))
- "Simulate a DHT service, remembering all insertions and ignoring expiration
- and replication. Cancellation is ignored (TODO). Only a single client is
- supported. When @var{explode} is signalled, the connection is closed."
- (define (slice->bv slice)
- (define bv (make-bytevector (slice-length slice)))
- (define bv/slice (bv-slice/read-write bv))
- (slice-copy! slice bv/slice)
- bv)
- (define (query->key query)
- (cons (query-type query) (slice->bv (query-key query))))
- (define (insertion->key insertion)
- (define datum (insertion->datum insertion))
- (cons (datum-type datum) (slice->bv (datum-key datum))))
- ;; Mapping from (numeric type + key bytevector)
- ;; --> (list of value . interested mq channels)
- (define table (make-hash-table))
- (define table-channel (make-channel))
- (define mq)
- (define mq-defined (make-condition))
- (define (handle-table spawn-fiber)
- (define (put-message/async channel message)
- (assert (channel? channel))
- (spawn-fiber
- (lambda ()
- (put-message channel message))))
- (match (perform-operation
- (choice-operation (get-operation table-channel)
- (wrap-operation (wait-operation explode)
- (lambda () 'explode))))
- ('explode
- (wait mq-defined)
- (close-queue! mq))
- (('start-get! query response-channel)
- (let* ((key (query->key query))
- (old (hash-ref table key '(() . ())))
- (old-values (car old))
- (channels (cdr old)))
- ;; Send currently known values.
- (for-each
- (lambda (old-value)
- (put-message/async response-channel old-value))
- old-values)
- ;; Send future values to the channel as well.
- (hash-set! table key
- `(,old-values ,response-channel ,@channels))))
- (('put! insertion)
- (let* ((key (insertion->key insertion))
- (old (hash-ref table key '(() . ())))
- (old-values (car old))
- (channels (cdr old))
- (new-values (cons insertion old-values)))
- ;; Send the new value.
- (for-each
- (lambda (response-channel)
- (put-message/async response-channel insertion))
- channels)
- (hash-set! table key `(,new-values . ,channels)))))
- (handle-table spawn-fiber))
- (lambda (port spawn-fiber)
- (spawn-fiber (lambda () (handle-table spawn-fiber)))
- (let^ ((! (simple-message-handler type* handle!*)
- (message-handler
- (type type*)
- ((interpose foo) foo)
- ((well-formed? s) #true)
- ((handle! slice) (handle!* slice))))
- (!^ (handle/put! message)
- "Respond to a @code{/:msg:dht:client:put} message."
- ((<-- (insertion _) (analyse-client-put message))
- (! insertion (copy-insertion insertion)))
- (put-message table-channel `(put! ,insertion)))
- (!^ (handle/start-get! message)
- ""
- ((! channel (make-channel))
- (<-- (query unique-id _) (analyse-client-get message))
- (! query (copy-query query)))
- (put-message table-channel `(start-get! ,query ,channel))
- (spawn-fiber
- (lambda ()
- (let^ ((/o/ loop)
- (! insertion (get-message channel))
- ;; The tests don't require get-path/put-path.
- (! search-result (datum->search-result
- (insertion->datum insertion)))
- (! message (construct-client-result search-result
- unique-id)))
- (wait mq-defined)
- (send-message! mq message)
- (loop))))
- (values))
- (! h (message-handlers
- (simple-message-handler
- (symbol-value message-type msg:dht:client:put)
- handle/put!)
- ;; TODO: handle properly
- (simple-message-handler
- (symbol-value message-type msg:dht:client:get:stop)
- (lambda (slice) (values)))
- (simple-message-handler
- (symbol-value message-type msg:dht:client:get)
- handle/start-get!))))
- (set! mq
- (port->message-queue port h no-error-handler #:spawn spawn-fiber))
- (signal-condition! mq-defined)
- (values))))
- (test-equal "put! sends one message to service, after connecting"
- i
- (let^ ((! connected? #false)
- (! (connected)
- (assert (not connected?))
- (set! connected? #true))
- (! message #false)
- (! message-received (make-condition))
- (! (handle slice)
- (when message
- (error "already received"))
- (set! message slice)
- (signal-condition! message-received))
- (! h (message-handlers
- (message-handler
- (type (symbol-value message-type msg:dht:client:put))
- ((interpose foo) foo)
- ((well-formed? s) #true)
- ((handle! slice) (handle slice))))))
- (call-with-services/fibers
- `(("dht" . ,(lambda (port spawn-fiber)
- (define mq
- (port->message-queue port h no-error-handler
- #:spawn spawn-fiber))
- (values))))
- (lambda (config spawn-fiber)
- (define server
- (connect config #:connected connected #:spawn spawn-fiber))
- (put! server i)
- (wait message-received)
- (pk 'server server) ; keep 'server' reachable
- (assert connected?)
- (assert message)
- (let^ ((<-- (insertion _)
- (analyse-client-put message)))
- ;; TODO: copy to make equal? work
- ;; (TODO: define equal? for slices)
- (copy-insertion insertion))))))
- ;; Squat two message types for tests below.
- (define type:ping 7)
- (define type:pong 8)
- (test-assert "synchronuous ping-pong with multiple balls (no interruptions, no cancellation)"
- (call-with-services/fibers
- `(("dht" . ,(simulate-dht-service)))
- (lambda (config spawn-fiber)
- (define N_ROUNDS 50)
- (define server
- (connect config #:spawn spawn-fiber))
- (define (round->key round)
- (define key (make-slice/read-write (sizeof /hashcode:512 '())))
- (slice-u64-set! key 0 round (endianness little))
- key)
- (define (make-a-insertion type round j)
- (define key (round->key round))
- (define value (make-slice/read-write 8))
- (slice-u64-set! value 0 j (endianness little))
- (datum->insertion (make-datum type key value)))
- (define (make-a-query type round)
- (define key (round->key round))
- (make-query type key))
- (define (n-responses-for-round round)
- (+ 1 (mod round 8)))
- (define (ping/pong type round)
- ;; round: number (used as key)
- ;; j: value
- ;;
- ;; Multiple values are inserted for the same key,
- ;; to test iteration.
- (let loop ((j 0))
- (when (< j (n-responses-for-round round))
- (put! server (make-a-insertion type round j))
- (loop (+ 1 j)))))
- (define (search-result->j type search-result)
- (define datum (search-result->datum search-result))
- (define value (datum-value datum))
- (assert (= (slice-length value) 8)) ; u64
- (assert (= type (datum-type datum)))
- (slice-u64-ref value 0 (endianness little)))
- (define (wait-for-values type round)
- (define done (make-condition))
- (define responses '())
- (define (found search-result)
- (set! responses
- (cons (search-result->j type search-result) responses))
- (define length/current (length responses))
- (define length/expected (n-responses-for-round round))
- (when (>= length/current length/expected)
- ;; Duplicated responses might happen in practice, but should
- ;; be avoided when feasible.
- (assert (= length/current length/expected))
- (assert (equal? (sort responses <) (iota length/expected)))
- ;; TODO: cancel query
- (signal-condition! done)))
- (define search (start-get! server (make-a-query type round) found
- ;; Not testing cancellation on GC here.
- #:linger? #true))
- (wait done))
- (define* (ping/pong* this-type other-type round)
- (when (< round N_ROUNDS)
- (ping/pong this-type round)
- (wait-for-values other-type round)
- (ping/pong* this-type other-type (+ 1 round))))
- (define (spawn-ping/pong* this-type other-type)
- (define done (make-condition))
- (spawn-fiber
- (lambda ()
- (ping/pong* this-type other-type 0)
- (signal-condition! done)))
- done)
- (define ping (spawn-ping/pong* type:ping type:pong))
- (define pong (spawn-ping/pong* type:pong type:ping))
- (wait ping)
- (wait pong)
- #true)))
- (test-assert "(DHT) close, not connected --> all fibers stop, no callbacks called"
- (close-not-connected-no-fallbacks "dht" connect disconnect!))
- (test-assert "(DHT) garbage collectable"
- (garbage-collectable "dht" connect))
- (define* (determine-reported-errors proc #:key (n-connections 1) (n-errors 1))
- (call-with-spawner/wait*
- (lambda (config spawn)
- (define errors '())
- (define currently-connected? #false)
- (define times-connected 0)
- (define times-errored 0)
- (define finally-disconnected-c (make-condition))
- (define all-errors-c (make-condition))
- (parameterize ((error-reporter (lambda foo
- (assert (> times-connected 0))
- (set! times-errored (+ 1 times-errored))
- (set! errors (cons foo errors))
- (when (>= times-errored n-errors)
- (signal-condition! all-errors-c)))))
- (define (connected)
- (assert (not currently-connected?))
- (set! currently-connected? #true)
- (set! times-connected (+ 1 times-connected))
- (assert (<= times-connected n-connections)))
- (define (disconnected)
- (assert currently-connected?)
- (set! currently-connected? #false)
- (when (= times-connected n-connections)
- (signal-condition! finally-disconnected-c)))
- (define server
- (connect config #:connected connected #:disconnected disconnected
- #:spawn spawn))
- ;; Give 'error-reporter' a chance to be called too often
- (sleep 0.001)
- ;; The error handler and 'disconnected' are called in no particular
- ;; order, so we have to wait for both.
- (wait finally-disconnected-c)
- (wait all-errors-c)
- ;; keep 'server' reachable long enough.
- (pk server)
- (and (not currently-connected?)
- (= times-connected n-connections) errors)))
- `(("dht" . ,proc))))
- (define (put-ill-formed-message port)
- (define b (make-bytevector (sizeof /:message-header '())))
- (define s (slice/write-only (bv-slice/read-write b)))
- (set%! /:message-header '(type) s
- (value->index (symbol-value message-type msg:dht:client:result)))
- (set%! /:message-header '(size) s (slice-length s))
- (put-bytevector port b))
- (test-equal "(DHT) ill-formed message from service --> all fibers stop, 'connected' and 'disconnected' called"
- `((logic:ill-formed
- ,(value->index (symbol-value message-type msg:dht:client:result))))
- (determine-reported-errors
- (lambda (port spawn-fiber)
- (put-ill-formed-message port)
- (close-port port))))
- ;; Allow reconnecting a few times and eventually ensure a permanent
- ;; disconnecting to make the test terminate.
- (define n-connections 7)
- (test-equal "(DHT) end-of-file --> reconnect (all fibers eventually stop)"
- `((logic:ill-formed
- ,(value->index (symbol-value message-type msg:dht:client:result))))
- (determine-reported-errors
- (let ((i 0))
- (lambda (port spawn-fiber)
- (set! i (+ i 1))
- (assert (<= i n-connections))
- (when (= i n-connections)
- (put-ill-formed-message port))
- (close-port port)))
- #:n-connections n-connections))
- ;; TODO: would be nice to test that old requests are submitted again
- ;; The aim is to show that the search callback can start search requests
- ;; of its own without any problems. While we're at it, the search results
- ;; are verified.
- ;;
- ;; First 'loop' searches for key 0, then for key 1 inside the search result
- ;; callback, etc.
- (test-assert "search callback re-entrancy"
- (call-with-services/fibers
- `(("dht" . ,(simulate-dht-service)))
- (lambda (config spawn-fiber)
- (define server (connect config))
- (define ROUNDS 20)
- (define type 0) ; arbitrary
- (define (make-a-query round)
- (define key (make-slice/read-write (sizeof /hashcode:512 '())))
- (slice-u64-set! key 0 round (endianness big))
- (make-query type key))
- (define (value round)
- (expt 2 round))
- (define done (make-condition))
- (let loop ((round 0))
- (define found? #false)
- (if (< round ROUNDS)
- (start-get! server (make-a-query round)
- (lambda (search-result)
- (define d (search-result->datum search-result))
- (assert (= round
- (slice-u64-ref (datum-key d) 0
- (endianness big))))
- (assert (= (value round)
- (slice-u64-ref (datum-value d) 0
- (endianness big))))
- (assert (not found?))
- (set! found? #true)
- (loop (+ round 1)))
- ;; Cancellation is tested elsewhere, don't automatically
- ;; cancel.
- #:linger? #true)
- (signal-condition! done)))
- (let loop ((round 0))
- (define key-s (make-slice/read-write (sizeof /hashcode:512 '())))
- (define value-s (make-slice/read-write (sizeof u64/big '())))
- (slice-u64-set! key-s 0 round (endianness big))
- (slice-u64-set! value-s 0 (value round) (endianness big))
- (put! server (datum->insertion (make-datum type key-s value-s)))
- (when (< round (- ROUNDS 1))
- (loop (+ round 1))))
- (wait done)
- #true)))
- ;; TODO: would be nice to verify that the necessary messages are sent to the
- ;; DHT service.
- (test-assert "cancelling a search within a search callback does not hang"
- (call-with-services/fibers
- `(("dht" . ,(simulate-dht-service)))
- (lambda (config spawn-fiber)
- (define server (connect config))
- (define datum (make-a-datum))
- (define query (make-query (datum-type datum) (datum-key datum)))
- (define search-defined (make-condition))
- (define done (make-condition))
- (define search
- (start-get! server query (lambda (a-result)
- (wait search-defined)
- (stop-get! search)
- (signal-condition! done))
- ;; The 'found' callback is responsible for cancellation.
- #:linger? #true))
- (signal-condition! search-defined)
- (put! server (datum->insertion datum))
- (wait done)
- #true)))
- (test-assert "cancelling a search multiple times does not hang"
- (call-with-services/fibers
- `(("dht" . ,(simulate-dht-service)))
- (lambda (config spawn-fiber)
- (define server (connect config))
- (define datum (make-a-datum))
- (define query (make-query (datum-type datum) (datum-key datum)))
- (define search (start-get! server query (lambda (foo) (values))
- ;; Not testing cancellation on GC here.
- #:linger? #true))
- (let loop ((n 0))
- (when (< n 40)
- (stop-get! search)
- (loop (+ n 1))))
- #true)))
- (test-assert "searches restarted after disconnect"
- (let ((stop-first-server (make-condition))
- (first-accepted (make-condition)))
- (call-with-services/fibers
- `(("dht" . ,(lambda args
- (if (signal-condition! first-accepted)
- (apply (simulate-dht-service stop-first-server) args)
- (apply (simulate-dht-service) args)))))
- (lambda (config spawn-fiber)
- (define connected/condition (make-condition))
- (define disconnected/condition (make-condition))
- (define (connected)
- (signal-condition! connected/condition))
- (define (disconnected)
- (signal-condition! disconnected/condition))
- (define server (connect config #:connected connected
- #:disconnected disconnected
- #:spawn spawn-fiber))
- ;; Start a search
- (define datum (make-a-datum))
- (define found/condition (make-condition))
- (define (found search-result)
- (unless (datum=? datum (search-result->datum search-result))
- (error "wrong search result"))
- (unless (signal-condition! found/condition)
- (error "multiple results")))
- (define query (make-query (datum-type datum) (datum-key datum)))
- (define search (start-get! server query found))
- ;; Give @var{server} a chance to actually send the request.
- ;; Removing the 'let loop' is possible, but would test some
- ;; different code paths (TODO enveloppe confirmation/cancellation).
- (wait connected/condition)
- (wait first-accepted)
- (let loop ((n 0))
- (when (< n 100)
- (yield-current-task)))
- ;; Break the connection, letting @var{server} reconnect.
- (signal-condition! stop-first-server)
- (wait disconnected/condition)
- ;; Insert the datum, such that @var{search} can complete (assuming
- ;; that @var{server} remembered to start the search again!).
- (put! server (datum->insertion datum))
- (wait found/condition)
- ;; Explicitely cancel 'search' such that it is not cancelled too
- ;; early due to GC.
- (stop-get! search)
- #true))))
- (test-end)
|