1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
|
(ns nrepl.cmdline-test
{:author "Chas Emerick"}
(:require
[clojure.java.io :refer [as-file]]
[clojure.string :as str]
[clojure.test :refer :all]
[nrepl.ack :as ack]
[nrepl.bencode :refer [write-bencode]]
[nrepl.cmdline :as cmd]
[nrepl.core :as nrepl]
[nrepl.core-test :refer [*server* *transport-fn* transport-fns]]
[nrepl.server :as server]
[nrepl.socket :refer [find-class unix-domain-flavor unix-socket-address]]
[nrepl.transport :as transport])
(:import
(com.hypirion.io Pipe ClosingPipe)
(java.lang ProcessBuilder$Redirect)
(java.net Socket SocketAddress)
(java.nio.channels Channels SocketChannel)
(java.nio.file Files)
(java.nio.file.attribute PosixFilePermissions)
(nrepl.server Server)))
(defn create-tmpdir
"Creates a temporary directory in parent (something clojure.java.io/as-path
can handle) with the specified permissions string (something
PosixFilePermissions/asFileAttribute can handle i.e. \"rw-------\") and
returns its Path."
[parent prefix permissions]
(let [nio-path (.toPath (as-file parent))
perms (PosixFilePermissions/fromString permissions)
attr (PosixFilePermissions/asFileAttribute perms)]
(Files/createTempDirectory nio-path prefix (into-array [attr]))))
(defn- start-server-for-transport-fn
[transport-fn f]
(with-open [^Server server (server/start-server
:transport-fn transport-fn
:handler (ack/handle-ack (server/default-handler)))]
(binding [*server* server
*transport-fn* transport-fn]
(testing (str (-> transport-fn meta :name) " transport")
(ack/reset-ack-port!)
(f))
(set! *print-length* nil)
(set! *print-level* nil))))
(defn- server-cmdline-fixture
[f]
(doseq [transport-fn transport-fns]
(start-server-for-transport-fn transport-fn f)))
(use-fixtures :each server-cmdline-fixture)
(defn- var->str
[sym]
(subs (str sym) 2))
(defn- sh
"A version of clojure.java.shell/sh that streams in/out/err.
Taken and edited from https://github.com/technomancy/leiningen/blob/f7e1adad6ff5137d6ea56bc429c3b620c6f84128/leiningen-core/src/leiningen/core/eval.clj"
^Process
[& cmd]
(let [proc (.exec (Runtime/getRuntime) ^"[Ljava.lang.String;" (into-array String cmd))]
(.addShutdownHook (Runtime/getRuntime)
(Thread. (fn [] (.destroy proc))))
(with-open [out (.getInputStream proc)
err (.getErrorStream proc)
in (.getOutputStream proc)]
(let [pump-out (doto (Pipe. out System/out) .start)
pump-err (doto (Pipe. err System/err) .start)
pump-in (ClosingPipe. System/in in)]
proc))))
(deftest repl-intro
(is (re-find #"nREPL" (cmd/repl-intro))))
(deftest help
(is (re-find #"Usage:" (cmd/help))))
(deftest parse-cli-values
(is (= {:other "string"
:middleware :middleware
:handler :handler
:transport :transport}
(cmd/parse-cli-values {:other "string"
:middleware ":middleware"
:handler ":handler"
:transport ":transport"}))))
(deftest args->cli-options
(is (= [{:middleware :middleware :repl "true"} ["extra" "args"]]
(cmd/args->cli-options ["-m" ":middleware" "-r" "true" "extra" "args"]))))
(deftest connection-opts
(is (= {:port 5000
:host "0.0.0.0"
:socket nil
:transport #'transport/bencode
:repl-fn #'nrepl.cmdline/run-repl}
(cmd/connection-opts {:port "5000"
:host "0.0.0.0"
:transport nil}))))
(deftest server-opts
(is (= {:bind "0.0.0.0"
:port 5000
:transport #'transport/bencode
:handler #'clojure.core/identity
:repl-fn #'clojure.core/identity
:greeting nil
:ack-port 2000}
(select-keys
(cmd/server-opts {:bind "0.0.0.0"
:port 5000
:ack 2000
:handler 'clojure.core/identity
:repl-fn 'clojure.core/identity})
[:bind :port :transport :greeting :handler :ack-port :repl-fn]))))
(deftest ack-server
(with-redefs [ack/send-ack (fn [_ _ _] true)]
(let [output (with-out-str
(cmd/ack-server {:port 6000}
{:ack-port 8000
:transport #'transport/bencode
:verbose true}))]
(is (= "ack'ing my port 6000 to other server running on port 8000\n"
output)))
(let [output (with-out-str
(cmd/ack-server {:port 6000}
{:ack-port 8000
:transport #'transport/bencode}))]
(is (= "" output)))))
(deftest server-started-message
(with-open [^Server server (server/start-server
:transport-fn #'transport/bencode
:handler server/default-handler)]
(is (re-find #"nREPL server started on port \d+ on host .* - .*//.*:\d+"
(cmd/server-started-message
server
{:transport #'transport/bencode})))))
(deftest ^:slow ack
(let [ack-port (:port *server*)
^Process
server-process (apply sh ["java" "-Dnreplacktest=y"
"-cp" (System/getProperty "java.class.path")
"nrepl.main"
"--ack" (str ack-port)
"--transport" (var->str *transport-fn*)])
acked-port (ack/wait-for-ack 100000)]
(try
(is acked-port "Timed out waiting for ack")
(when acked-port
(with-open [^nrepl.transport.FnTransport
transport-2 (nrepl/connect :port acked-port
:transport-fn *transport-fn*)]
(let [client (nrepl/client transport-2 1000)]
;; just a sanity check
(is (= "y"
(-> (nrepl/message client {:op "eval"
:code "(System/getProperty \"nreplacktest\")"})
first
nrepl/read-response-value
:value))))))
(finally
(.destroy server-process)))))
(deftest ^:slow explicit-port-argument
(let [ack-port (:port *server*)
free-port (with-open [ss (java.net.ServerSocket.)]
(.bind ss nil)
(.getLocalPort ss))
^Process
server-process (apply sh ["java" "-Dnreplacktest=y"
"-cp" (System/getProperty "java.class.path")
"nrepl.main"
"--port" (str free-port)
"--ack" (str ack-port)
"--transport" (var->str *transport-fn*)])
acked-port (ack/wait-for-ack 100000)]
(try
(is (= acked-port free-port))
(finally
(Thread/sleep 2000)
(.destroy server-process)))))
;;; Unix domain socket tests
(defn send-jdk-socket-message [message path]
(let [^SocketAddress addr (unix-socket-address path)
sock (SocketChannel/open addr)]
;; Assume it's safe to use Channels input/output streams here since
;; we're never reading and writing at the same time.
;; (cf. https://bugs.openjdk.java.net/browse/JDK-4509080 - not fixed
;; as of at least JDK 16).
(with-open [out (Channels/newOutputStream sock)]
(write-bencode out message))))
(defn send-junixsocket-message [message path]
(let [^Class sock-class (find-class 'org.newsclub.net.unix.AFUNIXSocket)
new-instance (.getDeclaredMethod sock-class "newInstance" nil)
addr (unix-socket-address path)]
(with-open [^Socket sock (.invoke new-instance nil nil)]
(.connect sock addr)
(write-bencode (.getOutputStream sock) message))))
(deftest ^:slow basic-fs-socket-behavior
(if-not unix-domain-flavor
(binding [*out* *err*]
;; Otherwise kaocha treats no tests as an error
(testing "Skipping UNIX domain socket tests for JDK < 16 without junixsocket dependency"
(is (not unix-domain-flavor))))
(let [tmpdir (create-tmpdir "target" "socket-test-" "rwx------")
sock-path (str tmpdir "/socket")
sock-file (as-file sock-path)]
(try
;; Use a Process rather than sh so we can see server errors
(let [cmd (into-array ["java"
"-cp" (System/getProperty "java.class.path")
"nrepl.main" "-s" sock-path])
server (.start (doto (ProcessBuilder. ^"[Ljava.lang.String;" cmd)
(.redirectOutput ProcessBuilder$Redirect/INHERIT)
(.redirectError ProcessBuilder$Redirect/INHERIT)))]
(try
;; we want to ensure the server is up before trying to connect to it
;; this is done by waiting till the socket file is created, and then
;; waiting an extra 1s. (extra wait seems to help with test reliability
;; in CI. Question: why are we not using ack to do this? To investigate
(while (not (.exists sock-file))
(Thread/sleep 100))
(Thread/sleep 1000)
(case unix-domain-flavor
:jdk
(send-jdk-socket-message {:code "(System/exit 42)" :op :eval}
sock-path)
:junixsocket
(send-junixsocket-message {:code "(System/exit 42)" :op :eval}
sock-path))
(is (= 42 (.waitFor server)))
(finally
(.destroy server))))
(finally
(.delete sock-file)
(Files/delete tmpdir))))))
(deftest ^:slow cmdline-namespace-resolution
(testing "Ensuring that namespace resolution works in the cmdline repl"
(let [test-input (str/join \newline ["::a"
"(ns a)" "::a"
"(ns b)" "::a"
"(ns user)" "::a"
"(require '[a :as z])" "::z/a"])
expected-output [":user/a"
"nil" ":a/a"
"nil" ":b/a"
"nil" ":user/a"
"nil" ":a/a"]
>devnull (fn [& _] nil)]
(binding [*in* (java.io.PushbackReader. (java.io.StringReader. test-input))]
(with-redefs [cmd/clean-up-and-exit >devnull]
(with-open [server (server/start-server)]
(let [results (atom [])]
(#'cmd/run-repl (:host server) (:port server) {:prompt >devnull :err >devnull :out >devnull :value #(swap! results conj %)})
(is (= expected-output @results)))))))))
(deftest ^:slow can-connect-to-unix-socket
(testing "We can connect to unix domain socker from the cli."
(if-not unix-domain-flavor
(binding [*out* *err*]
;; Otherwise kaocha treats no tests as an error
(testing "Skipping UNIX domain socket tests for JDK < 16 without junixsocket dependency"
(is (not unix-domain-flavor))))
(let [test-input (str/join \newline ["::a"
"(ns a)" "::a"
"(ns b)" "::a"
"(ns user)" "::a"
"(require '[a :as z])" "::z/a"])
expected-output [":user/a"
"nil" ":a/a"
"nil" ":b/a"
"nil" ":user/a"
"nil" ":a/a"]
>devnull (fn [& _] nil)
tmpdir (create-tmpdir "target" "socket-test-" "rwx------")
sock-path (str tmpdir "/socket")]
(binding [*in* (java.io.PushbackReader. (java.io.StringReader. test-input))]
(with-redefs [cmd/clean-up-and-exit >devnull]
(with-open [server (server/start-server :socket sock-path)]
(let [results (atom [])]
(#'cmd/run-repl {:server {:socket sock-path}
:options {:prompt >devnull :err >devnull :out >devnull :value #(swap! results conj %)}})
(is (= expected-output @results))))))))))
|