File: cmdline_test.clj

package info (click to toggle)
nrepl-clojure 1.0.0-5
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 1,996 kB
  • sloc: makefile: 50; java: 19; sh: 15; xml: 10
file content (295 lines) | stat: -rw-r--r-- 12,719 bytes parent folder | download
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))))))))))