(in-package :cl-user) (defpackage clack.test (:use :cl) (:import-from :clack :clackup :stop) (:import-from :dexador :*use-connection-pool*) (:import-from :rove :testing) (:import-from :usocket :socket-listen :socket-close :address-in-use-error :socket-error) (:export :*clack-test-handler* :*clack-test-port* :*clack-test-access-port* :*clackup-additional-args* :*enable-debug* :*use-https* :*random-port* :localhost :testing-app)) (in-package :clack.test) (defvar *clack-test-handler* :hunchentoot "Backend Handler to run tests on. String or Symbol are allowed.") (defvar *clack-test-port* 4242 "HTTP port number of Handler.") (defvar *clackup-additional-args* '() "Additional arguments for clackup.") (defvar *clack-test-access-port* *clack-test-port* "Port of localhost to request. Use if you want to set another port. The default is `*clack-test-port*`.") (defvar *enable-debug* t) (defvar *use-https* nil) (defvar *random-port* t) (defun port-available-p (port) (let (socket) (unwind-protect (handler-case (progn (setq socket (usocket:socket-listen "127.0.0.1" port :reuse-address t)) t) (usocket:address-in-use-error () nil) #+(and sbcl win32) (sb-bsd-sockets:socket-error () nil) (usocket:socket-error (e) (warn "USOCKET:SOCKET-ERROR: ~A" e) nil)) (when socket (usocket:socket-close socket) t)))) (defun server-running-p (port) (handler-case (let ((socket (usocket:socket-connect "127.0.0.1" port))) (usocket:socket-close socket) t) #+sbcl (sb-bsd-sockets:interrupted-error () nil) (usocket:socket-error () nil) (usocket:connection-refused-error () nil) (usocket:connection-reset-error () nil))) (defun random-port () "Return a port number not in use from 50000 to 60000." (loop for port from (+ 50000 (random 1000)) upto 60000 if (port-available-p port) return port)) (defun localhost (&optional (path "/") (port *clack-test-access-port*)) (check-type path string) (setf path (cond ((= 0 (length path)) "/") ((not (char= (aref path 0) #\/)) (concatenate 'string "/" path)) (t path))) (format nil "http~@[~*s~]://127.0.0.1:~D~A" *use-https* port path)) (defun %testing-app (app client) (let* ((*clack-test-port* (if *random-port* (random-port) *clack-test-port*)) (*clack-test-access-port* (if *random-port* *clack-test-port* *clack-test-access-port*)) (threads #+thread-support (bt2:all-threads) #-thread-support '())) (loop repeat 5 until (port-available-p *clack-test-port*) do (sleep 0.1) finally (unless (port-available-p *clack-test-port*) (error "Port ~D is already in use." *clack-test-port*))) (let ((acceptor (apply #'clackup app :server *clack-test-handler* :port *clack-test-port* :debug *enable-debug* :use-thread t :silent t *clackup-additional-args*)) (dex:*use-connection-pool* nil)) (loop until (server-running-p *clack-test-port*) do (sleep 0.1)) (multiple-value-prog1 (unwind-protect (funcall client) (stop acceptor) ;; Ensure all threads are finished for preventing from leaking #+thread-support (dolist (thread (bt2:all-threads)) (when (and (not (find thread threads)) (bt2:thread-alive-p thread)) (bt2:destroy-thread thread)))) (loop while (server-running-p *clack-test-port*) do (sleep 0.1)))))) (defmacro testing-app (desc app &body body) `(%testing-app ,app (lambda () (testing ,desc ,@body))))