utils.scm 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. ;; This file is part of scheme-GNUnet.
  2. ;; Copyright © 2021, 2022 GNUnet e.V.
  3. ;;
  4. ;; scheme-GNUnet is free software: you can redistribute it and/or modify it
  5. ;; under the terms of the GNU Affero General Public License as published
  6. ;; by the Free Software Foundation, either version 3 of the License,
  7. ;; or (at your option) any later version.
  8. ;;
  9. ;; scheme-GNUnet is distributed in the hope that it will be useful, but
  10. ;; WITHOUT ANY WARRANTY; without even the implied warranty of
  11. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. ;; Affero General Public License for more details.
  13. ;;
  14. ;; You should have received a copy of the GNU Affero General Public License
  15. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. ;;
  17. ;; SPDX-License-Identifier: AGPL-3.0-or-later
  18. (define-module (tests utils)
  19. #:use-module (srfi srfi-8)
  20. #:use-module (ice-9 match)
  21. #:use-module (ice-9 weak-vector)
  22. #:use-module ((rnrs hashtables) #:prefix #{rnrs:}#)
  23. #:use-module ((rnrs arithmetic bitwise)
  24. #:select (bitwise-ior))
  25. #:use-module ((rnrs base) #:select (assert))
  26. #:use-module ((fibers) #:prefix #{fibers:}#)
  27. #:autoload (fibers conditions) (make-condition signal-condition! wait)
  28. #:autoload (fibers timers) (sleep)
  29. #:autoload (gnu gnunet config db)
  30. (hash->configuration hash-key key=? set-value!)
  31. #:export (conservative-gc? calls-in-tail-position?
  32. call-with-services
  33. call-with-services/fibers
  34. call-with-spawner
  35. call-with-spawner/wait
  36. call-with-spawner/wait*
  37. call-with-temporary-directory
  38. make-nonblocking!
  39. call-with-absent-service
  40. trivial-service-config
  41. #{don't-call-me}#
  42. close-not-connected-no-fallbacks
  43. garbage-collectable))
  44. (define (make-nonblocking! sock)
  45. (fcntl sock F_SETFL
  46. (bitwise-ior (fcntl sock F_GETFL) O_NONBLOCK)))
  47. ;; Current versions of guile (at least 3.0.5) use a conservative
  48. ;; garbage collector, so some tests concerning garbage collection
  49. ;; might sometimes fail without indicating a bug. For reprodicible
  50. ;; builds, allow skipping these tests.
  51. (define (conservative-gc?)
  52. (if (equal? "yes" (getenv "TOLERATE_CONSERVATIVE_COLLECTORS"))
  53. #t
  54. #f))
  55. (define (calls-in-tail-position? proc)
  56. "Does @var{proc} calls its argument in tail position?
  57. Additionally, return the values returned to the argument
  58. of @var{proc} in-order. @var{proc} should not return multiple
  59. times."
  60. (receive (continuation . arguments)
  61. (let ((t (make-prompt-tag 'tail-position?)))
  62. (call-with-prompt t
  63. (lambda ()
  64. (proc (lambda args (apply abort-to-prompt t args))))
  65. (lambda _ (apply values _))))
  66. (apply values
  67. (= 1 (stack-length (make-stack continuation)))
  68. arguments)))
  69. ;; Some basic checks
  70. (assert (calls-in-tail-position? (lambda (thunk) (thunk))))
  71. ;; TODO figure out why these fail ...
  72. #;
  73. (assert (not (calls-in-tail-position? (lambda (thunk) (thunk) 1))))
  74. #;
  75. (assert (not (calls-in-tail-position? (lambda (thunk) (+ 1 (thunk))))))
  76. #;
  77. (assert (not (calls-in-tail-position? (lambda (thunk) (for-each thunk '("bla" "bla"))))))
  78. (define (call-with-temporary-directory proc)
  79. (let ((file (mkdtemp (in-vicinity (or (getenv "TMPDIR") "/tmp")
  80. "test-XXXXXX"))))
  81. (with-exception-handler
  82. (lambda (e)
  83. (system* "rm" "-r" file)
  84. (raise-exception e))
  85. (lambda ()
  86. (call-with-values
  87. (lambda () (proc file))
  88. (lambda the-values
  89. (system* "rm" "-r" file)
  90. (apply values the-values)))))))
  91. (define (call-with-services service-alist proc)
  92. "Call the procedure @var{proc} with a configuration database
  93. and a procedure behaving like @code{spawn-fiber}, in an environment
  94. where the services listed in @var{service-alist} can
  95. be connected to. The heads in @var{service-alist} are the names of
  96. the services and each tails is a list of a procedure accepting ports
  97. (connected to the client) and the procedure behaving like @code{spawn-fiber}."
  98. (define %thread-table (make-hash-table))
  99. (define (wrapped-spawn-fiber thunk)
  100. (define o (list))
  101. (hashq-set! %thread-table o 'running)
  102. (fibers:spawn-fiber
  103. (lambda ()
  104. (with-exception-handler
  105. (lambda (e)
  106. (hashq-set! %thread-table o (cons 'exception e))
  107. (raise-exception e))
  108. thunk)))
  109. (values))
  110. (define config (hash->configuration
  111. (rnrs:make-hashtable hash-key key=?)))
  112. (call-with-temporary-directory
  113. (lambda (dir)
  114. (define (start-service key+value)
  115. (define where (in-vicinity dir (string-append (car key+value) ".sock")))
  116. (set-value! identity config (car key+value) "UNIXPATH" where)
  117. (wrapped-spawn-fiber
  118. (lambda ()
  119. (define sock (socket AF_UNIX SOCK_STREAM 0))
  120. (bind sock AF_UNIX where)
  121. (listen sock 40)
  122. (make-nonblocking! sock)
  123. (let loop ()
  124. (define client-sock
  125. (car (accept sock (logior SOCK_NONBLOCK
  126. SOCK_CLOEXEC))))
  127. (wrapped-spawn-fiber
  128. (lambda ()
  129. ((cdr key+value) client-sock wrapped-spawn-fiber)))
  130. (loop)))))
  131. (for-each start-service service-alist)
  132. (call-with-values
  133. (lambda () (proc config wrapped-spawn-fiber))
  134. (lambda results
  135. ;; Make sure exceptions are visible
  136. (hash-for-each (lambda (key value)
  137. (match value
  138. (('exception . e)
  139. (raise-exception e))
  140. ('running (values))))
  141. %thread-table)
  142. (apply values results))))))
  143. (define (call-with-services/fibers service-alist proc)
  144. (fibers:run-fibers (lambda () (call-with-services service-alist proc))))
  145. (define* (call-with-spawner* proc service-alist . args)
  146. (apply fibers:run-fibers
  147. (lambda ()
  148. (call-with-services
  149. service-alist
  150. proc))
  151. args))
  152. (define (call-with-spawner proc . args)
  153. (apply call-with-spawner* (lambda (config spawn) (proc spawn)) '() args))
  154. ;; When done, wait for every fiber to complete.
  155. ;; Somewhat racy, don't use outside tests.
  156. (define* (call-with-spawner/wait* proc service-alist . args)
  157. (define h (make-weak-key-hash-table)) ; condition -> nothing in particular
  158. (apply call-with-spawner*
  159. (lambda (config spawn/not-waiting)
  160. (define (spawn thunk)
  161. (define done-condition (make-condition))
  162. (hashq-set! h done-condition #f)
  163. (spawn/not-waiting
  164. (lambda ()
  165. (thunk)
  166. (signal-condition! done-condition))))
  167. (define-values return-values
  168. (proc config spawn))
  169. ;; Make sure every fiber completes before returning.
  170. ;; XXX hash-for-each imposes a continuation barrier
  171. (for-each wait (hash-map->list (lambda (x y) x) h))
  172. (apply values return-values))
  173. service-alist
  174. args))
  175. (define (call-with-spawner/wait proc . args)
  176. (apply call-with-spawner/wait* (lambda (config spawn) (proc spawn)) '() args))
  177. (define (trivial-service-config what where)
  178. "Make a configuration where the socket location of the @var{what} service
  179. is @var{where}."
  180. (define config (hash->configuration
  181. (rnrs:make-hashtable hash-key key=?)))
  182. (set-value! identity config what "UNIXPATH" where)
  183. config)
  184. (define (call-with-absent-service what proc)
  185. "Call @var{proc} with a configuration in which the @var{what} service
  186. cannot be connected to."
  187. (call-with-temporary-directory
  188. (lambda (somewhere)
  189. ;; Something like "/dev/this-file-does-not-exist" would do as well.
  190. (define where (in-vicinity somewhere "sock.et"))
  191. (define config (trivial-service-config what where))
  192. (proc config))))
  193. (define (#{don't-call-me}# . rest)
  194. (error "oops ~a" rest))
  195. (define* (close-not-connected-no-fallbacks service connect disconnect!
  196. #:key (rest '()))
  197. "Try to connect to the @var{service} service in an environment where
  198. the service daemon is down. Verify that the 'connected' and 'disconnected'
  199. callbacks were not called. Also verify that all spawned fibers exit."
  200. (call-with-spawner/wait
  201. (lambda (spawn)
  202. (call-with-absent-service
  203. service
  204. (lambda (config)
  205. (define server (apply connect config #:spawn spawn
  206. #:connected #{don't-call-me}#
  207. #:disconnected #{don't-call-me}#
  208. rest))
  209. ;; Sleep to give the client fibers a chance to mistakenly
  210. ;; call a callback.
  211. (sleep 0.001)
  212. (disconnect! server)
  213. (sleep 0.001)
  214. #t)))))
  215. (define* (garbage-collectable service connect)
  216. "Try to connect to the @var{service} service in an an environment where
  217. the service daemon is down. Verify that the @var{connected} and
  218. @var{disconnected} callbacks were not called. Also verify that all spawned
  219. fiber exit and the fibers do not keep a reference to the service object."
  220. (define (test)
  221. (call-with-spawner/wait
  222. (lambda (spawn)
  223. (call-with-absent-service
  224. service
  225. (lambda (config)
  226. (define reference
  227. (weak-vector
  228. (connect config #:spawn spawn #:connected #{don't-call-me}#
  229. #:disconnected #{don't-call-me}#)))
  230. ;; Sleep to give the client fibers a chance to mistakenly
  231. ;; call a callback and to allow the fibers to actually stop.
  232. (let loop ((delay 0.0005))
  233. (pk 'loop delay)
  234. (gc)
  235. (pk 'gced)
  236. (sleep delay)
  237. (if (weak-vector-ref reference 0)
  238. ;; not yet collected, try again later.
  239. (and (< delay 2.) (loop (* 2 delay)))
  240. #true))))))) ; it was collected!
  241. (define n-trials 32)
  242. (let loop ((successes 0)
  243. (trials 0))
  244. (pk successes trials)
  245. (if (>= trials n-trials)
  246. (>= (/ successes trials) (if (conservative-gc?) 8/10 1))
  247. (loop (if (test) (+ 1 successes) successes) (+ 1 trials)))))