Compare commits
34 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47e55fe5e7 | ||
|
|
717cef7af5 | ||
|
|
cd968459a7 | ||
|
|
8b717eb001 | ||
|
|
6ad6045b94 | ||
|
|
b00133ca05 | ||
|
|
64ecb94de8 | ||
|
|
d29cf6aa65 | ||
|
|
1635931483 | ||
|
|
75c2216649 | ||
|
|
c2e3d8f8b8 | ||
|
|
16bea5b7db | ||
|
|
85c554e643 | ||
|
|
4fb0da7daf | ||
|
|
76313a7089 | ||
|
|
28cf3de4ef | ||
|
|
81f8845d72 | ||
|
|
decf791000 | ||
|
|
53f79da09d | ||
|
|
8bc0852799 | ||
|
|
6cbbdd118d | ||
|
|
5754d433aa | ||
|
|
6df443fabf | ||
|
|
93081b75e6 | ||
|
|
66867eee7f | ||
|
|
eb0b01c0a6 | ||
|
|
b9f3a28555 | ||
|
|
aee5337cef | ||
|
|
c2d6ba847c | ||
|
|
e075b13bfe | ||
|
|
842ff34739 | ||
|
|
f2cfdff899 | ||
|
|
5fbf1d7b04 | ||
|
|
8f059da7e6 |
20 changed files with 592 additions and 223 deletions
28
.circleci/config.yml
Normal file
28
.circleci/config.yml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
version: 2.1
|
||||
jobs:
|
||||
test:
|
||||
docker:
|
||||
- image: clojure:openjdk-11-tools-deps-1.10.3.1087-slim-bullseye
|
||||
working_directory: ~/repo
|
||||
environment:
|
||||
LEIN_ROOT: "true"
|
||||
BABASHKA_PLATFORM: linux
|
||||
resource_class: large
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-dependencies-{{ checksum "deps.edn" }}
|
||||
# fallback to using latest cache if no exact match is found
|
||||
- v1-dependencies-
|
||||
- run: |
|
||||
script/test
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.m2
|
||||
key: v1-dependencies-{{ checksum "deps.edn" }}
|
||||
workflows:
|
||||
version: 2
|
||||
ci:
|
||||
jobs:
|
||||
- test
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -3,3 +3,7 @@
|
|||
.lein-failures
|
||||
/pom.xml
|
||||
.lein-repl-history
|
||||
.cache
|
||||
.clj-kondo/babashka
|
||||
.clj-kondo/rewrite-clj
|
||||
src/scratch.clj
|
||||
|
|
|
|||
21
CHANGELOG.md
Normal file
21
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
- [#63](https://github.com/babashka/pods/issues/63): create directory before un-tarring
|
||||
- [#59](https://github.com/babashka/pods/issues/59): delete port file on exit
|
||||
- [#65](https://github.com/babashka/pods/issues/65): fix warnings when defining var with core name in JVM
|
||||
- [#66](https://github.com/babashka/pods/issues/66): Allow metadata on fn arguments for transit+json
|
||||
|
||||
## v0.2.0
|
||||
|
||||
- [#61](https://github.com/babashka/pods/issues/61): add transit as explicit JVM dependency
|
||||
- [#60](https://github.com/babashka/pods/issues/60): transform pod reader error into exception of caller
|
||||
- Switch "out" and "err" messages to print and flush instead of `println` ([@justone](https://github.com/justone))
|
||||
- Set TCP_NODELAY on transport socket ([@retrogradeorbit](https://github.com/retrogradeorbit))
|
||||
- Allow env vars OS_NAME & OS_ARCH to override os props ([@cap10morgan](https://github.com/cap10morgan))
|
||||
- [#49](https://github.com/babashka/pods/issues/49): don't log socket closed exception
|
||||
|
||||
## v0.1.0
|
||||
|
||||
Initial version
|
||||
82
README.md
82
README.md
|
|
@ -74,7 +74,44 @@ On the JVM:
|
|||
|
||||
### Where does the pod come from?
|
||||
|
||||
When calling `load-pod` with a string or vector of strings, the pod is looked up on the local file system (either using the PATH, or using an absolute path). When it is called with a qualified symbol and a version - like `(load-pod 'org.babashka/aws "0.0.5")` then it will be looked up in and downloaded from the [pod-registry](https://github.com/babashka/pod-registry).
|
||||
When calling `load-pod` with a string or vector of strings (or declaring it in your `bb.edn`),
|
||||
the pod is looked up on the local file system (either using the PATH, or using an absolute path).
|
||||
When it is called with a qualified symbol and a version - like `(load-pod 'org.babashka/aws "0.0.5")`
|
||||
then it will be looked up in and downloaded from the [pod-registry](https://github.com/babashka/pod-registry). You can customize the file system location that `load-pod` will use by setting the `BABASHKA_PODS_DIR` environment variable.
|
||||
|
||||
By default babashka will search for a pod binary matching your system's OS and arch. If you want to download
|
||||
pods for a different OS / arch (e.g. for deployment to servers), you can set one or both of the following
|
||||
environment variables:
|
||||
|
||||
- `BABASHKA_PODS_OS_NAME=Linux` (or `Mac OS X` or any other value returned by Java's `os.name` property)
|
||||
- `BABASHKA_PODS_OS_ARCH=aarch64` (or `amd64` or any other value returned by Java's `os.arch` property)
|
||||
|
||||
### In a babashka project
|
||||
|
||||
As of babashka 0.8.0 you can declare the pods your babashka project uses in your `bb.edn` file like so:
|
||||
|
||||
```clojure
|
||||
:pods {org.babashka/hsqldb {:version "0.1.0"} ; will be downloaded from the babashka pod registry
|
||||
my.local/pod {:path "../pod-my-local/my-pod-binary"
|
||||
:cache false}} ; optionally disable namespace caching if you're actively working on this pod
|
||||
```
|
||||
|
||||
Then you can just require the pods in your code like any other clojure lib:
|
||||
|
||||
```clojure
|
||||
(ns my.project
|
||||
(:require [pod.babashka.hsqldb :as sql]
|
||||
[my.local.pod :as my-pod]))
|
||||
|
||||
(def db "jdbc:hsqldb:mem:testdb;sql.syntax_mys=true")
|
||||
(sql/execute! db ["create table foo ( foo int );"])
|
||||
;;=> [#:next.jdbc{:update-count 0}]
|
||||
|
||||
(my-pod/do-a-thing "foo")
|
||||
;;=> "something"
|
||||
```
|
||||
|
||||
The pods will then be loaded on demand when you require them. No need to call `load-pod` explicitly.
|
||||
|
||||
## Sci
|
||||
|
||||
|
|
@ -93,7 +130,7 @@ light weight replacement for native interop (JNI, JNA, etc.).
|
|||
|
||||
### Examples
|
||||
|
||||
Beyond the already available pods mentioned above, eductional examples of pods
|
||||
Beyond the already available pods mentioned above, educational examples of pods
|
||||
can be found [here](examples):
|
||||
|
||||
- [pod-lispyclouds-sqlite](examples/pod-lispyclouds-sqlite): a pod that
|
||||
|
|
@ -191,7 +228,7 @@ JSON. It also declares that the pod exposes one namespace,
|
|||
|
||||
To encode payloads in EDN use `"edn"` and for Transit JSON use `"transit+json"`.
|
||||
|
||||
The pod encodes the above map to bencode and writes it to stdoud. The pod client
|
||||
The pod encodes the above map to bencode and writes it to stdout. The pod client
|
||||
reads this message from the pod's stdout.
|
||||
|
||||
Upon receiving this message, the pod client creates these namespaces and vars.
|
||||
|
|
@ -339,11 +376,15 @@ nil
|
|||
|
||||
#### Metadata
|
||||
|
||||
**From pod to pod client**
|
||||
|
||||
*Fixed Metadata on vars*
|
||||
|
||||
Pods may attach metadata to functions and macros by sending data to the pod client
|
||||
in a `"meta"` field as part of a `"var"` section. The metadata must be an appropriate
|
||||
map, encoded as an EDN string. This is only applicable to vars in the pod and will be
|
||||
ignored if the var refers to Client-side code, since metadata can already be defined
|
||||
in those code blocks.
|
||||
in those code blocks (see 'Dynamic Metadata' below to enable the encoding of metadata).
|
||||
|
||||
For example, a pod can define a function called `add`:
|
||||
|
||||
|
|
@ -355,6 +396,33 @@ For example, a pod can define a function called `add`:
|
|||
"meta" "{:doc \"arithmetic addition of 2 arguments\" :arglists ([a b])}"}]}]}
|
||||
```
|
||||
|
||||
*Dynamic Metadata*
|
||||
|
||||
Pods may send metadata on values returned to the client if metadata encoding is enabled
|
||||
for the particular transport format used by the pod.
|
||||
|
||||
For example, if your pod uses `:transit+json` as its format, you can enable metadata
|
||||
encoding by adding `:transform transit/write-meta` (or whatever transit is aliased to)
|
||||
to the optional map passed to `transit/writer`. e.g.:
|
||||
|
||||
````clojure
|
||||
(transit/writer baos :json {:transform transit/write-meta})
|
||||
````
|
||||
|
||||
##### From pod client to pod
|
||||
|
||||
Currently sending metadata on arguments passed to a pod function is available only for the
|
||||
`transit+json` format and can be enabled on a per var basis.
|
||||
|
||||
A pod can enable metadata to be read on arguments by sending the "arg-meta" field to "true"
|
||||
for the var representing that function. For example:
|
||||
|
||||
````clojure
|
||||
{:format :transit+json
|
||||
:namespaces [{:name "pod.babashka.demo"
|
||||
:vars [{"name" "round-trip" "arg-meta" "true"}]}]}
|
||||
````
|
||||
|
||||
#### Deferred namespace loading
|
||||
|
||||
When your pod exposes multiple namespaces that can be used independently from
|
||||
|
|
@ -429,7 +497,7 @@ The arguments to `babashka.pods/invoke` are:
|
|||
- a pod identifier string derived from the first described namespace.
|
||||
- the symbol of the var to invoke
|
||||
- the arguments to the var
|
||||
- an opts map containing `:handler` containing callback functions: `:success`, `:error` and `:done`
|
||||
- an opts map containing `:handlers` containing callback functions: `:success`, `:error` and `:done`
|
||||
|
||||
The return value of `babashka.pods/invoke` is a map containing `:result`. When
|
||||
not using callbacks, this is the return value from the pod var invocation. When
|
||||
|
|
@ -455,9 +523,9 @@ callback is only called if no errors were sent by the pod.
|
|||
|
||||
In the above example the wrapper function calls the pod identified by
|
||||
`"pod.babashka.filewatcher"`. It calls the var
|
||||
`pod.babashka.filewatcher/watch*`. In `:on-success` it pulls out received
|
||||
`pod.babashka.filewatcher/watch*`. In `:success` it pulls out received
|
||||
values, passing them to the user-provided callback. Additionally, it prints any
|
||||
errors received from the pod library in `:on-error` to `*err*`.
|
||||
errors received from the pod library in `:error` to `*err*`.
|
||||
|
||||
A user will then use `pod.babashka.filewatcher/watch` like this:
|
||||
|
||||
|
|
|
|||
2
bb.edn
Normal file
2
bb.edn
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
{:tasks {test {:doc "Run tests"
|
||||
:task (shell "script/test")}}}
|
||||
3
deps.edn
3
deps.edn
|
|
@ -1,6 +1,7 @@
|
|||
{:deps {nrepl/bencode {:mvn/version "1.1.0"}
|
||||
cheshire/cheshire {:mvn/version "5.10.0"}
|
||||
com.cognitect/transit-clj {:mvn/version "1.0.324"}}
|
||||
com.cognitect/transit-clj {:mvn/version "1.0.324"}
|
||||
babashka/fs {:mvn/version "0.1.6"}}
|
||||
:aliases
|
||||
{:sci
|
||||
{:extra-deps
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
(defproject babashka/babashka.pods "0.1.0"
|
||||
(defproject babashka/babashka.pods "0.2.0"
|
||||
:description "babashka pods"
|
||||
:url "https://github.com/babashka/babashka.pods"
|
||||
:scm {:name "git"
|
||||
|
|
@ -7,7 +7,9 @@
|
|||
:url "https://www.eclipse.org/legal/epl-1.0/"}
|
||||
:dependencies [[org.clojure/clojure "1.10.3"]
|
||||
[nrepl/bencode "1.1.0"]
|
||||
[cheshire "5.10.0"]]
|
||||
[cheshire "5.10.0"]
|
||||
[babashka/fs "0.1.6"]
|
||||
[com.cognitect/transit-clj "1.0.329"]]
|
||||
:deploy-repositories [["clojars" {:url "https://clojars.org/repo"
|
||||
:username :env/clojars_user
|
||||
:password :env/clojars_pass
|
||||
|
|
|
|||
17
script/changelog.clj
Executable file
17
script/changelog.clj
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
#!/usr/bin/env bb
|
||||
|
||||
(ns changelog
|
||||
(:require [clojure.string :as str]))
|
||||
|
||||
(let [changelog (slurp "CHANGELOG.md")
|
||||
replaced (str/replace changelog
|
||||
#" #(\d+)"
|
||||
(fn [[_ issue after]]
|
||||
(format " [#%s](https://github.com/babashka/pods/issues/%s)%s"
|
||||
issue issue (str after))))
|
||||
replaced (str/replace replaced
|
||||
#"@([a-zA-Z0-9-_]+)([, \.)])"
|
||||
(fn [[_ name after]]
|
||||
(format "[@%s](https://github.com/%s)%s"
|
||||
name name after)))]
|
||||
(spit "CHANGELOG.md" replaced))
|
||||
17
script/test
17
script/test
|
|
@ -8,24 +8,25 @@ export BABASHKA_POD_TEST_SOCKET
|
|||
# format = edn
|
||||
BABASHKA_POD_TEST_FORMAT=edn
|
||||
echo "Testing edn"
|
||||
clojure -A:test -n babashka.pods.jvm-test
|
||||
clojure -A:sci:test -n babashka.pods.sci-test
|
||||
clojure -M:test -n babashka.pods.jvm-test
|
||||
clojure -M:sci:test -n babashka.pods.sci-test
|
||||
clojure -M:test -n babashka.pods.impl-test
|
||||
|
||||
# format = json
|
||||
BABASHKA_POD_TEST_FORMAT=json
|
||||
echo "Testing json"
|
||||
clojure -A:test -n babashka.pods.jvm-test
|
||||
clojure -A:sci:test -n babashka.pods.sci-test
|
||||
clojure -M:test -n babashka.pods.jvm-test
|
||||
clojure -M:sci:test -n babashka.pods.sci-test
|
||||
|
||||
# format = json
|
||||
BABASHKA_POD_TEST_FORMAT="transit+json"
|
||||
echo "Testing transit"
|
||||
clojure -A:test -n babashka.pods.jvm-test
|
||||
clojure -A:sci:test -n babashka.pods.sci-test
|
||||
clojure -M:test -n babashka.pods.jvm-test
|
||||
clojure -M:sci:test -n babashka.pods.sci-test
|
||||
|
||||
# socket = true
|
||||
unset BABASHKA_POD_TEST_FORMAT
|
||||
BABASHKA_POD_TEST_SOCKET=true
|
||||
echo "Testing socket"
|
||||
clojure -A:test -n babashka.pods.jvm-test
|
||||
clojure -A:sci:test -n babashka.pods.sci-test
|
||||
clojure -M:test -n babashka.pods.jvm-test
|
||||
clojure -M:sci:test -n babashka.pods.sci-test
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@
|
|||
(defn bytes->string [^"[B" bytes]
|
||||
(String. bytes))
|
||||
|
||||
(defn bytes->boolean [^"[B" bytes]
|
||||
(= "true" (String. bytes)))
|
||||
|
||||
(defn get-string [m k]
|
||||
(-> (get m k)
|
||||
bytes->string))
|
||||
|
|
@ -36,6 +39,10 @@
|
|||
(some-> (get m k)
|
||||
bytes->string))
|
||||
|
||||
(defn get-maybe-boolean [m k]
|
||||
(some-> (get m k)
|
||||
bytes->boolean))
|
||||
|
||||
(defn next-id []
|
||||
(str (java.util.UUID/randomUUID)))
|
||||
|
||||
|
|
@ -83,10 +90,12 @@
|
|||
(let [wh (transit/write-handler tag-fn val-fn)]
|
||||
(swap! transit-default-write-handlers assoc *pod-id* wh)))
|
||||
|
||||
(defn transit-json-write [pod-id ^String s]
|
||||
(defn transit-json-write
|
||||
[pod-id ^String s metadata?]
|
||||
(with-open [baos (java.io.ByteArrayOutputStream. 4096)]
|
||||
(let [w (transit/writer baos :json {:handlers (get @transit-write-handler-maps pod-id)
|
||||
:default-handler (get @transit-default-write-handlers pod-id)})]
|
||||
(let [w (transit/writer baos :json (cond-> {:handlers (get @transit-write-handler-maps pod-id)
|
||||
:default-handler (get @transit-default-write-handlers pod-id)}
|
||||
metadata? (assoc :transform transit/write-meta)))]
|
||||
(transit/write w s)
|
||||
(str baos))))
|
||||
|
||||
|
|
@ -98,7 +107,7 @@
|
|||
write-fn (case format
|
||||
:edn pr-str
|
||||
:json cheshire/generate-string
|
||||
:transit+json #(transit-json-write (:pod-id pod) %))
|
||||
:transit+json #(transit-json-write (:pod-id pod) % (:arg-meta opts)))
|
||||
id (next-id)
|
||||
chan (if handlers handlers
|
||||
(promise))
|
||||
|
|
@ -128,11 +137,12 @@
|
|||
edn/read-string)
|
||||
name-sym (if vmeta
|
||||
(with-meta name-sym vmeta)
|
||||
name-sym)]
|
||||
name-sym)
|
||||
metadata? (get-maybe-boolean var "arg-meta")]
|
||||
[name-sym
|
||||
(or code
|
||||
(fn [& args]
|
||||
(let [res (invoke pod sym args {:async async?})]
|
||||
(let [res (invoke pod sym args {:async async? :arg-meta metadata?})]
|
||||
res)))]))
|
||||
vars))
|
||||
|
||||
|
|
@ -168,19 +178,26 @@
|
|||
(loop []
|
||||
(let [reply (try (read stdout)
|
||||
(catch java.io.EOFException _
|
||||
::EOF))]
|
||||
::EOF)
|
||||
(catch java.net.SocketException e
|
||||
(if (= "Socket closed" (ex-message e))
|
||||
::EOF
|
||||
(throw e))))]
|
||||
(when-not (identical? ::EOF reply)
|
||||
(let [id (get reply "id")
|
||||
id (bytes->string id)
|
||||
value* (find reply "value")
|
||||
value (some-> value*
|
||||
[exception value] (try (some->> value*
|
||||
second
|
||||
bytes->string
|
||||
read-fn)
|
||||
read-fn
|
||||
(vector nil))
|
||||
(catch Exception e
|
||||
[e nil]))
|
||||
status (get reply "status")
|
||||
status (set (map (comp keyword bytes->string) status))
|
||||
error? (contains? status :error)
|
||||
done? (or error? (contains? status :done))
|
||||
error? (or exception (contains? status :error))
|
||||
done? (or error? exception (contains? status :done))
|
||||
[ex-message ex-data]
|
||||
(when error?
|
||||
[(or (some-> (get reply "ex-message")
|
||||
|
|
@ -198,8 +215,9 @@
|
|||
:vars (bencode->vars pod name-str v)}))
|
||||
chan (get @chans id)
|
||||
promise? (instance? clojure.lang.IPending chan)
|
||||
exception (when (and promise? error?)
|
||||
(ex-info ex-message ex-data))
|
||||
exception (or exception
|
||||
(when (and promise? error?)
|
||||
(ex-info ex-message ex-data)))
|
||||
;; NOTE: if we need more fine-grained handlers, we will add
|
||||
;; a :raw handler that will just get the bencode message's raw
|
||||
;; data
|
||||
|
|
@ -215,9 +233,12 @@
|
|||
;; listening to output synchronous.
|
||||
(when out
|
||||
(binding [*out* out-stream]
|
||||
(println out)))
|
||||
(when err (binding [*out* err-stream]
|
||||
(println err)))
|
||||
(print out)
|
||||
(.flush ^java.io.Writer out-stream)))
|
||||
(when err
|
||||
(binding [*out* err-stream]
|
||||
(print err)
|
||||
(.flush ^java.io.Writer err-stream)))
|
||||
(when (or value* error? namespace)
|
||||
(cond promise?
|
||||
(deliver chan (cond error? exception
|
||||
|
|
@ -248,27 +269,24 @@
|
|||
(defn lookup-pod [pod-id]
|
||||
(get @pods pod-id))
|
||||
|
||||
(defn destroy* [{:keys [:stdin :process :ops]}]
|
||||
(if (contains? ops :shutdown)
|
||||
(do (write stdin
|
||||
{"op" "shutdown"
|
||||
"id" (next-id)})
|
||||
(.waitFor ^Process process))
|
||||
(.destroy ^Process process)))
|
||||
|
||||
(defn destroy [pod-id-or-pod]
|
||||
(let [pod-id (get-pod-id pod-id-or-pod)]
|
||||
(when-let [pod (lookup-pod pod-id)]
|
||||
(if (contains? (:ops pod) :shutdown)
|
||||
(do (write (:stdin pod)
|
||||
{"op" "shutdown"
|
||||
"id" (next-id)})
|
||||
(.waitFor ^Process (:process pod)))
|
||||
(.destroy ^Process (:process pod)))
|
||||
(destroy* pod)
|
||||
(when-let [rns (:remove-ns pod)]
|
||||
(doseq [[ns-name _] (:namespaces pod)]
|
||||
(rns ns-name))))
|
||||
(swap! pods dissoc pod-id)
|
||||
nil))
|
||||
|
||||
(def next-pod-id
|
||||
(let [counter (atom 0)]
|
||||
(fn []
|
||||
(let [[o _] (swap-vals! counter inc)]
|
||||
o))))
|
||||
|
||||
(def bytes->symbol
|
||||
(comp symbol bytes->string))
|
||||
|
||||
|
|
@ -291,7 +309,8 @@
|
|||
the socket is connected."
|
||||
^Socket
|
||||
[^String hostname ^Integer port]
|
||||
(Socket. hostname port))
|
||||
(doto (Socket. hostname port)
|
||||
(.setTcpNoDelay true)))
|
||||
|
||||
(defn close-socket
|
||||
"Close the socket, and also closes its input and output streams."
|
||||
|
|
@ -301,7 +320,8 @@
|
|||
(catch java.net.SocketException _ nil)))
|
||||
|
||||
(defn port-file [pid]
|
||||
(io/file (str ".babashka-pod-" pid ".port")))
|
||||
(doto (io/file (str ".babashka-pod-" pid ".port"))
|
||||
(.deleteOnExit)))
|
||||
|
||||
(defn read-port [^java.io.File port-file]
|
||||
(loop []
|
||||
|
|
@ -310,29 +330,35 @@
|
|||
(let [s (slurp f)]
|
||||
(when (str/ends-with? s "\n")
|
||||
(str/trim s))))]
|
||||
(Integer. s)
|
||||
(Integer/parseInt s)
|
||||
(recur)))))
|
||||
|
||||
(defn debug [& strs]
|
||||
(binding [*out* *err*]
|
||||
(println (str/join " " (map pr-str strs)))))
|
||||
|
||||
(defn load-pod
|
||||
([pod-spec] (load-pod pod-spec nil))
|
||||
([pod-spec opts]
|
||||
(let [{:keys [:version :force]} opts
|
||||
resolved (when (qualified-symbol? pod-spec)
|
||||
(defn resolve-pod [pod-spec {:keys [:version :path :force] :as opts}]
|
||||
(when (qualified-symbol? pod-spec)
|
||||
(when (and (not version) (not path))
|
||||
(throw (IllegalArgumentException. "Version or path must be provided")))
|
||||
(when (and version path)
|
||||
(throw (IllegalArgumentException. "You must provide either version or path, not both"))))
|
||||
(let [resolved (when (and (qualified-symbol? pod-spec) version)
|
||||
(resolver/resolve pod-spec version force))
|
||||
opts (if resolved
|
||||
(if-let [extra-opts (:options resolved)]
|
||||
(merge opts extra-opts)
|
||||
opts)
|
||||
opts)
|
||||
{:keys [:remove-ns :resolve :transport]} opts
|
||||
pod-spec (cond resolved [(:executable resolved)]
|
||||
pod-spec (cond
|
||||
resolved [(:executable resolved)]
|
||||
path [path]
|
||||
(string? pod-spec) [pod-spec]
|
||||
:else pod-spec)
|
||||
pb (ProcessBuilder. ^java.util.List pod-spec)
|
||||
:else pod-spec)]
|
||||
{:pod-spec pod-spec, :opts opts}))
|
||||
|
||||
(defn run-pod [pod-spec {:keys [:transport] :as _opts}]
|
||||
(let [pb (ProcessBuilder. ^java.util.List pod-spec)
|
||||
socket? (identical? :socket transport)
|
||||
_ (if socket?
|
||||
(.inheritIO pb)
|
||||
|
|
@ -355,14 +381,53 @@
|
|||
[socket
|
||||
(.getOutputStream socket)
|
||||
(PushbackInputStream. (.getInputStream socket))])
|
||||
[nil (.getOutputStream p) (java.io.PushbackInputStream. (.getInputStream p))])
|
||||
_ (write stdin {"op" "describe"
|
||||
[nil (.getOutputStream p) (java.io.PushbackInputStream. (.getInputStream p))])]
|
||||
{:process p
|
||||
:socket socket
|
||||
:stdin stdin
|
||||
:stdout stdout}))
|
||||
|
||||
(defn describe-pod [{:keys [:stdin :stdout]}]
|
||||
(write stdin {"op" "describe"
|
||||
"id" (next-id)})
|
||||
reply (read stdout)
|
||||
format (-> (get reply "format") bytes->string keyword)
|
||||
ops (some->> (get reply "ops") keys (map keyword) set)
|
||||
(read stdout))
|
||||
|
||||
(defn describe->ops [describe-reply]
|
||||
(some->> (get describe-reply "ops") keys (map keyword) set))
|
||||
|
||||
(defn describe->metadata [describe-reply resolve-fn]
|
||||
(let [format (-> (get describe-reply "format") bytes->string keyword)
|
||||
ops (describe->ops describe-reply)
|
||||
readers (when (identical? :edn format)
|
||||
(read-readers reply resolve))
|
||||
(read-readers describe-reply resolve-fn))]
|
||||
{:format format, :ops ops, :readers readers}))
|
||||
|
||||
(defn run-pod-for-metadata [pod-spec opts]
|
||||
(let [running-pod (run-pod pod-spec opts)
|
||||
describe-reply (describe-pod running-pod)
|
||||
ops (describe->ops describe-reply)]
|
||||
(destroy* (assoc running-pod :ops ops))
|
||||
describe-reply))
|
||||
|
||||
(defn load-pod-metadata [unresolved-pod-spec {:keys [:download-only] :as opts}]
|
||||
(let [{:keys [:pod-spec :opts]} (resolve-pod unresolved-pod-spec opts)]
|
||||
(if download-only
|
||||
(resolver/warn "Not running pod" unresolved-pod-spec "to pre-cache metadata because OS and/or arch are different than system")
|
||||
(run-pod-for-metadata pod-spec opts))))
|
||||
|
||||
(defn load-pod
|
||||
([pod-spec] (load-pod pod-spec nil))
|
||||
([pod-spec opts]
|
||||
(let [{:keys [:pod-spec :opts]} (resolve-pod pod-spec opts)
|
||||
{:keys [:remove-ns :resolve]} opts
|
||||
|
||||
{p :process, stdin :stdin, stdout :stdout, socket :socket
|
||||
:as running-pod}
|
||||
(run-pod pod-spec opts)
|
||||
|
||||
reply (or (:metadata opts)
|
||||
(describe-pod running-pod))
|
||||
{:keys [:format :ops :readers]} (describe->metadata reply resolve)
|
||||
pod {:process p
|
||||
:pod-spec pod-spec
|
||||
:stdin stdin
|
||||
|
|
|
|||
|
|
@ -15,29 +15,43 @@
|
|||
"x86_64"
|
||||
arch))
|
||||
|
||||
(def os {:os/name (System/getProperty "os.name")
|
||||
:os/arch (let [arch (System/getProperty "os.arch")]
|
||||
(normalize-arch arch))})
|
||||
(defn normalize-os [os]
|
||||
(-> os str/lower-case (str/replace #"\s+" "_")))
|
||||
|
||||
(def os
|
||||
(delay
|
||||
{:os/name (or (System/getenv "BABASHKA_PODS_OS_NAME")
|
||||
(System/getProperty "os.name"))
|
||||
:os/arch (let [arch (or (System/getenv "BABASHKA_PODS_OS_ARCH")
|
||||
(System/getProperty "os.arch"))]
|
||||
(normalize-arch arch))}))
|
||||
|
||||
(defn warn [& strs]
|
||||
(binding [*out* *err*]
|
||||
(apply println strs)))
|
||||
|
||||
(defn match-artifacts [package]
|
||||
(defn match-artifacts
|
||||
([package] (match-artifacts package (:os/arch @os)))
|
||||
([package arch]
|
||||
(let [artifacts (:pod/artifacts package)
|
||||
res (filter (fn [{os-name :os/name
|
||||
os-arch :os/arch}]
|
||||
(let [os-arch (normalize-arch os-arch)]
|
||||
(and (re-matches (re-pattern os-name) (:os/name os))
|
||||
(and (re-matches (re-pattern os-name) (:os/name @os))
|
||||
(re-matches (re-pattern os-arch)
|
||||
(:os/arch os)))))
|
||||
arch))))
|
||||
artifacts)]
|
||||
(when (empty? res)
|
||||
(if (empty? res)
|
||||
(if (and (= "Mac OS X" (:os/name @os))
|
||||
(= "aarch64" (:os/arch @os)))
|
||||
;; Rosetta2 fallback on Apple M1 machines
|
||||
(match-artifacts package "x86_64")
|
||||
(throw (IllegalArgumentException. (format "No executable found for pod %s (%s) and OS %s/%s"
|
||||
(:pod/name package)
|
||||
(:pod/version package)
|
||||
(:os/name os)
|
||||
(:os/arch os)))))
|
||||
res))
|
||||
(:os/name @os)
|
||||
(:os/arch @os)))))
|
||||
res))))
|
||||
|
||||
(defn unzip [{:keys [^java.io.File zip-file
|
||||
^java.io.File destination-dir
|
||||
|
|
@ -77,7 +91,10 @@
|
|||
^"[Ljava.nio.file.CopyOption;"
|
||||
(into-array
|
||||
[java.nio.file.StandardCopyOption/REPLACE_EXISTING])))
|
||||
(sh "tar" "xf" (.getPath tmp-file) "--directory" (.getPath destination-dir))
|
||||
(.mkdirs destination-dir)
|
||||
(let [res (sh "tar" "xf" (.getPath tmp-file) "--directory" (.getPath destination-dir))]
|
||||
(when-not (zero? (:exit res))
|
||||
(throw (ex-info (:err res) res))))
|
||||
(.delete tmp-file)))
|
||||
|
||||
(defn make-executable [dest-dir executables verbose?]
|
||||
|
|
@ -97,11 +114,20 @@
|
|||
(with-open [is (.getInputStream conn)]
|
||||
(io/copy is dest))))
|
||||
|
||||
(def pod-manifests-dir
|
||||
;; wrapped in delay for GraalVM native-image
|
||||
(delay (io/file (or (System/getenv "XDG_DATA_HOME")
|
||||
(defn repo-dir []
|
||||
(io/file (if-let [pods-dir (System/getenv "BABASHKA_PODS_DIR")]
|
||||
(io/file pods-dir)
|
||||
(io/file (or
|
||||
(System/getenv "XDG_DATA_HOME")
|
||||
(System/getProperty "user.home"))
|
||||
".babashka" "pods" "repository")))
|
||||
".babashka"
|
||||
"pods"))
|
||||
"repository"))
|
||||
|
||||
(def pods-repo-dir
|
||||
;; wrapped in delay for GraalVM native-image
|
||||
(delay
|
||||
(repo-dir)))
|
||||
|
||||
(defn github-url [qsym version]
|
||||
(format
|
||||
|
|
@ -110,7 +136,7 @@
|
|||
|
||||
(defn pod-manifest
|
||||
[qsym version force?]
|
||||
(let [f (io/file @pod-manifests-dir (str qsym) (str version) "manifest.edn")]
|
||||
(let [f (io/file @pods-repo-dir (str qsym) (str version) "manifest.edn")]
|
||||
(if (and (not force?)
|
||||
(.exists f))
|
||||
(edn/read-string (slurp f))
|
||||
|
|
@ -121,27 +147,30 @@
|
|||
^java.io.File
|
||||
[{pod-name :pod/name
|
||||
pod-version :pod/version}]
|
||||
(let [base-file
|
||||
(if-let [pods-dir (System/getenv "BABASHKA_PODS_DIR")]
|
||||
(io/file pods-dir)
|
||||
(io/file (or
|
||||
(System/getenv "XDG_CACHE_HOME")
|
||||
(System/getProperty "user.home"))
|
||||
".babashka"
|
||||
"pods"
|
||||
"pods"))]
|
||||
(io/file base-file
|
||||
"repository"
|
||||
(str pod-name)
|
||||
pod-version))
|
||||
pod-version
|
||||
(normalize-os (:os/name @os))
|
||||
(:os/arch @os))))
|
||||
|
||||
(defn data-dir
|
||||
^java.io.File
|
||||
[{pod-name :pod/name
|
||||
pod-version :pod/version}]
|
||||
(io/file (or
|
||||
(System/getenv "XDG_DATA_HOME")
|
||||
(System/getProperty "user.home"))
|
||||
".babashka"
|
||||
"pods"
|
||||
"repository"
|
||||
(io/file @pods-repo-dir
|
||||
(str pod-name)
|
||||
pod-version))
|
||||
pod-version
|
||||
(normalize-os (:os/name @os))
|
||||
(:os/arch @os)))
|
||||
|
||||
(defn sha256 [file]
|
||||
(let [buf (byte-array 8192)
|
||||
|
|
|
|||
|
|
@ -6,17 +6,18 @@
|
|||
(defn- unroot-resource [^String path]
|
||||
(symbol (.. path
|
||||
(substring 1)
|
||||
(replace \/ \. )
|
||||
(replace \/ \.)
|
||||
(replace \_ \-))))
|
||||
|
||||
(defn- process-namespace [{:keys [:name :vars]}]
|
||||
(binding [*ns* (load-string (format "(ns %s) *ns*" name))]
|
||||
(doseq [[var-sym v] vars]
|
||||
(when-let [maybe-core (some-> (ns-resolve *ns* var-sym) meta :ns str symbol)]
|
||||
(when (= 'clojure.core maybe-core)
|
||||
(ns-unmap *ns* var-sym)))
|
||||
(cond
|
||||
(ifn? v)
|
||||
(do
|
||||
(ns-unmap *ns* var-sym)
|
||||
(intern name var-sym v))
|
||||
(intern name var-sym v)
|
||||
(string? v)
|
||||
(load-string v)))))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
(ns babashka.pods.sci
|
||||
(:require [babashka.pods.impl :as impl]
|
||||
[sci.core :as sci]))
|
||||
[sci.core :as sci]
|
||||
[clojure.java.io :as io]
|
||||
[babashka.pods.impl.resolver :as resolver]
|
||||
[babashka.fs :as fs])
|
||||
(:import (java.io PushbackInputStream File)))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(defn- process-namespace [ctx {:keys [:name :vars]}]
|
||||
(let [env (:env ctx)
|
||||
|
|
@ -19,6 +25,50 @@
|
|||
(string? var-value)
|
||||
(sci/eval-string* ctx var-value))))))
|
||||
|
||||
(defn metadata-cache-file ^File [^File bb-edn-file pod-spec {:keys [:version :path]}]
|
||||
(if version
|
||||
(io/file (resolver/cache-dir {:pod/name pod-spec :pod/version version})
|
||||
"metadata.cache")
|
||||
(let [config-dir (.getParentFile bb-edn-file)
|
||||
cache-dir (io/file config-dir ".babashka")
|
||||
pod-file (-> path io/file .getName)
|
||||
cache-file (io/file cache-dir (str pod-file ".metadata.cache"))]
|
||||
cache-file)))
|
||||
|
||||
(defn load-metadata-from-cache [bb-edn-file pod-spec opts]
|
||||
(let [cache-file (metadata-cache-file bb-edn-file pod-spec opts)]
|
||||
(when (.exists cache-file)
|
||||
(with-open [r (PushbackInputStream. (io/input-stream cache-file))]
|
||||
(impl/read r)))))
|
||||
|
||||
(defn load-pod-metadata* [bb-edn-file pod-spec {:keys [:version :cache] :as opts}]
|
||||
(let [metadata (impl/load-pod-metadata pod-spec opts)
|
||||
cache-file (when (and metadata cache)
|
||||
(metadata-cache-file bb-edn-file pod-spec opts))]
|
||||
(when cache-file
|
||||
(io/make-parents cache-file)
|
||||
(when (fs/writable? (fs/parent cache-file))
|
||||
(with-open [w (io/output-stream cache-file)]
|
||||
(impl/write w metadata))))
|
||||
metadata))
|
||||
|
||||
(defn load-pod-metadata
|
||||
([pod-spec opts] (load-pod-metadata nil pod-spec opts))
|
||||
([bb-edn-file pod-spec {:keys [:cache] :as opts}]
|
||||
(let [metadata
|
||||
(if-let [cached-metadata (when cache
|
||||
(load-metadata-from-cache bb-edn-file
|
||||
pod-spec
|
||||
opts))]
|
||||
cached-metadata
|
||||
(load-pod-metadata* bb-edn-file pod-spec opts))]
|
||||
(reduce
|
||||
(fn [pod-namespaces ns]
|
||||
(let [ns-sym (-> ns (get "name") impl/bytes->string symbol)]
|
||||
(assoc pod-namespaces ns-sym {:pod-spec pod-spec
|
||||
:opts (assoc opts :metadata metadata)})))
|
||||
{} (get metadata "namespaces")))))
|
||||
|
||||
(defn load-pod
|
||||
([ctx pod-spec] (load-pod ctx pod-spec nil))
|
||||
([ctx pod-spec version opts] (load-pod ctx pod-spec (assoc opts :version version)))
|
||||
|
|
|
|||
|
|
@ -61,6 +61,12 @@
|
|||
(transit/write w s)
|
||||
(str baos))))
|
||||
|
||||
(defn transit-json-write-meta [s]
|
||||
(with-open [baos (java.io.ByteArrayOutputStream. 4096)]
|
||||
(let [w (transit/writer baos :json {:transform transit/write-meta})]
|
||||
(transit/write w s)
|
||||
(str baos))))
|
||||
|
||||
(defn run-pod [cli-args]
|
||||
(let [format (cond (contains? cli-args "--json") :json
|
||||
(contains? cli-args "--transit+json") :transit+json
|
||||
|
|
@ -130,6 +136,10 @@
|
|||
{"name" "read-other-tag"
|
||||
"code" "(defn read-other-tag [x] [x x])"
|
||||
"meta" "{:doc \"unread\"}"}
|
||||
{"name" "round-trip-meta"
|
||||
"arg-meta" "true"}
|
||||
{"name" "dont-round-trip-meta"
|
||||
"arg-meta" "false"}
|
||||
{"name" "-local-date-time"}
|
||||
{"name" "transit-stuff"
|
||||
"code" "
|
||||
|
|
@ -151,7 +161,8 @@
|
|||
(babashka.pods/add-transit-read-handler! \"java.array\"
|
||||
into-array)
|
||||
|
||||
"}]
|
||||
"}
|
||||
{"name" "incorrect-edn"}]
|
||||
dependents)}
|
||||
{"name" "pod.test-pod.loaded"
|
||||
"defer" "true"}
|
||||
|
|
@ -207,14 +218,14 @@
|
|||
"id" id})
|
||||
pod.test-pod/print
|
||||
(do (write out
|
||||
{"out" (pr-str args)
|
||||
{"out" (with-out-str (prn args))
|
||||
"id" id})
|
||||
(write out
|
||||
{"status" ["done"]
|
||||
"id" id}))
|
||||
pod.test-pod/print-err
|
||||
(do (write out
|
||||
{"err" (pr-str args)
|
||||
{"err" (with-out-str (prn args))
|
||||
"id" id})
|
||||
(write out
|
||||
{"status" ["done"]
|
||||
|
|
@ -234,11 +245,32 @@
|
|||
{"status" ["done"]
|
||||
"id" id
|
||||
"value" "#my/other-tag[1]"})
|
||||
pod.test-pod/round-trip-meta
|
||||
(write out
|
||||
{"status" ["done"]
|
||||
"id" id
|
||||
"value"
|
||||
(case format
|
||||
:transit+json (transit-json-write-meta (first args))
|
||||
(write-fn (first args)))})
|
||||
pod.test-pod/dont-round-trip-meta
|
||||
(write out
|
||||
{"status" ["done"]
|
||||
"id" id
|
||||
"value"
|
||||
(case format
|
||||
:transit+json (transit-json-write-meta (first args))
|
||||
(write-fn (first args)))})
|
||||
pod.test-pod/-local-date-time
|
||||
(write out
|
||||
{"status" ["done"]
|
||||
"id" id
|
||||
"value" (write-fn (first args))}))
|
||||
"value" (write-fn (first args))})
|
||||
pod.test-pod/incorrect-edn
|
||||
(write out
|
||||
{"status" ["done"]
|
||||
"id" id
|
||||
"value" (write-fn {(keyword "foo bar") 1})}))
|
||||
(recur))
|
||||
:shutdown (System/exit 0)
|
||||
:load-ns (let [ns (-> (get message "ns")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
(require '[babashka.pods :as pods])
|
||||
|
||||
(pods/load-pod 'org.babashka/buddy "0.0.1")
|
||||
(pods/load-pod 'org.babashka/buddy "0.1.0")
|
||||
|
||||
(require '[pod.babashka.buddy.codecs :as codecs]
|
||||
'[pod.babashka.buddy.hash :as hash])
|
||||
|
|
@ -8,4 +8,4 @@
|
|||
(println (-> (hash/sha256 "foobar")
|
||||
(codecs/bytes->hex)))
|
||||
|
||||
(pods/load-pod 'org.babashka/etaoin) ;; should cause error when version is missing
|
||||
(pods/load-pod 'org.babashka/etaoin) ;; should cause error when version & path are missing
|
||||
|
|
|
|||
|
|
@ -84,12 +84,31 @@
|
|||
(.isArray (class v)))
|
||||
true))
|
||||
|
||||
(def round-trip-meta
|
||||
(if (= "transit+json" fmt)
|
||||
(= {:my-meta 2} (meta (pod.test-pod/round-trip-meta (with-meta [2] {:my-meta 2}))))
|
||||
true))
|
||||
|
||||
(def round-trip-meta-nested
|
||||
(if (= "transit+json" fmt)
|
||||
(= {:my-meta 3} (meta (first (pod.test-pod/round-trip-meta [(with-meta [3] {:my-meta 3})]))))
|
||||
true))
|
||||
|
||||
(def dont-round-trip-meta
|
||||
(if (= "transit+json" fmt)
|
||||
(= nil (meta (pod.test-pod/dont-round-trip-meta (with-meta [2] {:my-meta 2}))))
|
||||
true))
|
||||
|
||||
(require '[pod.test-pod.only-code :as only-code])
|
||||
(def should-be-1 (only-code/foo))
|
||||
|
||||
(require '[pod.test-pod.loaded2 :as loaded2])
|
||||
(def loaded (loaded2/loaded 1))
|
||||
|
||||
(def incorrect-edn-response
|
||||
(try (pod.test-pod/incorrect-edn)
|
||||
(catch Exception e (ex-message e))))
|
||||
|
||||
(pods/unload-pod pod-id)
|
||||
(def successfully-removed (nil? (find-ns 'pod.test-pod)))
|
||||
|
||||
|
|
@ -112,7 +131,11 @@
|
|||
fn-called
|
||||
local-date-time
|
||||
assoc-string-array
|
||||
round-trip-meta
|
||||
round-trip-meta-nested
|
||||
dont-round-trip-meta
|
||||
should-be-1
|
||||
add-sync-meta
|
||||
error-meta
|
||||
read-other-tag-meta]
|
||||
read-other-tag-meta
|
||||
incorrect-edn-response]
|
||||
|
|
|
|||
11
test/babashka/pods/impl_test.clj
Normal file
11
test/babashka/pods/impl_test.clj
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
(ns babashka.pods.impl-test
|
||||
(:require [clojure.test :refer :all]
|
||||
[babashka.pods.impl :refer :all]))
|
||||
|
||||
(deftest load-pod-test
|
||||
(testing "resolve fn gets called when pod has EDN data readers"
|
||||
(let [resolved? (atom false)
|
||||
test-resolve (fn [_sym]
|
||||
(reset! resolved? true))]
|
||||
(load-pod ["clojure" "-M:test-pod"] {:resolve test-resolve})
|
||||
(is @resolved?))))
|
||||
|
|
@ -25,4 +25,4 @@
|
|||
(catch Exception e
|
||||
e)))]
|
||||
(is (str/includes? (str out) "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"))
|
||||
(is (str/includes? (pr-str ex) "Version must be provided" ))))
|
||||
(is (str/includes? (pr-str ex) "Version or path must be provided"))))
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@
|
|||
_ (vreset! ctx-ref ctx)
|
||||
ret (sci/binding [sci/out out
|
||||
sci/err err]
|
||||
(sci/eval-string* ctx test-program))]
|
||||
(binding [*out* out
|
||||
*err* err]
|
||||
(sci/eval-string* ctx test-program)))]
|
||||
(assertions out err ret)))
|
||||
|
||||
(deftest pod-registry-test
|
||||
|
|
@ -36,4 +38,4 @@
|
|||
(catch Exception e
|
||||
e)))]
|
||||
(is (str/includes? (str out) "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"))
|
||||
(is (str/includes? (pr-str ex) "Version must be provided" ))))
|
||||
(is (str/includes? (pr-str ex) "Version or path must be provided"))))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
(ns babashka.pods.test-common
|
||||
(:require [clojure.java.io :as io]
|
||||
[clojure.string :as str]
|
||||
[clojure.test :refer [is]]))
|
||||
|
||||
(def test-program (slurp (io/file "test-resources" "test_program.clj")))
|
||||
|
|
@ -9,7 +10,12 @@
|
|||
;; (.println System/err out)
|
||||
;; (.println System/err err)
|
||||
(doseq [[expected actual]
|
||||
(map vector '["pod.test-pod"
|
||||
(map vector (replace
|
||||
{::edn-error (if (= "edn"
|
||||
(System/getenv "BABASHKA_POD_TEST_FORMAT"))
|
||||
"Map literal must contain an even number of forms"
|
||||
::dont-care)}
|
||||
'["pod.test-pod"
|
||||
pod.test-pod
|
||||
{:a 1, :b 2}
|
||||
6
|
||||
|
|
@ -28,15 +34,21 @@
|
|||
3
|
||||
true ;; local-date
|
||||
true ;; roundtrip string array
|
||||
true ;; roundtrip metadata
|
||||
true ;; roundtrip metadata nested
|
||||
true ;; dont roundtrip metadata (when arg-meta "false"/ absent)
|
||||
1
|
||||
"add the arguments"
|
||||
nil
|
||||
nil]
|
||||
nil
|
||||
::edn-error])
|
||||
(concat ret (repeat ::nil)))]
|
||||
(if (instance? java.util.regex.Pattern expected)
|
||||
(cond (instance? java.util.regex.Pattern expected)
|
||||
(is (re-find expected actual))
|
||||
(= ::dont-care expected) nil
|
||||
:else
|
||||
(is (= expected actual))))
|
||||
(is (= "(\"hello\" \"print\" \"this\" \"debugging\" \"message\")\n:foo\n:foo\n" (str out)))
|
||||
(is (= "(\"hello\" \"print\" \"this\" \"error\")\n" (str err))))
|
||||
(is (str/starts-with? (str err) "(\"hello\" \"print\" \"this\" \"error\")" )))
|
||||
|
||||
(def pod-registry (slurp (io/file "test-resources" "pod_registry.clj")))
|
||||
|
|
|
|||
Loading…
Reference in a new issue