diff --git a/.circleci/config.yml b/.circleci/config.yml index c25a8ea2..a7cb1471 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,6 +38,7 @@ jobs: name: Run JVM tests command: | script/test + script/run_lib_tests # - run: # name: Run as tools.deps dependency # command: | @@ -227,16 +228,20 @@ jobs: paths: - ~/.m2 key: v1-dependencies-{{ checksum "project.clj" }} - # docker: - # docker: - # - image: circleci/buildpack-deps:stretch - # steps: - # - checkout - # - setup_remote_docker: - # docker_layer_caching: true - # - run: - # name: Build Docker image - # command: .circleci/script/docker + docker: + docker: + - image: circleci/buildpack-deps:stretch + steps: + - checkout + - run: + name: "Pull Submodules" + command: | + git submodule init + git submodule update + - setup_remote_docker + - run: + name: Build Docker image + command: .circleci/script/docker workflows: version: 2 @@ -253,11 +258,11 @@ workflows: - jvm - linux - mac - # - docker: - # filters: - # branches: - # only: master - # requires: - # - jvm - # - linux - # - mac + - docker: + filters: + branches: + only: master + requires: + - jvm + - linux + - mac diff --git a/.circleci/script/docker b/.circleci/script/docker index 69384b31..d2bda10e 100755 --- a/.circleci/script/docker +++ b/.circleci/script/docker @@ -1,7 +1,9 @@ #!/usr/bin/env bash -image_name="borkdude/clj-kondo" -image_tag=$(cat resources/CLJ_KONDO_VERSION) +set -eo pipefail + +image_name="borkdude/babashka" +image_tag=$(cat resources/BABASHKA_VERSION) latest_tag="latest" if [[ $image_tag =~ SNAPSHOT$ ]]; then diff --git a/.circleci/script/publish_artifact.clj b/.circleci/script/publish_artifact.clj index cb55f321..1b5f5b74 100755 --- a/.circleci/script/publish_artifact.clj +++ b/.circleci/script/publish_artifact.clj @@ -1,14 +1,18 @@ (require '[clojure.java.shell :refer [sh]] - '[cheshire.core :refer [generate-string]]) + '[clojure.java.io :as io] + '[cheshire.core :refer [generate-string]] + '[clojure.string :as str]) (def channel "#babashka_circleci_builds") #_(def channel "#_test") +(def babashka-version (str/trim (slurp (io/file "resources" "BABASHKA_VERSION")))) -(def text (format "[%s - %s@%s]: https://%s-201467090-gh.circle-artifacts.com/0/release/babashka-0.0.61-SNAPSHOT-%s-amd64.zip" +(def text (format "[%s - %s@%s]: https://%s-201467090-gh.circle-artifacts.com/0/release/babashka-%s-%s-amd64.zip" (System/getenv "BABASHKA_PLATFORM") (System/getenv "CIRCLE_BRANCH") (System/getenv "CIRCLE_SHA1") (System/getenv "CIRCLE_BUILD_NUM") + babashka-version (System/getenv "BABASHKA_PLATFORM"))) (def slack-hook-url (System/getenv "SLACK_HOOK_URL")) diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index e27865ec..e4db19e4 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -3,4 +3,5 @@ babashka.impl.Pattern/gen-wrapper-fn clojure.core/def babashka.impl.File/gen-wrapper-fn-2 clojure.core/def babashka.impl.Pattern/gen-wrapper-fn-2 clojure.core/def - babashka.impl.Pattern/gen-constants clojure.core/declare}} + babashka.impl.Pattern/gen-constants clojure.core/declare} + :linters {:unsorted-namespaces {:level :warning}}} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e45310df..c7fbd994 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,6 +1,6 @@ # These are supported funding model platforms -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: borkdude # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: borkdude open_collective: # Replace with a single Open Collective username ko_fi: borkdude diff --git a/CHANGES.md b/CHANGES.md index e75cccb9..fff6f671 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,10 @@ ## Breaking changes +## v0.0.71 +- #267 Change behavior of reader conditionals: the `:clj` branch is taken when + it occurs before a `:bb` branch. + ## v0.0.44 - 0.0.45 - #173: Rename `*in*` to `*input*` (in the `user` namespace). The reason for this is that itt shadowed `clojure.core/*in*` when used unqualified. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..267c3304 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM ubuntu AS BASE + +RUN apt-get update +RUN apt-get install -yy curl unzip build-essential zlib1g-dev +WORKDIR "/opt" +RUN curl -sLO https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java8-linux-amd64-19.3.1.tar.gz +RUN tar -xzf graalvm-ce-java8-linux-amd64-19.3.1.tar.gz +ENV GRAALVM_HOME="/opt/graalvm-ce-java8-19.3.1" +ENV JAVA_HOME="/opt/graalvm-ce-java8-19.3.1/bin" +ENV PATH="$PATH:$JAVA_HOME" +COPY . . +RUN apt install -y sudo +RUN ./.circleci/script/install-leiningen +RUN ./script/compile +RUN cp bb /usr/local/bin + + +FROM ubuntu:bionic +COPY --from=BASE /usr/local/bin/bb /usr/local/bin +CMD ["bb"] diff --git a/README.md b/README.md index 3adb0722..00c3e269 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,11 @@ Upgrade: yay -S babashka-bin +### Windows + +On Windows you can install using [scoop](https://scoop.sh/) and the +[scoop-clojure](https://github.com/littleli/scoop-clojure) bucket. + ### Installer script Install via the installer script: @@ -153,9 +158,10 @@ Options: -f, --file Evaluate a file. -cp, --classpath Classpath to use. -m, --main Call the -main function from namespace with args. - --repl Start REPL + --repl Start REPL. Use rlwrap for history. --socket-repl Start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666). --time Print execution time before exiting. + -- Stop parsing args and pass everything after -- to *command-line-args* If neither -e, -f, or --socket-repl are specified, then the first argument that is not parsed as a option is treated as a file if it exists, or as an expression otherwise. Everything after that is bound to *command-line-args*. @@ -172,7 +178,7 @@ enumerated explicitly. - `clojure.set` aliased as `set` - `clojure.edn` aliased as `edn`: - `read-string` -- `clojure.java.shell` aliases as `shell` +- `clojure.java.shell` aliased as `shell` - `clojure.java.io` aliased as `io`: - `as-relative-path`, `as-url`, `copy`, `delete-file`, `file`, `input-stream`, `make-parents`, `output-stream`, `reader`, `resource`, `writer` @@ -181,6 +187,8 @@ enumerated explicitly. `async`. The `alt` and `go` macros are not available but `alts!!` does work as it is a function. - `clojure.stacktrace` +- `clojure.test` +- `clojure.pprint`: `pprint` (currently backed by [fipp](https://github.com/brandonbloom/fipp)'s `fipp.edn/pprint`) - [`clojure.tools.cli`](https://github.com/clojure/tools.cli) aliased as `tools.cli` - [`clojure.data.csv`](https://github.com/clojure/data.csv) aliased as `csv` - [`cheshire.core`](https://github.com/dakrone/cheshire) aliased as `json` @@ -241,11 +249,29 @@ $ bb example.clj Command-line arguments can be retrieved using `*command-line-args*`. -### Additional functions +### Additional namespaces -Additionally, babashka adds the following functions: +#### babashka.classpath -- `wait/wait-for-port`. Usage: +Contains the function `add-classpath` which can be used to add to the classpath +dynamically: + +``` clojure +(require '[babashka.classpath :refer [add-classpath]] + '[clojure.java.shell :refer [sh]]) +(def medley-dep '{:deps {medley {:git/url "https://github.com/borkdude/medley" + :sha "91adfb5da33f8d23f75f0894da1defe567a625c0"}}}) +(def cp (:out (sh "clojure" "-Spath" "-Sdeps" (str medley-dep)))) +(add-classpath cp) +(require '[medley.core :as m]) +(m/index-by :id [{:id 1} {:id 2}]) ;;=> {1 {:id 1}, 2 {:id 2}} +``` + +#### babashka.wait + +Contains the functions: `wait-for-port` and `wait-for-path`. + +Usage of `wait-for-port`: ``` clojure (wait/wait-for-port "localhost" 8080) @@ -254,29 +280,37 @@ Additionally, babashka adds the following functions: Waits for TCP connection to be available on host and port. Options map supports `:timeout` and `:pause`. If `:timeout` is provided and reached, `:default`'s value (if any) is returned. The `:pause` option determines the time waited between retries. -- `wait/wait-for-path`. Usage: +Usage of `wait-for-path`: ``` clojure (wait/wait-for-path "/tmp/wait-path-test") (wait/wait-for-path "/tmp/wait-path-test" {:timeout 1000 :pause 1000}) ``` -Waits for file path to be available. Options map supports `:default`, `:timeout` and `:pause`. If `:timeout` is provided and reached, `:default`'s value (if any) is returned. The `:pause` option determines the time waited between retries. +Waits for file path to be available. Options map supports `:default`, `:timeout` +and `:pause`. If `:timeout` is provided and reached, `:default`'s value (if any) +is returned. The `:pause` option determines the time waited between retries. -- `sig/pipe-signal-received?`. Usage: +The namespace `babashka.wait` is aliased as `wait` in the `user` namespace. + +#### babashka.signal + +Contains the function `signal/pipe-signal-received?`. Usage: ``` clojure -(sig/pipe-signal-received?) +(signal/pipe-signal-received?) ``` Returns true if `PIPE` signal was received. Example: ``` shellsession -$ bb '((fn [x] (println x) (when (not (sig/pipe-signal-received?)) (recur (inc x)))) 0)' | head -n2 +$ bb '((fn [x] (println x) (when (not (signal/pipe-signal-received?)) (recur (inc x)))) 0)' | head -n2 1 2 ``` +The namespace `babashka.signal` is aliased as `signal` in the `user` namespace. + ## Running a file Scripts may be executed from a file using `-f` or `--file`: @@ -485,19 +519,49 @@ $ bb script.clj -h ## Reader conditionals -Babashka supports reader conditionals using the `:bb` feature: +Babashka supports reader conditionals by taking either the `:bb` or `:clj` +branch, whichever comes first. NOTE: the `:clj` branch behavior was added in +version 0.0.71, before that version the `:clj` branch was ignored. ``` clojure -$ cat example.clj -#?(:clj (in-ns 'foo) :bb (println "babashka doesn't support in-ns yet!")) +$ bb "#?(:bb :hello :clj :bye)" +:hello -$ ./bb example.clj -babashka doesn't support in-ns yet! +$ bb "#?(:clj :bye :bb :hello)" +:bye + +$ bb "[1 2 #?@(:bb [] :clj [1])]" +[1 2] ``` -## Socket REPL +## Running tests -Start the socket REPL like this: +Babashka bundles `clojure.test`. To make CI scripts fail you can use a simple +runner like this: + +``` shell +#!/usr/bin/env bash +bb -cp "src:test:resources" \ + -e "(require '[clojure.test :as t] '[borkdude.deps-test]) + (let [{:keys [:fail :error]} (t/run-tests 'borkdude.deps-test)] + (System/exit (+ fail error)))" +``` + +## REPL + +Babashka supports both a REPL and socket REPL. To start the REPL, type: + +``` shell +$ bb --repl +``` + +To get history with up and down arrows, use `rlwrap`: + +``` shell +$ rlwrap bb --repl +``` + +To start the socket REPL you can do this: ``` shellsession $ bb --socket-repl 1666 @@ -571,8 +635,10 @@ Differences with Clojure: - A subset of Java classes are supported. -- Only the `clojure.core`, `clojure.set`, `clojure.string` and `clojure.walk` - namespaces are available from Clojure. +- Only the `clojure.core`, `clojure.edn`, `clojue.java.io`, + `clojure.java.shell`, `clojure.set`, `clojure.stacktrace`, `clojure.string`, + `clojure.template`, `clojure.test` and `clojure.walk` namespaces are available + from Clojure. - Interpretation comes with overhead. Therefore tight loops are likely slower than in Clojure on the JVM. @@ -590,14 +656,43 @@ The following libraries are known to work with Babashka: A port of the [clojure](https://github.com/clojure/brew-install/) bash script to Clojure / babashka. -#### [spartan.test](https://github.com/borkdude/spartan.test/) +#### [spartan.spec](https://github.com/borkdude/spartan.spec/) -A minimal test framework compatible with babashka. +An babashka-compatible implementation of `clojure.spec.alpha`. -#### [medley](https://github.com/borkdude/medley/) +#### [missing.test.assertions](https://github.com/borkdude/missing.test.assertions) -A fork of [medley](https://github.com/weavejester/medley) made compatible with -babashka. Requires `bb` >= v0.0.58. +This library checks if no assertions have been made in a test: + +``` shell +$ export BABASHKA_CLASSPATH=$(clojure -Spath -Sdeps '{:deps {borkdude/missing.test.assertions {:git/url "https://github.com/borkdude/missing.test.assertions" :sha "603cb01bee72fb17addacc53c34c85612684ad70"}}}') + +$ lein bb "(require '[missing.test.assertions] '[clojure.test :as t]) (t/deftest foo) (t/run-tests)" + +Testing user +WARNING: no assertions made in test foo + +Ran 1 tests containing 0 assertions. +0 failures, 0 errors. +{:test 1, :pass 0, :fail 0, :error 0, :type :summary} +``` + +#### [medley](https://github.com/weavejester/medley/) + +Requires `bb` >= v0.0.71. Latest coordinates checked with with bb: + +``` clojure +{:git/url "https://github.com/weavejester" :sha "a4e5fb5383f5c0d83cb2d005181a35b76d8a136d"} +``` + +Example: + +``` shell +$ export BABASHKA_CLASSPATH=$(clojure -Spath -Sdeps '{:deps {medley {:git/url "https://github.com/weavejester" :sha "a4e5fb5383f5c0d83cb2d005181a35b76d8a136d"}}}') + +$ bb -e "(require '[medley.core :as m]) (m/index-by :id [{:id 1} {:id 2}])" +{1 {:id 1}, 2 {:id 2}} +``` #### [clj-http-lite](https://github.com/borkdude/clj-http-lite) @@ -612,7 +707,15 @@ $ bb "(require '[clj-http.lite.client :as client]) (:status (client/get \"https: #### [limit-break](https://github.com/technomancy/limit-break) -A debug REPL library. Example: +A debug REPL library. + +Latest coordinates checked with with bb: + +``` clojure +{:git/url "https://github.com/technomancy/limit-break" :sha "050fcfa0ea29fe3340927533a6fa6fffe23bfc2f" :deps/manifest :deps} +``` + +Example: ``` shell $ export BABASHKA_CLASSPATH="$(clojure -Sdeps '{:deps {limit-break {:git/url "https://github.com/technomancy/limit-break" :sha "050fcfa0ea29fe3340927533a6fa6fffe23bfc2f" :deps/manifest :deps}}}' -Spath)" @@ -626,8 +729,47 @@ break> x 1 ``` +#### [clojure-csv](https://github.com/davidsantiago/clojure-csv) + +A library for reading and writing CSV files. Note that babashka already comes +with `clojure.data.csv`, but in case you need this other library, this is how +you can use it: + +``` shell +export BABASHKA_CLASSPATH="$(clojure -Sdeps '{:deps {clojure-csv {:mvn/version "RELEASE"}}}' -Spath)" + +./bb -e " +(require '[clojure-csv.core :as csv]) +(csv/write-csv (csv/parse-csv \"a,b,c\n1,2,3\")) +" +``` + +#### [regal](https://github.com/lambdaisland/regal) + +Requires `bb` >= v0.0.71. Latest coordinates checked with with bb: + +``` clojure +{:git/url "https://github.com/lambdaisland/regal" :sha "8d300f8e15f43480801766b7762530b6d412c1e6"} +``` + +Example: + +``` shell +$ export BABASHKA_CLASSPATH=$(clojure -Spath -Sdeps '{:deps {regal {:git/url "https://github.com/lambdaisland/regal" :sha "8d300f8e15f43480801766b7762530b6d412c1e6"}}}') + +$ bb -e "(require '[lambdaisland.regal :as regal]) (regal/regex [:* \"ab\"])" +#"(?:\Qab\E)*" +``` + +#### [spartan.test](https://github.com/borkdude/spartan.test/) + +A minimal test framework compatible with babashka. This library is deprecated +since babashka v0.0.68 which has `clojure.test` built-in. + + ### Blogs +- [Babashka: a quick example](https://juxt.pro/blog/posts/babashka.html) by Malcolm Sparks - [Clojure Start Time in 2019](https://stuartsierra.com/2019/12/21/clojure-start-time-in-2019) by Stuart Sierra - [Advent of Random Hacks](https://lambdaisland.com/blog/2019-12-19-advent-of-parens-19-advent-of-random-hacks) @@ -704,14 +846,31 @@ bb '(-> *input* first :name (subs 1))' "0.0.4" ``` -### Get latest OS-specific download url from Github +### Generate deps.edn entry for a gitlib -``` shellsession -$ curl -s https://api.github.com/repos/borkdude/babashka/releases | -jet --from json --keywordize | -bb '(-> *input* first :assets)' | -bb '(some #(re-find #".*linux.*" (:browser_download_url %)) *input*)' -"https://github.com/borkdude/babashka/releases/download/v0.0.4/babashka-0.0.4-linux-amd64.zip" +``` clojure +#!/usr/bin/env bb + +(require '[clojure.java.shell :refer [sh]] + '[clojure.string :as str]) + +(let [[username project branch] *command-line-args* + branch (or branch "master") + url (str "https://github.com/" username "/" project) + sha (-> (sh "git" "ls-remote" url branch) + :out + (str/split #"\s") + first)] + {:git/url url + :sha sha}) +``` + +``` shell +$ gitlib.clj nate fs +{:git/url "https://github.com/nate/fs", :sha "75b9fcd399ac37cb4f9752a4c7a6755f3fbbc000"} +$ clj -Sdeps "{:deps {fs $(gitlib.clj nate fs)}}" \ + -e "(require '[nate.fs :as fs]) (fs/creation-time \".\")" +#object[java.nio.file.attribute.FileTime 0x5c748168 "2019-07-05T14:06:26Z"] ``` ### View download statistics from Clojars @@ -789,6 +948,18 @@ See [examples/http_server.clj](https://github.com/borkdude/babashka/blob/master/ Original by [@souenzzo](https://gist.github.com/souenzzo/a959a4c5b8c0c90df76fe33bb7dfe201) +### Print random docstring + +See [examples/random_doc.clj](https://github.com/borkdude/babashka/blob/master/examples/random_doc.clj) + +``` shell +$ examples/random_doc.clj +------------------------- +clojure.core/ffirst +([x]) + Same as (first (first x)) +``` + ## Thanks - [adgoji](https://www.adgoji.com/) for financial support diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..29ad77d2 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,61 @@ +--- + +version: "v-{build}" + +image: Visual Studio 2015 + +clone_folder: C:\projects\babashka + +environment: + GRAALVM_HOME: C:\projects\babashka\graalvm\graalvm-ce-java8-19.3.0 + +cache: + - C:\ProgramData\chocolatey\lib -> project.clj, appveyor.yml + - '%USERPROFILE%\.m2 -> project.clj' + - 'graalvm -> appveyor.yml' + +clone_script: +- ps: >- + if(-not $env:APPVEYOR_PULL_REQUEST_NUMBER) { + git clone -q --branch=$env:APPVEYOR_REPO_BRANCH https://github.com/$env:APPVEYOR_REPO_NAME.git $env:APPVEYOR_BUILD_FOLDER + cd $env:APPVEYOR_BUILD_FOLDER + git checkout -qf $env:APPVEYOR_REPO_COMMIT + } else { + git clone -q https://github.com/$env:APPVEYOR_REPO_NAME.git $env:APPVEYOR_BUILD_FOLDER + cd $env:APPVEYOR_BUILD_FOLDER + git fetch -q origin +refs/pull/$env:APPVEYOR_PULL_REQUEST_NUMBER/merge: + git checkout -qf FETCH_HEAD + } +- cmd: git submodule update --init --recursive + +build_script: +- cmd: >- + powershell -Command "(New-Object Net.WebClient).DownloadFile('https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein.bat', 'lein.bat')" + + call lein self-install + +# set CLJ_KONDO_TEST_ENV=jvm + +# call script/test.bat + +- cmd: >- + choco install windows-sdk-7.1 + + call "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" + + powershell -Command "if (Test-Path('graalvm')) { return } else { (New-Object Net.WebClient).DownloadFile('https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.0/graalvm-ce-java8-windows-amd64-19.3.0.zip', 'graalvm.zip') }" + + powershell -Command "if (Test-Path('graalvm')) { return } else { Expand-Archive graalvm.zip graalvm }" + +# call script/compile.bat + +# - cmd: >- +# lein clean + +# set CLJ_KONDO_TEST_ENV=native + +# call script/test.bat + +# artifacts: +# - path: babashka-*-windows-amd64.zip +# name: babashka diff --git a/deps.edn b/deps.edn index e0eef190..2c28c015 100644 --- a/deps.edn +++ b/deps.edn @@ -1,13 +1,20 @@ {:paths ["src" "sci/src" "resources" "sci/resources"], :deps {org.clojure/clojure {:mvn/version "1.10.1"}, org.clojure/tools.reader {:mvn/version "1.3.2"}, - borkdude/edamame {:mvn/version "0.0.10-alpha.4"}, + borkdude/edamame {:mvn/version "0.0.10"}, borkdude/graal.locking {:mvn/version "0.0.2"}, borkdude/sci.impl.reflector {:mvn/version "0.0.1"} - org.clojure/core.async {:mvn/version "0.4.500"}, + org.clojure/core.async {:mvn/version "1.0.567"}, org.clojure/tools.cli {:mvn/version "0.4.2"}, - org.clojure/data.csv {:mvn/version "0.1.4"}, - org.clojure/data.xml {:mvn/version "0.2.0-alpha6"}, - cheshire {:mvn/version "5.9.0"}} + org.clojure/data.csv {:mvn/version "1.0.0"}, + cheshire {:mvn/version "5.10.0"} + org.clojure/data.xml {:mvn/version "0.2.0-alpha6"} + fipp {:mvn/version "0.6.22"}} :aliases {:main - {:main-opts ["-m" "babashka.main"]}}} + {:main-opts ["-m" "babashka.main"]} + :profile + {:extra-deps + {com.clojure-goes-fast/clj-async-profiler {:mvn/version "0.4.0"}} + :extra-paths ["test"] + :jvm-opts ["-Djdk.attach.allowAttachSelf"] + :main-opts ["-m" "babashka.profile"]}}} diff --git a/doc/dev.md b/doc/dev.md index 5de89932..a5c1b8da 100644 --- a/doc/dev.md +++ b/doc/dev.md @@ -43,7 +43,7 @@ Test the native version: ## Build -To build this project, set `$GRAALVM_HOME` to the GraalVM distribution directory. +To build this project, set `$GRAALVM_HOME` to the GraalVM distribution directory. Currently we are using GraalVM JDK8. Then run: @@ -58,6 +58,22 @@ We're only registering the size of the macOS binary (as built on CircleCI). 2020/01/08, ..., 38.7mb / 11.3mb zipped Added: `clojure.data.xml`. Growth: 1.8mb / 0.4mb zipped. -2020/01/08, 303ca9e825d76a4a45bc4240a59139d342c13964: 36.9mb / 10.8mb zipped. +2020/02/19, e43727955a2cdabd2bb0189c20dd7f9a18156fc9 +Added fipp.edn/pprint +40598268 - 39744804 = 853kb added. + +2020/02/09, c8fd1c7931d7842ebaec1fa8faf06d4ab58573bd +Added java.lang.BigInteger and java.security.MessageDigest. +39281972 - 39072764 = 209kb added. + +2020/04/02 v0.0.69 38883676 + +2020/01/24, 43eef7075f9dac038d8d28a5ee4e49b6affd9864: 38.3mb, 11.1mb zipped +Added hierarchies (derive, isa?, etc). + +2020/01/23, 485fef7df54d6701936704573468a1ec4c66d221: 37.4mb / 10.9mb zipped +Added: StringBuilder, java.io.{Reader,Writer,PrinterWriter,PushbackReader} + +2020/01/08, 303ca9e825d76a4a45bc4240a59139d342c13964: 36.9mb / 10.8mb zipped Removing cheshire from bb: 36.2mb / 10.5mb zipped. diff --git a/examples/random_doc.clj b/examples/random_doc.clj new file mode 100755 index 00000000..c30c4214 --- /dev/null +++ b/examples/random_doc.clj @@ -0,0 +1,11 @@ +#!/usr/bin/env bb + +(require '[clojure.repl]) + +(defmacro random-doc [] + (let [sym (-> (ns-publics 'clojure.core) keys rand-nth)] + (if (:doc (meta (resolve sym))) + `(clojure.repl/doc ~sym) + `(random-doc)))) + +(random-doc) diff --git a/project.clj b/project.clj index 70a59c9d..8bd811f8 100644 --- a/project.clj +++ b/project.clj @@ -11,15 +11,17 @@ :resource-paths ["resources" "sci/resources"] :dependencies [[org.clojure/clojure "1.10.1"] [org.clojure/tools.reader "1.3.2"] - [borkdude/edamame "0.0.10-alpha.4"] + [borkdude/edamame "0.0.10"] [borkdude/graal.locking "0.0.2"] [borkdude/sci.impl.reflector "0.0.1"] - [org.clojure/core.async "0.4.500"] + [org.clojure/core.async "1.0.567"] [org.clojure/tools.cli "0.4.2"] - [org.clojure/data.csv "0.1.4"] + [org.clojure/data.csv "1.0.0"] [org.clojure/data.xml "0.2.0-alpha6"] - [cheshire "5.9.0"]] - :profiles {:test {:dependencies [[clj-commons/conch "0.9.2"]]} + [cheshire "5.10.0"] + [fipp "0.6.22"]] + :profiles {:test {:dependencies [[clj-commons/conch "0.9.2"] + [com.clojure-goes-fast/clj-async-profiler "0.4.0"]]} :uberjar {:global-vars {*assert* false} :jvm-opts ["-Dclojure.compiler.direct-linking=true" "-Dclojure.spec.skip-macros=true"] diff --git a/resources/BABASHKA_RELEASED_VERSION b/resources/BABASHKA_RELEASED_VERSION index 67ccd586..cf9f1739 100644 --- a/resources/BABASHKA_RELEASED_VERSION +++ b/resources/BABASHKA_RELEASED_VERSION @@ -1 +1 @@ -0.0.60 \ No newline at end of file +0.0.71 \ No newline at end of file diff --git a/resources/BABASHKA_VERSION b/resources/BABASHKA_VERSION index e2e3d510..75b9c2a8 100644 --- a/resources/BABASHKA_VERSION +++ b/resources/BABASHKA_VERSION @@ -1 +1 @@ -0.0.61-SNAPSHOT \ No newline at end of file +0.0.72-SNAPSHOT \ No newline at end of file diff --git a/sci b/sci index 57b584ba..eebb4566 160000 --- a/sci +++ b/sci @@ -1 +1 @@ -Subproject commit 57b584ba0a6a1f74a887d350463a700976dd37d8 +Subproject commit eebb456628beb2ac0d1e31c2be46ee0683b9ee7a diff --git a/script/compile b/script/compile index ac925a5a..fbb59dc7 100755 --- a/script/compile +++ b/script/compile @@ -2,19 +2,17 @@ set -eo pipefail -NATIVE_IMAGE=`which native-image` || true - -if [ -z "$NATIVE_IMAGE" ]; then - if [ -z "$GRAALVM_HOME" ]; then - echo "Please set GRAALVM_HOME" - exit 1 - fi - - "$GRAALVM_HOME/bin/gu" install native-image || true - - NATIVE_IMAGE="$GRAALVM_HOME/bin/native-image" +if [ -z "$GRAALVM_HOME" ]; then + echo "Please set GRAALVM_HOME" + exit 1 fi +if [ -z "$BABASHKA_XMX" ]; then + export BABASHKA_XMX="-J-Xmx3g" +fi + +"$GRAALVM_HOME/bin/gu" install native-image || true + BABASHKA_VERSION=$(cat resources/BABASHKA_VERSION) # # We also need to AOT sci, else something didn't work in the Mac build on CircleCI @@ -23,10 +21,12 @@ BABASHKA_VERSION=$(cat resources/BABASHKA_VERSION) # mkdir -p src/sci # cp -R /tmp/sci/src/* src +export JAVA_HOME=$GRAALVM_HOME + lein with-profiles +reflection do run lein do clean, uberjar -$NATIVE_IMAGE \ +$GRAALVM_HOME/bin/native-image \ -jar target/babashka-$BABASHKA_VERSION-standalone.jar \ -H:Name=bb \ -H:+ReportExceptionStackTraces \ @@ -44,6 +44,6 @@ $NATIVE_IMAGE \ --verbose \ --no-fallback \ --no-server \ - "-J-Xmx3g" + "$BABASHKA_XMX" lein clean diff --git a/script/lib_tests/clj_http_lite_test b/script/lib_tests/clj_http_lite_test index d9c0cf43..8ca39fb0 100755 --- a/script/lib_tests/clj_http_lite_test +++ b/script/lib_tests/clj_http_lite_test @@ -4,7 +4,13 @@ set -eo pipefail export BABASHKA_CLASSPATH=$(clojure -Sdeps '{:deps {clj-http-lite {:git/url "https://github.com/borkdude/clj-http-lite" :sha "f44ebe45446f0f44f2b73761d102af3da6d0a13e"}}}' -Spath) -./bb -e " +if [ "$BABASHKA_TEST_ENV" = "native" ]; then + BB_CMD="./bb" +else + BB_CMD="lein bb" +fi + +$BB_CMD -e " (require '[clj-http.lite.client :as client]) (prn (:status (client/get \"https://www.clojure.org\"))) @@ -13,5 +19,15 @@ export BABASHKA_CLASSPATH=$(clojure -Sdeps '{:deps {clj-http-lite {:git/url "htt (prn (:status (client/post \"https://postman-echo.com/post\"))) -(prn (:status (client/put \"https://postman-echo.com/put\"))) +(prn (:status (client/post \"https://postman-echo.com/post\" + {:body (json/generate-string {:a 1}) + :headers {\"X-Hasura-Role\" \"admin\"} + :content-type :json + :accept :json}))) + +(prn (:status (client/put \"https://postman-echo.com/put\" + {:body (json/generate-string {:a 1}) + :headers {\"X-Hasura-Role\" \"admin\"} + :content-type :json + :accept :json}))) " diff --git a/script/lib_tests/clojure_csv_test b/script/lib_tests/clojure_csv_test new file mode 100755 index 00000000..c9b6adf0 --- /dev/null +++ b/script/lib_tests/clojure_csv_test @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -eo pipefail + +export BABASHKA_CLASSPATH="$(clojure -Sdeps '{:deps {clojure-csv {:mvn/version "RELEASE"}}}' -Spath)" + +if [ "$BABASHKA_TEST_ENV" = "native" ]; then + BB_CMD="./bb" +else + BB_CMD="lein bb" +fi + +$BB_CMD -e " +(require '[clojure-csv.core :as csv]) +(prn (csv/write-csv (csv/parse-csv \"a,b,c\n1,2,3\"))) +" diff --git a/script/lib_tests/deps_clj_test b/script/lib_tests/deps_clj_test index 29d78dc3..d23ae8b6 100755 --- a/script/lib_tests/deps_clj_test +++ b/script/lib_tests/deps_clj_test @@ -2,8 +2,14 @@ set -eo pipefail +if [ "$BABASHKA_TEST_ENV" = "native" ]; then + BB_CMD="./bb" +else + BB_CMD="lein bb" +fi + curl -sL https://raw.githubusercontent.com/borkdude/deps.clj/master/deps.clj -o deps_test.clj chmod +x deps_test.clj -./bb deps_test.clj -Sdescribe +$BB_CMD deps_test.clj -Sdescribe rm deps_test.clj diff --git a/script/lib_tests/medley_test b/script/lib_tests/medley_test new file mode 100755 index 00000000..9bdd30fd --- /dev/null +++ b/script/lib_tests/medley_test @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -eo pipefail + +export BABASHKA_CLASSPATH="$(clojure -Sdeps '{:deps {medley {:git/url "https://github.com/weavejester/medley" :sha "a4e5fb5383f5c0d83cb2d005181a35b76d8a136d"}}}' -Spath)" + +if [ "$BABASHKA_TEST_ENV" = "native" ]; then + BB_CMD="./bb" +else + BB_CMD="lein bb" +fi + +$BB_CMD " +(require '[medley.core :refer [index-by random-uuid]]) +(prn (index-by :id [{:id 1} {:id 2}])) +(prn (random-uuid)) +" diff --git a/script/lib_tests/regal_test b/script/lib_tests/regal_test new file mode 100755 index 00000000..76dcca5a --- /dev/null +++ b/script/lib_tests/regal_test @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -eo pipefail + +export BABASHKA_CLASSPATH="$(clojure -Sdeps '{:deps {regal {:git/url "https://github.com/lambdaisland/regal" :sha "8d300f8e15f43480801766b7762530b6d412c1e6"}}}' -Spath)" + +if [ "$BABASHKA_TEST_ENV" = "native" ]; then + BB_CMD="./bb" +else + BB_CMD="lein bb" +fi + +$BB_CMD "(require '[lambdaisland.regal :as re]) (re/regex [:range \a \z])" diff --git a/script/lib_tests/spartan_spec_test b/script/lib_tests/spartan_spec_test index 5afe02ef..0100ca83 100755 --- a/script/lib_tests/spartan_spec_test +++ b/script/lib_tests/spartan_spec_test @@ -4,8 +4,14 @@ set -eo pipefail export BABASHKA_CLASSPATH=$(clojure -Sdeps '{:deps {spartan.spec {:git/url "https://github.com/borkdude/spartan.spec" :sha "16f7eec4b6589c77c96c9fcf989f78fffcee7c4c"}}}' -Spath) -./bb -e " -(require '[spartan.spec :as s]) -(s/explain (s/cat :i int? :s string?) [1 :foo]) -(s/conform (s/cat :i int? :s string?) [1 \"foo\"]) +if [ "$BABASHKA_TEST_ENV" = "native" ]; then + BB_CMD="./bb" +else + BB_CMD="lein bb" +fi + +$BB_CMD -e " +(time (require '[spartan.spec :as s])) +(time (s/explain (s/cat :i int? :s string?) [1 :foo])) +(time (s/conform (s/cat :i int? :s string?) [1 \"foo\"])) " diff --git a/script/run_lib_tests b/script/run_lib_tests index 9e107365..8e0695a9 100755 --- a/script/run_lib_tests +++ b/script/run_lib_tests @@ -5,3 +5,6 @@ set -eo pipefail script/lib_tests/clj_http_lite_test script/lib_tests/deps_clj_test script/lib_tests/spartan_spec_test +script/lib_tests/clojure_csv_test +script/lib_tests/regal_test +script/lib_tests/medley_test diff --git a/src/babashka/impl/async.clj b/src/babashka/impl/async.clj index 61022448..9cfa421a 100644 --- a/src/babashka/impl/async.clj +++ b/src/babashka/impl/async.clj @@ -1,6 +1,7 @@ (ns babashka.impl.async {:no-doc true} - (:require [clojure.core.async :as async])) + (:require [clojure.core.async :as async] + [clojure.core.async.impl.protocols :as protocols])) (defn thread [_ _ & body] @@ -66,3 +67,5 @@ 'untap async/untap 'untap-all async/untap-all}) +(def async-protocols-namespace + {'ReadPort protocols/ReadPort}) diff --git a/src/babashka/impl/classes.clj b/src/babashka/impl/classes.clj index eacb8514..c6696613 100644 --- a/src/babashka/impl/classes.clj +++ b/src/babashka/impl/classes.clj @@ -21,8 +21,10 @@ java.io.InputStream java.io.IOException java.io.OutputStream + java.io.Reader java.io.StringReader java.io.StringWriter + java.io.Writer java.lang.ArithmeticException java.lang.AssertionError java.lang.Boolean @@ -30,15 +32,18 @@ java.lang.Double java.lang.Exception java.lang.Integer + java.lang.Long java.lang.Math java.util.concurrent.LinkedBlockingQueue java.lang.Object java.lang.String + java.lang.StringBuilder java.lang.System java.lang.Throwable java.lang.Process java.lang.ProcessBuilder java.lang.ProcessBuilder$Redirect + java.math.BigInteger java.net.URI java.net.HttpURLConnection java.net.ServerSocket @@ -58,6 +63,7 @@ java.nio.file.attribute.FileTime java.nio.file.attribute.PosixFilePermission java.nio.file.attribute.PosixFilePermissions + java.security.MessageDigest java.time.format.DateTimeFormatter java.time.Clock java.time.DateTimeException @@ -91,7 +97,10 @@ java.util.zip.GZIPOutputStream] :constructors [clojure.lang.Delay clojure.lang.MapEntry - clojure.lang.LineNumberingPushbackReader] + clojure.lang.LineNumberingPushbackReader + java.io.EOFException + java.io.PrintWriter + java.io.PushbackReader] :methods [borkdude.graal.LockFix ;; support for locking ] :fields [clojure.lang.PersistentQueue] @@ -183,7 +192,12 @@ (cond (instance? java.nio.file.Path v) java.nio.file.Path (instance? java.lang.Process v) - java.lang.Process))))) + java.lang.Process + ;; added for issue #239 regarding clj-http-lite + (instance? java.io.ByteArrayOutputStream v) + java.io.ByteArrayOutputStream + (instance? java.security.MessageDigest v) + java.security.MessageDigest))))) (def class-map (gen-class-map)) diff --git a/src/babashka/impl/clojure/core.clj b/src/babashka/impl/clojure/core.clj index d3a69ee4..bb7502af 100644 --- a/src/babashka/impl/clojure/core.clj +++ b/src/babashka/impl/clojure/core.clj @@ -6,6 +6,15 @@ (defn locking* [form bindings v f & args] (apply @#'locking/locking form bindings v f args)) +(defn time* + "Evaluates expr and prints the time it took. Returns the value of + expr." + [_ _ expr] + `(let [start# (. System (nanoTime)) + ret# ~expr] + (prn (str "Elapsed time: " (/ (double (- (. System (nanoTime)) start#)) 1000000.0) " msecs")) + ret#)) + (def core-extras {'file-seq file-seq 'agent agent @@ -18,5 +27,6 @@ 'shutdown-agents shutdown-agents 'slurp slurp 'spit spit + 'time (with-meta time* {:sci/macro true}) 'Throwable->map Throwable->map 'compare-and-set! compare-and-set!}) diff --git a/src/babashka/impl/clojure/core/server.clj b/src/babashka/impl/clojure/core/server.clj index 2b1c821f..113934a0 100644 --- a/src/babashka/impl/clojure/core/server.clj +++ b/src/babashka/impl/clojure/core/server.clj @@ -14,7 +14,8 @@ :no-doc true} babashka.impl.clojure.core.server (:refer-clojure :exclude [locking]) - (:require [sci.core :as sci]) + (:require [sci.core :as sci] + [sci.impl.vars :as vars]) (:import [clojure.lang LineNumberingPushbackReader] [java.net InetAddress Socket ServerSocket SocketException] @@ -44,7 +45,8 @@ (try (sci/with-bindings {sci/in in sci/out out - sci/err err} + sci/err err + vars/current-ns (vars/->SciNamespace 'user nil)} (swap! server assoc-in [:sessions client-id] {}) (apply accept args)) (catch SocketException _disconnect) diff --git a/src/babashka/impl/clojure/test.clj b/src/babashka/impl/clojure/test.clj new file mode 100644 index 00000000..60fcec78 --- /dev/null +++ b/src/babashka/impl/clojure/test.clj @@ -0,0 +1,787 @@ + ; Copyright (c) Rich Hickey. All rights reserved. + ; The use and distribution terms for this software are covered by the + ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) + ; which can be found in the file epl-v10.html at the root of this distribution. + ; By using this software in any fashion, you are agreeing to be bound by + ; the terms of this license. + ; You must not remove this notice, or any other, from this software. + +;;; test.clj: test framework for Clojure + +;; by Stuart Sierra +;; March 28, 2009 + +;; Thanks to Chas Emerick, Allen Rohner, and Stuart Halloway for +;; contributions and suggestions. + +(ns + ^{:author "Stuart Sierra, with contributions and suggestions by + Chas Emerick, Allen Rohner, and Stuart Halloway", + :doc "A unit testing framework. + + ASSERTIONS + + The core of the library is the \"is\" macro, which lets you make + assertions of any arbitrary expression: + + (is (= 4 (+ 2 2))) + (is (instance? Integer 256)) + (is (.startsWith \"abcde\" \"ab\")) + + You can type an \"is\" expression directly at the REPL, which will + print a message if it fails. + + user> (is (= 5 (+ 2 2))) + + FAIL in (:1) + expected: (= 5 (+ 2 2)) + actual: (not (= 5 4)) + false + + The \"expected:\" line shows you the original expression, and the + \"actual:\" shows you what actually happened. In this case, it + shows that (+ 2 2) returned 4, which is not = to 5. Finally, the + \"false\" on the last line is the value returned from the + expression. The \"is\" macro always returns the result of the + inner expression. + + There are two special assertions for testing exceptions. The + \"(is (thrown? c ...))\" form tests if an exception of class c is + thrown: + + (is (thrown? ArithmeticException (/ 1 0))) + + \"(is (thrown-with-msg? c re ...))\" does the same thing and also + tests that the message on the exception matches the regular + expression re: + + (is (thrown-with-msg? ArithmeticException #\"Divide by zero\" + (/ 1 0))) + + DOCUMENTING TESTS + + \"is\" takes an optional second argument, a string describing the + assertion. This message will be included in the error report. + + (is (= 5 (+ 2 2)) \"Crazy arithmetic\") + + In addition, you can document groups of assertions with the + \"testing\" macro, which takes a string followed by any number of + assertions. The string will be included in failure reports. + Calls to \"testing\" may be nested, and all of the strings will be + joined together with spaces in the final report, in a style + similar to RSpec + + (testing \"Arithmetic\" + (testing \"with positive integers\" + (is (= 4 (+ 2 2))) + (is (= 7 (+ 3 4)))) + (testing \"with negative integers\" + (is (= -4 (+ -2 -2))) + (is (= -1 (+ 3 -4))))) + + Note that, unlike RSpec, the \"testing\" macro may only be used + INSIDE a \"deftest\" or \"with-test\" form (see below). + + + DEFINING TESTS + + There are two ways to define tests. The \"with-test\" macro takes + a defn or def form as its first argument, followed by any number + of assertions. The tests will be stored as metadata on the + definition. + + (with-test + (defn my-function [x y] + (+ x y)) + (is (= 4 (my-function 2 2))) + (is (= 7 (my-function 3 4)))) + + As of Clojure SVN rev. 1221, this does not work with defmacro. + See http://code.google.com/p/clojure/issues/detail?id=51 + + The other way lets you define tests separately from the rest of + your code, even in a different namespace: + + (deftest addition + (is (= 4 (+ 2 2))) + (is (= 7 (+ 3 4)))) + + (deftest subtraction + (is (= 1 (- 4 3))) + (is (= 3 (- 7 4)))) + + This creates functions named \"addition\" and \"subtraction\", which + can be called like any other function. Therefore, tests can be + grouped and composed, in a style similar to the test framework in + Peter Seibel's \"Practical Common Lisp\" + + + (deftest arithmetic + (addition) + (subtraction)) + + The names of the nested tests will be joined in a list, like + \"(arithmetic addition)\", in failure reports. You can use nested + tests to set up a context shared by several tests. + + + RUNNING TESTS + + Run tests with the function \"(run-tests namespaces...)\": + + (run-tests 'your.namespace 'some.other.namespace) + + If you don't specify any namespaces, the current namespace is + used. To run all tests in all namespaces, use \"(run-all-tests)\". + + By default, these functions will search for all tests defined in + a namespace and run them in an undefined order. However, if you + are composing tests, as in the \"arithmetic\" example above, you + probably do not want the \"addition\" and \"subtraction\" tests run + separately. In that case, you must define a special function + named \"test-ns-hook\" that runs your tests in the correct order: + + (defn test-ns-hook [] + (arithmetic)) + + Note: test-ns-hook prevents execution of fixtures (see below). + + + OMITTING TESTS FROM PRODUCTION CODE + + You can bind the variable \"*load-tests*\" to false when loading or + compiling code in production. This will prevent any tests from + being created by \"with-test\" or \"deftest\". + + + FIXTURES + + Fixtures allow you to run code before and after tests, to set up + the context in which tests should be run. + + A fixture is just a function that calls another function passed as + an argument. It looks like this: + + (defn my-fixture [f] + Perform setup, establish bindings, whatever. + (f) Then call the function we were passed. + Tear-down / clean-up code here. + ) + + Fixtures are attached to namespaces in one of two ways. \"each\" + fixtures are run repeatedly, once for each test function created + with \"deftest\" or \"with-test\". \"each\" fixtures are useful for + establishing a consistent before/after state for each test, like + clearing out database tables. + + \"each\" fixtures can be attached to the current namespace like this: + (use-fixtures :each fixture1 fixture2 ...) + The fixture1, fixture2 are just functions like the example above. + They can also be anonymous functions, like this: + (use-fixtures :each (fn [f] setup... (f) cleanup...)) + + The other kind of fixture, a \"once\" fixture, is only run once, + around ALL the tests in the namespace. \"once\" fixtures are useful + for tasks that only need to be performed once, like establishing + database connections, or for time-consuming tasks. + + Attach \"once\" fixtures to the current namespace like this: + (use-fixtures :once fixture1 fixture2 ...) + + Note: Fixtures and test-ns-hook are mutually incompatible. If you + are using test-ns-hook, fixture functions will *never* be run. + + + SAVING TEST OUTPUT TO A FILE + + All the test reporting functions write to the var *test-out*. By + default, this is the same as *out*, but you can rebind it to any + PrintWriter. For example, it could be a file opened with + clojure.java.io/writer. + + + EXTENDING TEST-IS (ADVANCED) + + You can extend the behavior of the \"is\" macro by defining new + methods for the \"assert-expr\" multimethod. These methods are + called during expansion of the \"is\" macro, so they should return + quoted forms to be evaluated. + + You can plug in your own test-reporting framework by rebinding + the \"report\" function: (report event) + + The 'event' argument is a map. It will always have a :type key, + whose value will be a keyword signaling the type of event being + reported. Standard events with :type value of :pass, :fail, and + :error are called when an assertion passes, fails, and throws an + exception, respectively. In that case, the event will also have + the following keys: + + :expected The form that was expected to be true + :actual A form representing what actually occurred + :message The string message given as an argument to 'is' + + The \"testing\" strings will be a list in \"*testing-contexts*\", and + the vars being tested will be a list in \"*testing-vars*\". + + Your \"report\" function should wrap any printing calls in the + \"with-test-out\" macro, which rebinds *out* to the current value + of *test-out*. + + For additional event types, see the examples in the code. +"} + babashka.impl.clojure.test + (:require [babashka.impl.clojure.stacktrace :as stack] + [babashka.impl.common :refer [ctx]] + [clojure.string :as str] + [clojure.template :as temp] + [sci.core :as sci] + [sci.impl.analyzer :as ana] + [sci.impl.namespaces :as sci-namespaces] + [sci.impl.vars :as vars])) + +;; Nothing is marked "private" here, so you can rebind things to plug +;; in your own testing or reporting frameworks. + + +;;; USER-MODIFIABLE GLOBALS + +(defonce + ^{:doc "True by default. If set to false, no test functions will + be created by deftest, set-test, or with-test. Use this to omit + tests when compiling or loading production code."} + load-tests + (sci/new-dynamic-var '*load-tests* true)) + +(def + ^{:doc "The maximum depth of stack traces to print when an Exception + is thrown during a test. Defaults to nil, which means print the + complete stack trace."} + stack-trace-depth + (sci/new-dynamic-var '*stack-trace-depth* nil)) + + +;;; GLOBALS USED BY THE REPORTING FUNCTIONS + +(def report-counters (sci/new-dynamic-var '*report-counters* nil)) ; bound to a ref of a map in test-ns + +(def initial-report-counters ; used to initialize *report-counters* + (sci/new-dynamic-var '*initial-report-counters* {:test 0, :pass 0, :fail 0, :error 0})) + +(def testing-vars (sci/new-dynamic-var '*testing-vars* (list))) ; bound to hierarchy of vars being tested + +(def testing-contexts (sci/new-dynamic-var '*testing-contexts* (list))) ; bound to hierarchy of "testing" strings + +(def test-out (sci/new-dynamic-var '*test-out* sci/out)) ; PrintWriter for test reporting output + +(defmacro with-test-out-internal + "Runs body with *out* bound to the value of *test-out*." + {:added "1.1"} + [& body] + `(sci/binding [sci/out @test-out] + ~@body)) + +;;; UTILITIES FOR REPORTING FUNCTIONS + +(defn file-position + "Returns a vector [filename line-number] for the nth call up the + stack. + + Deprecated in 1.2: The information needed for test reporting is + now on :file and :line keys in the result map." + {:added "1.1" + :deprecated "1.2"} + [n] + (let [^StackTraceElement s (nth (.getStackTrace (new java.lang.Throwable)) n)] + [(.getFileName s) (.getLineNumber s)])) + +(defn testing-vars-str + "Returns a string representation of the current test. Renders names + in *testing-vars* as a list, then the source file and line of + current assertion." + {:added "1.1"} + [m] + (let [{:keys [:file :line]} (meta (first @testing-vars))] + (str + ;; Uncomment to include namespace in failure report: + ;;(ns-name (:ns (meta (first *testing-vars*)))) "/ " + (reverse (map #(:name (meta %)) @testing-vars)) + " (" file ":" line ")"))) + +(defn testing-contexts-str + "Returns a string representation of the current test context. Joins + strings in *testing-contexts* with spaces." + {:added "1.1"} + [] + (apply str (interpose " " (reverse @testing-contexts)))) + +(defn inc-report-counter + "Increments the named counter in *report-counters*, a ref to a map. + Does nothing if *report-counters* is nil." + {:added "1.1"} + [name] + (when @report-counters + (swap! @report-counters update-in [name] (fnil inc 0)))) + +;;; TEST RESULT REPORTING + +(defmulti + ^{:doc "Generic reporting function, may be overridden to plug in + different report formats (e.g., TAP, JUnit). Assertions such as + 'is' call 'report' to indicate results. The argument given to + 'report' will be a map with a :type key. See the documentation at + the top of test_is.clj for more information on the types of + arguments for 'report'." + :dynamic true + :added "1.1"} + report :type) + +(defn- stacktrace-file-and-line + [stacktrace] + (if (seq stacktrace) + (let [^StackTraceElement s (first stacktrace)] + {:file (.getFileName s) :line (.getLineNumber s)}) + {:file nil :line nil})) + +(defn do-report + "Add file and line information to a test result and call report. + If you are writing a custom assert-expr method, call this function + to pass test results to report." + {:added "1.2"} + [m] + (report + (case + (:type m) + :fail m + :error (merge (stacktrace-file-and-line (.getStackTrace ^Throwable (:actual m))) m) + m))) + +(defmethod report :default [m] + (with-test-out-internal (prn m))) + +(defmethod report :pass [m] + (with-test-out-internal (inc-report-counter :pass))) + +(defmethod report :fail [m] + (with-test-out-internal + (inc-report-counter :fail) + (println "\nFAIL in" (testing-vars-str m)) + (when (seq @testing-contexts) (println (testing-contexts-str))) + (when-let [message (:message m)] (println message)) + (println "expected:" (pr-str (:expected m))) + (println " actual:" (pr-str (:actual m))))) + +(defmethod report :error [m] + (with-test-out-internal + (inc-report-counter :error) + (println "\nERROR in" (testing-vars-str m)) + (when (seq @testing-contexts) (println (testing-contexts-str))) + (when-let [message (:message m)] (println message)) + (println "expected:" (pr-str (:expected m))) + (print " actual: ") + (let [actual (:actual m)] + (if (instance? Throwable actual) + (stack/print-cause-trace actual @stack-trace-depth) + (prn actual))))) + +(defmethod report :summary [m] + (with-test-out-internal + (println "\nRan" (:test m) "tests containing" + (+ (:pass m) (:fail m) (:error m)) "assertions.") + (println (:fail m) "failures," (:error m) "errors."))) + +(defmethod report :begin-test-ns [m] + (with-test-out-internal + (println "\nTesting" (sci-namespaces/sci-ns-name (:ns m))))) + +;; Ignore these message types: +(defmethod report :end-test-ns [m]) +(defmethod report :begin-test-var [m]) +(defmethod report :end-test-var [m]) + + + +;;; UTILITIES FOR ASSERTIONS + +(defn function? + "Returns true if argument is a function or a symbol that resolves to + a function (not a macro)." + {:added "1.1"} + [x] + (if (symbol? x) ;; TODO + (when-let [v (second (ana/lookup @ctx x false))] + (when-let [value (if (vars/var? v) @v v)] + (and (fn? value) + (not (:sci/macro (meta v)))))) + (fn? x))) + +(defn assert-predicate + "Returns generic assertion code for any functional predicate. The + 'expected' argument to 'report' will contains the original form, the + 'actual' argument will contain the form with all its sub-forms + evaluated. If the predicate returns false, the 'actual' form will + be wrapped in (not...)." + {:added "1.1"} + [msg form] + (let [args (rest form) + pred (first form)] + `(let [values# (list ~@args) + result# (apply ~pred values#)] + (if result# + (clojure.test/do-report {:type :pass, :message ~msg, + :expected '~form, :actual (cons ~pred values#)}) + (clojure.test/do-report {:type :fail, :message ~msg, + :expected '~form, :actual (list '~'not (cons '~pred values#))})) + result#))) + +(defn assert-any + "Returns generic assertion code for any test, including macros, Java + method calls, or isolated symbols." + {:added "1.1"} + [msg form] + `(let [value# ~form] + (if value# + (clojure.test/do-report {:type :pass, :message ~msg, + :expected '~form, :actual value#}) + (clojure.test/do-report {:type :fail, :message ~msg, + :expected '~form, :actual value#})) + value#)) + + + +;;; ASSERTION METHODS + +;; You don't call these, but you can add methods to extend the 'is' +;; macro. These define different kinds of tests, based on the first +;; symbol in the test expression. + +(defmulti assert-expr + (fn [msg form] + (cond + (nil? form) :always-fail + (seq? form) (first form) + :else :default))) + +(defmethod assert-expr :always-fail [msg form] + ;; nil test: always fail + `(clojure.test/do-report {:type :fail, :message ~msg})) + +(defmethod assert-expr :default [msg form] + (if (and (sequential? form) (function? (first form))) + (assert-predicate msg form) + (assert-any msg form))) + +(defmethod assert-expr 'instance? [msg form] + ;; Test if x is an instance of y. + `(let [klass# ~(nth form 1) + object# ~(nth form 2)] + (let [result# (instance? klass# object#)] + (if result# + (clojure.test/do-report {:type :pass, :message ~msg, + :expected '~form, :actual (class object#)}) + (clojure.test/do-report {:type :fail, :message ~msg, + :expected '~form, :actual (class object#)})) + result#))) + +(defmethod assert-expr 'thrown? [msg form] + ;; (is (thrown? c expr)) + ;; Asserts that evaluating expr throws an exception of class c. + ;; Returns the exception thrown. + (let [klass (second form) + body (nthnext form 2)] + `(try ~@body + (clojure.test/do-report {:type :fail, :message ~msg, + :expected '~form, :actual nil}) + (catch ~klass e# + (clojure.test/do-report {:type :pass, :message ~msg, + :expected '~form, :actual e#}) + e#)))) + +(defmethod assert-expr 'thrown-with-msg? [msg form] + ;; (is (thrown-with-msg? c re expr)) + ;; Asserts that evaluating expr throws an exception of class c. + ;; Also asserts that the message string of the exception matches + ;; (with re-find) the regular expression re. + (let [klass (nth form 1) + re (nth form 2) + body (nthnext form 3)] + `(try ~@body + (clojure.test/do-report {:type :fail, :message ~msg, :expected '~form, :actual nil}) + (catch ~klass e# + (let [m# (.getMessage e#)] + (if (re-find ~re m#) + (clojure.test/do-report {:type :pass, :message ~msg, + :expected '~form, :actual e#}) + (clojure.test/do-report {:type :fail, :message ~msg, + :expected '~form, :actual e#}))) + e#)))) + + +(defmacro try-expr + "Used by the 'is' macro to catch unexpected exceptions. + You don't call this." + {:added "1.1"} + [msg form] + `(try ~(assert-expr msg form) + (catch Throwable t# + (clojure.test/do-report {:type :error, :message ~msg, + :expected '~form, :actual t#})))) + + + +;;; ASSERTION MACROS + +;; You use these in your tests. + +(defmacro is + "Generic assertion macro. 'form' is any predicate test. + 'msg' is an optional message to attach to the assertion. + + Example: (is (= 4 (+ 2 2)) \"Two plus two should be 4\") + + Special forms: + + (is (thrown? c body)) checks that an instance of c is thrown from + body, fails if not; then returns the thing thrown. + + (is (thrown-with-msg? c re body)) checks that an instance of c is + thrown AND that the message on the exception matches (with + re-find) the regular expression re." + {:added "1.1"} + ([form] + `(clojure.test/is ~form nil)) + ([form msg] `(clojure.test/try-expr ~msg ~form))) + +(defmacro are + "Checks multiple assertions with a template expression. + See clojure.template/do-template for an explanation of + templates. + + Example: (are [x y] (= x y) + 2 (+ 1 1) + 4 (* 2 2)) + Expands to: + (do (is (= 2 (+ 1 1))) + (is (= 4 (* 2 2)))) + + Note: This breaks some reporting features, such as line numbers." + {:added "1.1"} + [argv expr & args] + (if (or + ;; (are [] true) is meaningless but ok + (and (empty? argv) (empty? args)) + ;; Catch wrong number of args + (and (pos? (count argv)) + (pos? (count args)) + (zero? (mod (count args) (count argv))))) + `(temp/do-template ~argv (clojure.test/is ~expr) ~@args) + (throw (IllegalArgumentException. "The number of args doesn't match are's argv.")))) + +(defmacro testing + "Adds a new string to the list of testing contexts. May be nested, + but must occur inside a test function (deftest)." + {:added "1.1"} + [string & body] + `(binding [clojure.test/*testing-contexts* (conj clojure.test/*testing-contexts* ~string)] + ~@body)) + + + +;;; DEFINING TESTS + +(defmacro with-test + "Takes any definition form (that returns a Var) as the first argument. + Remaining body goes in the :test metadata function for that Var. + + When *load-tests* is false, only evaluates the definition, ignoring + the tests." + {:added "1.1"} + [definition & body] + (if @load-tests + `(doto ~definition (alter-meta! assoc :test (fn [] ~@body))) + definition)) + + +(defmacro deftest + "Defines a test function with no arguments. Test functions may call + other tests, so tests may be composed. If you compose tests, you + should also define a function named test-ns-hook; run-tests will + call test-ns-hook instead of testing all vars. + + Note: Actually, the test body goes in the :test metadata on the var, + and the real function (the value of the var) calls test-var on + itself. + + When *load-tests* is false, deftest is ignored." + {:added "1.1"} + [name & body] + (when @load-tests + `(def ~(vary-meta name assoc :test `(fn [] ~@body)) + (fn [] (clojure.test/test-var (var ~name)))))) + +(defmacro deftest- + "Like deftest but creates a private var." + {:added "1.1"} + [name & body] + (when @load-tests + `(def ~(vary-meta name assoc :test `(fn [] ~@body) :private true) + (fn [] (test-var (var ~name)))))) + + +(defmacro set-test + "Experimental. + Sets :test metadata of the named var to a fn with the given body. + The var must already exist. Does not modify the value of the var. + + When *load-tests* is false, set-test is ignored." + {:added "1.1"} + [name & body] + (when @load-tests + `(alter-meta! (var ~name) assoc :test (fn [] ~@body)))) + + + +;;; DEFINING FIXTURES + +(def ^:private ns->fixtures (atom {})) + +(defn- add-ns-meta + "Adds elements in coll to the current namespace metadata as the + value of key." + {:added "1.1"} + [key coll] + (swap! ns->fixtures assoc-in [(sci-namespaces/sci-ns-name @vars/current-ns) key] coll)) + +(defmulti use-fixtures + "Wrap test runs in a fixture function to perform setup and + teardown. Using a fixture-type of :each wraps every test + individually, while :once wraps the whole run in a single function." + {:added "1.1"} + (fn [fixture-type & args] fixture-type)) + +(defmethod use-fixtures :each [fixture-type & args] + (add-ns-meta ::each-fixtures args)) + +(defmethod use-fixtures :once [fixture-type & args] + (add-ns-meta ::once-fixtures args)) + +(defn- default-fixture + "The default, empty, fixture function. Just calls its argument." + {:added "1.1"} + [f] + (f)) + +(defn compose-fixtures + "Composes two fixture functions, creating a new fixture function + that combines their behavior." + {:added "1.1"} + [f1 f2] + (fn [g] (f1 (fn [] (f2 g))))) + +(defn join-fixtures + "Composes a collection of fixtures, in order. Always returns a valid + fixture function, even if the collection is empty." + {:added "1.1"} + [fixtures] + (reduce compose-fixtures default-fixture fixtures)) + + + + +;;; RUNNING TESTS: LOW-LEVEL FUNCTIONS + +(defn test-var + "If v has a function in its :test metadata, calls that function, + with *testing-vars* bound to (conj *testing-vars* v)." + {:dynamic true, :added "1.1"} + [v] + (when-let [t (:test (meta v))] + (sci/binding [testing-vars (conj @testing-vars v)] + (do-report {:type :begin-test-var, :var v}) + (inc-report-counter :test) + (try (t) + (catch Throwable e + (do-report {:type :error, :message "Uncaught exception, not in assertion." + :expected nil, :actual e}))) + (do-report {:type :end-test-var, :var v})))) + +(defn test-vars + "Groups vars by their namespace and runs test-vars on them with + appropriate fixtures applied." + {:added "1.6"} + [vars] + (doseq [[ns vars] (group-by (comp :ns meta) vars) + :when ns] + (let [ns-name (sci-namespaces/sci-ns-name ns) + fixtures (get @ns->fixtures ns-name) + once-fixture-fn (join-fixtures (::once-fixtures fixtures)) + each-fixture-fn (join-fixtures (::each-fixtures fixtures))] + (once-fixture-fn + (fn [] + (doseq [v vars] + (when (:test (meta v)) + (each-fixture-fn (fn [] (test-var v)))))))))) + +(defn test-all-vars + "Calls test-vars on every var interned in the namespace, with fixtures." + {:added "1.1"} + [ctx ns] + (test-vars (vals (sci-namespaces/sci-ns-interns ctx ns)))) + +(defn test-ns + "If the namespace defines a function named test-ns-hook, calls that. + Otherwise, calls test-all-vars on the namespace. 'ns' is a + namespace object or a symbol. + + Internally binds *report-counters* to a ref initialized to + *initial-report-counters*. Returns the final, dereferenced state of + *report-counters*." + {:added "1.1"} + [ctx ns] + (sci/binding [report-counters (atom @initial-report-counters)] + (let [ns-obj (sci-namespaces/sci-the-ns ctx ns)] + (do-report {:type :begin-test-ns, :ns ns-obj}) + ;; If the namespace has a test-ns-hook function, call that: + (let [ns-sym (sci-namespaces/sci-ns-name ns-obj)] + (if-let [v (get-in @(:env ctx) [:namespaces ns-sym 'test-ns-hook])] + (@v) + ;; Otherwise, just test every var in the namespace. + (test-all-vars ctx ns-obj))) + (do-report {:type :end-test-ns, :ns ns-obj})) + @@report-counters)) + + + +;;; RUNNING TESTS: HIGH-LEVEL FUNCTIONS + +(defn run-tests + "Runs all tests in the given namespaces; prints results. + Defaults to current namespace if none given. Returns a map + summarizing test results." + {:added "1.1"} + ([ctx] (run-tests ctx @vars/current-ns)) + ([ctx & namespaces] + (let [summary (assoc (apply merge-with + (map #(test-ns ctx %) namespaces)) + :type :summary)] + (do-report summary) + summary))) + +(defn run-all-tests + "Runs all tests in all namespaces; prints results. + Optional argument is a regular expression; only namespaces with + names matching the regular expression (with re-matches) will be + tested." + {:added "1.1"} + ([ctx] (apply run-tests ctx (sci-namespaces/sci-all-ns ctx))) + ([ctx re] (apply run-tests ctx + (filter #(re-matches re (name (sci-namespaces/sci-ns-name %))) + (sci-namespaces/sci-all-ns ctx))))) + +(defn successful? + "Returns true if the given test summary indicates all tests + were successful, false otherwise." + {:added "1.1"} + [summary] + (and (zero? (:fail summary 0)) + (zero? (:error summary 0)))) diff --git a/src/babashka/impl/common.clj b/src/babashka/impl/common.clj new file mode 100644 index 00000000..938b89a6 --- /dev/null +++ b/src/babashka/impl/common.clj @@ -0,0 +1,4 @@ +(ns babashka.impl.common) + +;; placeholder for ctx +(def ctx (volatile! nil)) diff --git a/src/babashka/impl/repl.clj b/src/babashka/impl/repl.clj index 3460aaeb..12fb91a6 100644 --- a/src/babashka/impl/repl.clj +++ b/src/babashka/impl/repl.clj @@ -7,6 +7,7 @@ [clojure.tools.reader.reader-types :as r] [sci.impl.interpreter :refer [eval-form]] [sci.impl.parser :as parser] + [sci.impl.vars :as vars] [sci.core :as sci] [sci.impl.io :as sio])) @@ -29,7 +30,8 @@ "REPL.") (sio/println "Use :repl/quit or :repl/exit to quit the REPL.") (sio/println "Clojure rocks, Bash reaches.") - (sio/println))) + (sio/println) + (eval-form sci-ctx '(require '[clojure.repl :refer [dir doc]])))) :read (or read (fn [_request-prompt request-exit] ;; (prn "PEEK" @sci/in (r/peek-char @sci/in)) @@ -57,7 +59,7 @@ expr)] ret))) :need-prompt (or need-prompt (fn [] true)) - :prompt (or prompt #(sio/printf "%s=> " (-> sci-ctx :env deref :current-ns))) + :prompt (or prompt #(sio/printf "%s=> " (vars/current-ns-name))) :flush (or flush sio/flush) :print (or print sio/prn) :caught (or caught repl-caught))))) diff --git a/src/babashka/impl/test.clj b/src/babashka/impl/test.clj new file mode 100644 index 00000000..497963eb --- /dev/null +++ b/src/babashka/impl/test.clj @@ -0,0 +1,56 @@ +(ns babashka.impl.test + (:require [babashka.impl.clojure.test :as t])) + +(defn macrofy [v] + (with-meta v {:sci/macro true})) + +(defn contextualize [v] + (with-meta v {:sci.impl/op :needs-ctx})) + +(def clojure-test-namespace + {'*load-tests* t/load-tests + '*stack-trace-depth* t/stack-trace-depth + '*report-counters* t/report-counters + '*initial-report-counters* t/initial-report-counters + '*testing-vars* t/testing-vars + '*testing-contexts* t/testing-contexts + '*test-out* t/test-out + ;; 'with-test-out (macrofy @#'t/with-test-out) + ;; 'file-position t/file-position + 'testing-vars-str t/testing-vars-str + 'testing-contexts-str t/testing-contexts-str + 'inc-report-counter t/inc-report-counter + 'report t/report + 'do-report t/do-report + ;; assertion utilities + 'function? t/function? + 'assert-predicate t/assert-predicate + 'assert-any t/assert-any + ;; assertion methods + 'assert-expr t/assert-expr + 'try-expr (with-meta @#'t/try-expr + {:sci/macro true}) + ;; assertion macros + 'is (with-meta @#'t/is + {;; :sci.impl/op :needs-ctx + :sci/macro true}) + 'are (macrofy @#'t/are) + 'testing (macrofy @#'t/testing) + ;; defining tests + 'with-test (macrofy @#'t/with-test) + 'deftest (macrofy @#'t/deftest) + 'deftest- (macrofy @#'t/deftest-) + 'set-test (macrofy @#'t/set-test) + ;; fixtures + 'use-fixtures t/use-fixtures + 'compose-fixtures t/compose-fixtures + 'join-fixtures t/join-fixtures + ;; running tests: low level + 'test-var t/test-var + 'test-vars t/test-vars + 'test-all-vars (with-meta t/test-all-vars {:sci.impl/op :needs-ctx}) + 'test-ns (with-meta t/test-ns {:sci.impl/op :needs-ctx}) + ;; running tests: high level + 'run-tests (contextualize t/run-tests) + 'run-all-tests (contextualize t/run-all-tests) + 'successful? t/successful?}) diff --git a/src/babashka/main.clj b/src/babashka/main.clj index 553bd931..77834a02 100644 --- a/src/babashka/main.clj +++ b/src/babashka/main.clj @@ -1,7 +1,7 @@ (ns babashka.main {:no-doc true} (:require - [babashka.impl.async :refer [async-namespace]] + [babashka.impl.async :refer [async-namespace async-protocols-namespace]] [babashka.impl.cheshire :refer [cheshire-core-namespace]] [babashka.impl.classes :as classes] [babashka.impl.classpath :as cp] @@ -10,26 +10,31 @@ [babashka.impl.clojure.java.shell :refer [shell-namespace]] [babashka.impl.clojure.main :refer [demunge]] [babashka.impl.clojure.stacktrace :refer [stacktrace-namespace print-stack-trace]] + [babashka.impl.common :as common] [babashka.impl.csv :as csv] [babashka.impl.pipe-signal-handler :refer [handle-pipe! pipe-signal-received?]] [babashka.impl.repl :as repl] [babashka.impl.socket-repl :as socket-repl] + [babashka.impl.test :as t] [babashka.impl.tools.cli :refer [tools-cli-namespace]] [babashka.impl.xml :as xml] [babashka.wait :as wait] [clojure.edn :as edn] [clojure.java.io :as io] [clojure.string :as str] + [fipp.edn :as fipp] [sci.addons :as addons] [sci.core :as sci] [sci.impl.interpreter :refer [eval-string*]] [sci.impl.opts :as sci-opts] + [sci.impl.unrestrict :refer [*unrestricted*]] [sci.impl.vars :as vars]) (:gen-class)) -(sci/alter-var-root sci/in (constantly *in*)) -(sci/alter-var-root sci/out (constantly *out*)) -(sci/alter-var-root sci/err (constantly *err*)) +(binding [*unrestricted* true] + (sci/alter-var-root sci/in (constantly *in*)) + (sci/alter-var-root sci/out (constantly *out*)) + (sci/alter-var-root sci/err (constantly *err*))) (set! *warn-on-reflection* true) ;; To detect problems when generating the image, run: @@ -42,6 +47,7 @@ (if options (let [opt (first options)] (case opt + ("--") (assoc opts-map :command-line-args (next options)) ("--version") {:version true} ("--help" "-h" "-?") {:help? true} ("--verbose")(recur (next options) @@ -69,6 +75,14 @@ (assoc opts-map :shell-in true :shell-out true)) + ("-iO") (recur (next options) + (assoc opts-map + :shell-in true + :edn-out true)) + ("-Io") (recur (next options) + (assoc opts-map + :edn-in true + :shell-out true)) ("-IO") (recur (next options) (assoc opts-map :edn-in true @@ -100,22 +114,24 @@ ("--eval", "-e") (let [options (next options)] (recur (next options) - (assoc opts-map :expression (first options)))) + (update opts-map :expressions (fnil conj []) (first options)))) ("--main", "-m") (let [options (next options)] (recur (next options) (assoc opts-map :main (first options)))) - (if (some opts-map [:file :socket-repl :expression :main]) + (if (some opts-map [:file :socket-repl :expressions :main]) (assoc opts-map :command-line-args options) - (if (and (not= \( (first (str/trim opt))) - (.exists (io/file opt))) - (assoc opts-map - :file opt - :command-line-args (next options)) - (assoc opts-map - :expression opt - :command-line-args (next options)))))) + (let [trimmed-opt (str/triml opt) + c (.charAt trimmed-opt 0)] + (case c + (\( \{ \[ \* \@ \#) + (-> opts-map + (update :expressions (fnil conj []) (first options)) + (assoc :command-line-args (next options))) + (assoc opts-map + :file opt + :command-line-args (next options))))))) opts-map))] opts)) @@ -166,9 +182,10 @@ -f, --file Evaluate a file. -cp, --classpath Classpath to use. -m, --main Call the -main function from namespace with args. - --repl Start REPL + --repl Start REPL. Use rlwrap for history. --socket-repl Start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666). --time Print execution time before exiting. + -- Stop parsing args and pass everything after -- to *command-line-args* If neither -e, -f, or --socket-repl are specified, then the first argument that is not parsed as a option is treated as a file if it exists, or as an expression otherwise. Everything after that is bound to *command-line-args*.")) @@ -190,7 +207,7 @@ Everything after that is bound to *command-line-args*.")) (defn load-file* [sci-ctx f] (let [f (io/file f) s (slurp f)] - (sci/with-bindings {vars/file-var (.getCanonicalPath f)} + (sci/with-bindings {vars/current-file (.getCanonicalPath f)} (eval-string* sci-ctx s)))) (defn eval* [sci-ctx form] @@ -204,15 +221,11 @@ Everything after that is bound to *command-line-args*.")) (defn exit [n] (throw (ex-info "" {:bb/exit-code n}))) -;; (sci/set-var-root! sci/*in* *in*) -;; (sci/set-var-root! sci/*out* *out*) -;; (sci/set-var-root! sci/*err* *err*) - (def aliases - '{tools.cli 'clojure.tools.cli + '{tools.cli clojure.tools.cli edn clojure.edn wait babashka.wait - sig babashka.signal + signal babashka.signal shell clojure.java.shell io clojure.java.io async clojure.core.async @@ -220,21 +233,35 @@ Everything after that is bound to *command-line-args*.")) json cheshire.core xml clojure.data.xml}) +(def cp-state (atom nil)) + +(defn add-classpath* [add-to-cp] + (swap! cp-state + (fn [{:keys [:cp]}] + (let [new-cp + (if-not cp add-to-cp + (str cp (System/getProperty "path.separator") add-to-cp))] + {:loader (cp/loader new-cp) + :cp new-cp}))) + nil) + (def namespaces {'clojure.tools.cli tools-cli-namespace - 'clojure.edn {'read edn/read - 'read-string edn/read-string} 'clojure.java.shell shell-namespace 'babashka.wait {'wait-for-port wait/wait-for-port 'wait-for-path wait/wait-for-path} 'babashka.signal {'pipe-signal-received? pipe-signal-received?} 'clojure.java.io io-namespace 'clojure.core.async async-namespace + 'clojure.core.async.impl.protocols async-protocols-namespace 'clojure.data.csv csv/csv-namespace 'cheshire.core cheshire-core-namespace 'clojure.stacktrace stacktrace-namespace 'clojure.main {'demunge demunge} 'clojure.repl {'demunge demunge} + 'clojure.test t/clojure-test-namespace + 'babashka.classpath {'add-classpath add-classpath*} + 'clojure.pprint {'pprint fipp/pprint} 'clojure.data.xml xml/xml-namespace}) (def bindings @@ -248,7 +275,9 @@ Everything after that is bound to *command-line-args*.")) (if exit-code [nil exit-code] (do (if verbose? (print-stack-trace e) - (println (.getMessage e))) + (println (str (.. e getClass getName) + (when-let [m (.getMessage e)] + (str ": " m)) ))) (flush) [nil 1]))))) @@ -257,156 +286,170 @@ Everything after that is bound to *command-line-args*.")) (handle-pipe!) #_(binding [*out* *err*] (prn "M" (meta (get bindings 'future)))) - (let [t0 (System/currentTimeMillis) - {:keys [:version :shell-in :edn-in :shell-out :edn-out - :help? :file :command-line-args - :expression :stream? :time? - :repl :socket-repl - :verbose? :classpath - :main :uberscript] :as _opts} - (parse-opts args) - read-next (fn [*in*] - (if (pipe-signal-received?) - ::EOF - (if stream? - (if shell-in (or (read-line) ::EOF) - (read-edn)) - (delay (cond shell-in - (shell-seq *in*) - edn-in - (edn-seq *in*) - :else - (edn/read *in*)))))) - uberscript-sources (atom ()) - env (atom {}) - classpath (or classpath - (System/getenv "BABASHKA_CLASSPATH")) - loader (when classpath - (cp/loader classpath)) - load-fn (when classpath - (fn [{:keys [:namespace]}] - (let [res (cp/source-for-namespace loader namespace nil)] - (when uberscript (swap! uberscript-sources conj (:source res))) - res))) - _ (when file (vars/bindRoot vars/file-var (.getCanonicalPath (io/file file)))) - ctx {:aliases aliases - :namespaces (-> namespaces - (assoc 'clojure.core - (assoc core-extras - '*command-line-args* - (sci/new-dynamic-var '*command-line-args* command-line-args) - '*file* vars/file-var - '*warn-on-reflection* reflection-var)) - (assoc-in ['clojure.java.io 'resource] - #(when classpath (cp/getResource loader % {:url? true})))) - :bindings bindings - :env env - :features #{:bb} - :classes classes/class-map - :imports '{ArithmeticException java.lang.ArithmeticException - AssertionError java.lang.AssertionError - Boolean java.lang.Boolean - Class java.lang.Class - Double java.lang.Double - Exception java.lang.Exception - IllegalArgumentException java.lang.IllegalArgumentException - Integer java.lang.Integer - File java.io.File - Math java.lang.Math - Object java.lang.Object - ProcessBuilder java.lang.ProcessBuilder - String java.lang.String - System java.lang.System - Thread java.lang.Thread} - :load-fn load-fn - :dry-run uberscript} - ctx (addons/future ctx) - sci-ctx (sci-opts/init ctx) - _ (swap! (:env sci-ctx) - (fn [env] - (update-in env [:namespaces 'clojure.core] assoc - 'eval #(eval* sci-ctx %) - 'load-file #(load-file* sci-ctx %)))) - _ (swap! (:env sci-ctx) - (fn [env] - (assoc-in env [:namespaces 'clojure.main 'repl] - (fn [& opts] - (let [opts (apply hash-map opts)] - (repl/start-repl! sci-ctx opts)))))) - preloads (some-> (System/getenv "BABASHKA_PRELOADS") (str/trim)) - [expression exit-code] - (cond expression [expression nil] - main [(format "(ns user (:require [%1$s])) (apply %1$s/-main *command-line-args*)" - main) nil] - file (try [(read-file file) nil] - (catch Exception e - (error-handler* e verbose?)))) - exit-code - ;; handle preloads - (if exit-code exit-code - (do (when preloads (try (eval-string* sci-ctx preloads) - (catch Throwable e - (error-handler* e verbose?)))) - nil)) - exit-code - (or exit-code - (sci/with-bindings {reflection-var false} - (or - #_(binding [*out* *err*] - (prn ">>" _opts)) - (second - (cond version - [(print-version) 0] - help? - [(print-help) 0] - repl [(repl/start-repl! sci-ctx) 0] - socket-repl [(start-socket-repl! socket-repl sci-ctx) 0] - (not (str/blank? expression)) + (binding [*unrestricted* true] + (sci/binding [reflection-var false + vars/current-ns (vars/->SciNamespace 'user nil)] + (let [t0 (System/currentTimeMillis) + {:keys [:version :shell-in :edn-in :shell-out :edn-out + :help? :file :command-line-args + :expressions :stream? :time? + :repl :socket-repl + :verbose? :classpath + :main :uberscript] :as _opts} + (parse-opts args) + read-next (fn [*in*] + (if (pipe-signal-received?) + ::EOF + (if stream? + (if shell-in (or (read-line) ::EOF) + (read-edn)) + (delay (cond shell-in + (shell-seq *in*) + edn-in + (edn-seq *in*) + :else + (edn/read *in*)))))) + uberscript-sources (atom ()) + env (atom {}) + classpath (or classpath + (System/getenv "BABASHKA_CLASSPATH")) + _ (when classpath + (add-classpath* classpath)) + load-fn (fn [{:keys [:namespace]}] + (when-let [{:keys [:loader]} @cp-state] + (let [res (cp/source-for-namespace loader namespace nil)] + (when uberscript (swap! uberscript-sources conj (:source res))) + res))) + _ (when file (vars/bindRoot vars/current-file (.getCanonicalPath (io/file file)))) + ctx {:aliases aliases + :namespaces (-> namespaces + (assoc 'clojure.core + (assoc core-extras + '*command-line-args* + (sci/new-dynamic-var '*command-line-args* command-line-args) + '*warn-on-reflection* reflection-var)) + (assoc-in ['clojure.java.io 'resource] + #(when-let [{:keys [:loader]} @cp-state] (cp/getResource loader % {:url? true})))) + :bindings bindings + :env env + :features #{:bb :clj} + :classes classes/class-map + :imports '{ArithmeticException java.lang.ArithmeticException + AssertionError java.lang.AssertionError + Boolean java.lang.Boolean + Class java.lang.Class + Double java.lang.Double + Exception java.lang.Exception + IllegalArgumentException java.lang.IllegalArgumentException + Integer java.lang.Integer + File java.io.File + Long java.lang.Long + Math java.lang.Math + Object java.lang.Object + ProcessBuilder java.lang.ProcessBuilder + String java.lang.String + StringBuilder java.lang.StringBuilder + System java.lang.System + Thread java.lang.Thread + Throwable java.lang.Throwable} + :load-fn load-fn + :dry-run uberscript} + ctx (addons/future ctx) + sci-ctx (sci-opts/init ctx) + _ (vreset! common/ctx sci-ctx) + _ (swap! (:env sci-ctx) + (fn [env] + (update-in env [:namespaces 'clojure.core] assoc + 'eval #(eval* sci-ctx %) + 'load-file #(load-file* sci-ctx %)))) + _ (swap! (:env sci-ctx) + (fn [env] + (assoc-in env [:namespaces 'clojure.main 'repl] + (fn [& opts] + (let [opts (apply hash-map opts)] + (repl/start-repl! sci-ctx opts)))))) + preloads (some-> (System/getenv "BABASHKA_PRELOADS") (str/trim)) + [expressions exit-code] + (cond expressions [expressions nil] + main [[(format "(ns user (:require [%1$s])) (apply %1$s/-main *command-line-args*)" + main)] nil] + file (try [[(read-file file)] nil] + (catch Exception e + (error-handler* e verbose?)))) + expression (str/join " " expressions) ;; this might mess with the locations... + exit-code + ;; handle preloads + (if exit-code exit-code + (do (when preloads (try - (loop [in (read-next *in*)] - (let [_ (swap! env update-in [:namespaces 'user] - assoc (with-meta '*input* - (when-not stream? - {:sci.impl/deref! true})) - (sci/new-dynamic-var '*input* in))] - (if (identical? ::EOF in) - [nil 0] ;; done streaming - (let [res [(let [res (eval-string* sci-ctx expression)] - (when (some? res) - (if-let [pr-f (cond shell-out println - edn-out prn)] - (if (coll? res) - (doseq [l res - :while (not (pipe-signal-received?))] - (pr-f l)) - (pr-f res)) - (prn res)))) 0]] - (if stream? - (recur (read-next *in*)) - res))))) + (eval-string* sci-ctx preloads) (catch Throwable e - (error-handler* e verbose?))) - uberscript [nil 0] - :else [(repl/start-repl! sci-ctx) 0])) - 1))) - t1 (System/currentTimeMillis)] - (flush) - (when uberscript - uberscript - (let [uberscript-out uberscript] - (spit uberscript-out "") ;; reset file - (doseq [s @uberscript-sources] - (spit uberscript-out s :append true)) - (spit uberscript-out preloads :append true) - (spit uberscript-out expression :append true))) - (when time? (binding [*out* *err*] - (println "bb took" (str (- t1 t0) "ms.")))) - exit-code)) + (error-handler* e verbose?)))) + nil)) + exit-code + (or exit-code + (second + (cond version + [(print-version) 0] + help? + [(print-help) 0] + repl [(repl/start-repl! sci-ctx) 0] + socket-repl [(start-socket-repl! socket-repl sci-ctx) 0] + (not (str/blank? expression)) + (try + (loop [in (read-next *in*)] + (let [_ (swap! env update-in [:namespaces 'user] + assoc (with-meta '*input* + (when-not stream? + {:sci.impl/deref! true})) + (sci/new-dynamic-var '*input* in))] + (if (identical? ::EOF in) + [nil 0] ;; done streaming + (let [res [(let [res (eval-string* sci-ctx expression)] + (when (some? res) + (if-let [pr-f (cond shell-out println + edn-out prn)] + (if (coll? res) + (doseq [l res + :while (not (pipe-signal-received?))] + (pr-f l)) + (pr-f res)) + (prn res)))) 0]] + (if stream? + (recur (read-next *in*)) + res))))) + (catch Throwable e + (error-handler* e verbose?))) + uberscript [nil 0] + :else [(repl/start-repl! sci-ctx) 0])) + 1) + t1 (System/currentTimeMillis)] + (flush) + (when uberscript + uberscript + (let [uberscript-out uberscript] + (spit uberscript-out "") ;; reset file + (doseq [s @uberscript-sources] + (spit uberscript-out s :append true)) + (spit uberscript-out preloads :append true) + (spit uberscript-out expression :append true))) + (when time? (binding [*out* *err*] + (println "bb took" (str (- t1 t0) "ms.")))) + exit-code)))) (defn -main [& args] - (let [exit-code (apply main args)] - (System/exit exit-code))) + (if-let [dev-opts (System/getenv "BABASHKA_DEV")] + (let [{:keys [:n]} (edn/read-string dev-opts) + last-iteration (dec n)] + (dotimes [i n] + (if (< i last-iteration) + (with-out-str (apply main args)) + (do (apply main args) + (binding [*out* *err*] + (println "ran" n "times")))))) + (let [exit-code (apply main args)] + (System/exit exit-code)))) ;;;; Scratch diff --git a/test-resources/babashka/assert_expr.clj b/test-resources/babashka/assert_expr.clj new file mode 100644 index 00000000..6983e91e --- /dev/null +++ b/test-resources/babashka/assert_expr.clj @@ -0,0 +1,21 @@ +(require '[clojure.test :refer [is deftest] :as t]) + +(defmethod t/assert-expr 'roughly [msg form] + `(let [op1# ~(nth form 1) + op2# ~(nth form 2) + tolerance# (if (= 4 ~(count form)) ~(last form) 2) + decimals# (/ 1. (Math/pow 10 tolerance#)) + result# (< (Math/abs (- op1# op2#)) decimals#)] + (t/do-report + {:type (if result# :pass :fail) + :message ~msg + :expected (format "%s should be roughly %s with %s tolerance" + op1# op2# decimals#) + :actual result#}) + result#)) + +(deftest PI-test + (is (roughly 3.14 Math/PI 2)) + (is (roughly 3.14 Math/PI 3))) + +(t/test-var #'PI-test) diff --git a/test/babashka/classpath_test.clj b/test/babashka/classpath_test.clj index dd20b197..733cf2e0 100644 --- a/test/babashka/classpath_test.clj +++ b/test/babashka/classpath_test.clj @@ -2,8 +2,8 @@ (:require [babashka.test-utils :as tu] [clojure.edn :as edn] - [clojure.test :as t :refer [deftest is]] - [clojure.java.io :as io])) + [clojure.java.io :as io] + [clojure.test :as t :refer [deftest is]])) (defn bb [input & args] (edn/read-string (apply tu/bb (when (some? input) (str input)) (map str args)))) @@ -14,7 +14,10 @@ "(require '[my-script :as ms]) (ms/foo)"))) (is (= "hello from foo\n" (tu/bb nil "--classpath" "test-resources/babashka/src_for_classpath_test/foo.jar" - "(require '[foo :as f]) (f/foo)")))) + "(require '[foo :as f]) (f/foo)"))) + (is (thrown-with-msg? Exception #"not require" + (tu/bb nil + "(require '[foo :as f])")))) (deftest classpath-env-test ;; for this test you have to set `BABASHKA_CLASSPATH` to test-resources/babashka/src_for_classpath_test/env @@ -30,7 +33,7 @@ (deftest uberscript-test (let [tmp-file (java.io.File/createTempFile "uberscript" ".clj")] (.deleteOnExit tmp-file) - (tu/bb nil "--classpath" "test-resources/babashka/src_for_classpath_test" "-m" "my.main" "--uberscript" (.getPath tmp-file)) + (is (empty? (tu/bb nil "--classpath" "test-resources/babashka/src_for_classpath_test" "-m" "my.main" "--uberscript" (.getPath tmp-file)))) (is (= "(\"1\" \"2\" \"3\" \"4\")\n" (tu/bb nil "--file" (.getPath tmp-file) "1" "2" "3" "4"))))) diff --git a/test/babashka/file_var_test.clj b/test/babashka/file_var_test.clj index 909373bb..0ab76cb9 100644 --- a/test/babashka/file_var_test.clj +++ b/test/babashka/file_var_test.clj @@ -8,10 +8,11 @@ (apply tu/bb (when (some? input) (str input)) (map str args))) (deftest file-var-test - (let [[f1 f2 f3] + (let [[f1 f2 f3 f4] (str/split (bb nil "--classpath" "test/babashka/scripts" "test/babashka/scripts/file_var.bb") #"\n")] (is (str/ends-with? f1 "file_var_classpath.bb")) (is (str/ends-with? f2 "loaded_by_file_var.bb")) - (is (str/ends-with? f3 "file_var.bb")))) + (is (str/ends-with? f3 "file_var.bb")) + (is (str/ends-with? f4 "file_var.bb")))) diff --git a/test/babashka/impl/repl_test.clj b/test/babashka/impl/repl_test.clj index d0f1bc5b..29a932c8 100644 --- a/test/babashka/impl/repl_test.clj +++ b/test/babashka/impl/repl_test.clj @@ -14,9 +14,10 @@ (vars/bindRoot sci/err *err*) (defn repl! [] - (start-repl! (init {:bindings {'*command-line-args* - ["a" "b" "c"]} - :env (atom {})}))) + (sci/with-bindings {vars/current-ns (vars/->SciNamespace 'user nil)} + (start-repl! (init {:bindings {'*command-line-args* + ["a" "b" "c"]} + :env (atom {})})))) (defn assert-repl [expr expected] (is (str/includes? (sci/with-out-str diff --git a/test/babashka/impl/socket_repl_test.clj b/test/babashka/impl/socket_repl_test.clj index 83038f0e..8bad7823 100644 --- a/test/babashka/impl/socket_repl_test.clj +++ b/test/babashka/impl/socket_repl_test.clj @@ -55,6 +55,8 @@ (is (socket-command "#?(:bb 1337 :clj 8888)" "1337"))) (testing "*1, *2, *3, *e" (is (socket-command "1\n*1" "1"))) + (testing "*ns*" + (is (socket-command "(ns foo.bar) (ns-name *ns*)" "foo.bar"))) (finally (if tu/jvm? (stop-repl!) diff --git a/test/babashka/java_security_test.clj b/test/babashka/java_security_test.clj new file mode 100644 index 00000000..d619a4b2 --- /dev/null +++ b/test/babashka/java_security_test.clj @@ -0,0 +1,20 @@ +(ns babashka.java-security-test + (:require + [babashka.test-utils :as test-utils] + [clojure.edn :as edn] + [clojure.test :as test :refer [deftest is]])) + +(defn bb [expr] + (edn/read-string (apply test-utils/bb nil [(str expr)]))) + +(defn signature [algo] + (clojure.walk/postwalk-replace {::algo algo} + '(defn signature [^String s] + (let [algorithm (java.security.MessageDigest/getInstance ::algo) + digest (.digest algorithm (.getBytes s))] + (format "%032x" (java.math.BigInteger. 1 digest)))))) + +(deftest java-security-test + (is (= "49f68a5c8493ec2c0bf489821c21fc3b" (bb (list 'do (signature "MD5") '(signature "hi"))))) + (is (= "c22b5f9178342609428d6f51b2c5af4c0bde6a42" (bb (list 'do (signature "SHA-1") '(signature "hi"))))) + (is (= "8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4" (bb (list 'do (signature "SHA-256") '(signature "hi")))))) diff --git a/test/babashka/main_test.clj b/test/babashka/main_test.clj index bcd4a984..6d5c3587 100644 --- a/test/babashka/main_test.clj +++ b/test/babashka/main_test.clj @@ -13,24 +13,19 @@ (edn/read-string (apply test-utils/bb (when (some? input) (str input)) (map str args)))) (deftest parse-opts-test - (is (= {:expression "(println 123)"} - (main/parse-opts ["-e" "(println 123)"]))) - - (is (= {:expression "(println 123)"} - (main/parse-opts ["--eval" "(println 123)"]))) - + (is (= 123 (bb nil "(println 123)"))) + (is (= 123 (bb nil "-e" "(println 123)"))) + (is (= 123 (bb nil "--eval" "(println 123)"))) (testing "distinguish automatically between expression or file name" - (is (= {:expression "(println 123)" - :command-line-args nil} - (main/parse-opts ["(println 123)"]))) + (is (= {:result 8080} (bb nil "test/babashka/scripts/tools.cli.bb"))) + (is (thrown-with-msg? Exception #"does not exist" (bb nil "foo.clj"))) + (is (thrown-with-msg? Exception #"does not exist" (bb nil "-help")))) + (is (= "1 2 3" (bb nil "-e" "(require '[clojure.string :as str1])" "-e" "(str1/join \" \" [1 2 3])"))) + (is (= '("-e" "1") (bb nil "-e" "*command-line-args*" "--" "-e" "1")))) - (is (= {:file "src/babashka/main.clj" - :command-line-args nil} - (main/parse-opts ["src/babashka/main.clj"]))) - - (is (= {:expression "does-not-exist" - :command-line-args nil} - (main/parse-opts ["does-not-exist"]))))) +(deftest print-error-test + (is (thrown-with-msg? Exception #"java.lang.NullPointerException" + (bb nil "(subs nil 0 0)")))) (deftest main-test (testing "-io behaves as identity" @@ -165,6 +160,12 @@ (deftest future-test (is (= 6 (bb nil "@(future (+ 1 2 3))")))) + +(deftest promise-test + (is (= :timeout (bb nil "(deref (promise) 1 :timeout)"))) + (is (= :ok (bb nil "(let [x (promise)] + (deliver x :ok) + @x)")))) (deftest process-builder-test (is (str/includes? (bb nil " @@ -235,8 +236,9 @@ (is (zero? (bb nil "(try (/ 1 0) (catch ArithmeticException _ 0))")))) (deftest reader-conditionals-test - (is (= :hello (bb nil "#?(:clj (in-ns 'foo)) (println :hello)"))) - (is (= :hello (bb nil "#?(:bb :hello :default :bye)")))) + (is (= :hello (bb nil "#?(:bb :hello :default :bye)"))) + (is (= :hello (bb nil "#?(:clj :hello :bb :bye)"))) + (is (= [1 2] (bb nil "[1 2 #?@(:bb [] :clj [1])]")))) (deftest csv-test (is (= '(["Adult" "87727"] ["Elderly" "43914"] ["Child" "33411"] ["Adolescent" "29849"] @@ -250,8 +252,7 @@ (deftest Pattern-test (is (= ["1" "2" "3"] (bb nil "(vec (.split (java.util.regex.Pattern/compile \"f\") \"1f2f3\"))"))) - (is (= java.util.regex.Pattern/CANON_EQ - (bb nil "java.util.regex.Pattern/CANON_EQ")))) + (is (true? (bb nil "(some? java.util.regex.Pattern/CANON_EQ)")))) (deftest writer-test (let [tmp-file (java.io.File/createTempFile "bbb" "bbb") @@ -340,6 +341,20 @@ (is (= "12" (bb nil "(let [xml (xml/parse-str \"12\")] (xml/emit-str xml))")))) +(deftest uberscript-test + (let [tmp-file (java.io.File/createTempFile "uberscript" ".clj")] + (.deleteOnExit tmp-file) + (is (empty? (bb nil "--uberscript" (.getPath tmp-file) "-e" "(System/exit 1)"))) + (is (= "(System/exit 1)" (slurp tmp-file))))) + +(deftest unrestricted-access + (testing "babashka is allowed to mess with built-in vars" + (is (= 1 (bb nil " +(def inc2 inc) (alter-var-root #'clojure.core/inc (constantly dec)) +(let [res (inc 2)] + (alter-var-root #'clojure.core/inc (constantly inc2)) + res)"))))) + ;;;; Scratch (comment diff --git a/test/babashka/profile.clj b/test/babashka/profile.clj new file mode 100644 index 00000000..2bb90052 --- /dev/null +++ b/test/babashka/profile.clj @@ -0,0 +1,12 @@ +(ns babashka.profile + (:require [babashka.main :as main])) + +(comment) + +;; clojure -A:profile -e "(prn (loop [val 0 cnt 1000000] (if (pos? cnt) (recur (inc val) (dec cnt)) val)))" + +(require '[clj-async-profiler.core :as prof]) + +(defn -main [& options] + (prof/profile (apply main/main options)) + (shutdown-agents)) diff --git a/test/babashka/scripts/file_var.bb b/test/babashka/scripts/file_var.bb index a35bf126..d92a8976 100644 --- a/test/babashka/scripts/file_var.bb +++ b/test/babashka/scripts/file_var.bb @@ -4,3 +4,5 @@ (require '[file-var-classpath]) (load-file (io/file "test" "babashka" "scripts" "loaded_by_file_var.bb")) (println *file*) +(defn foo []) +(println (:file (meta #'foo))) diff --git a/test/babashka/test_test.clj b/test/babashka/test_test.clj new file mode 100644 index 00000000..eb6bc975 --- /dev/null +++ b/test/babashka/test_test.clj @@ -0,0 +1,71 @@ +(ns babashka.test-test + (:require + [babashka.test-utils :as tu] + [clojure.string :as str] + [clojure.test :as t :refer [deftest is]] + [clojure.java.io :as io])) + +(defn bb [& args] + (apply tu/bb nil (map str args))) + +(deftest deftest-test + (is (str/includes? + (bb "(require '[clojure.test :as t]) (t/deftest foo (t/is (= 4 5))) (foo)") + "expected: (= 4 5)\n actual: (not (= 4 5))\n"))) + +(deftest run-tests-test + (let [output (bb "(require '[clojure.test :as t]) (t/deftest foo (t/is (= 4 5))) (t/run-tests)")] + (is (str/includes? output "Testing user")) + (is (str/includes? output "{:test 1, :pass 0, :fail 1, :error 0, :type :summary}")))) + +(deftest run-all-tests-test + (let [output (bb " +(require '[clojure.test :as t]) +(t/deftest foo (t/is (= 4 5))) +(ns foobar) +(require '[clojure.test :as t]) +(t/run-all-tests)")] + (is (str/includes? output "Testing user")) + (is (str/includes? output "Testing foobar")) + (is (str/includes? output "{:test 1, :pass 0, :fail 1, :error 0, :type :summary}")))) + +(deftest fixtures-test + (let [output (bb " +(require '[clojure.test :as t]) +(defn once [f] (prn :once-before) (f) (prn :once-after)) +(defn each [f] (prn :each-before) (f) (prn :each-after)) +(t/use-fixtures :once once) +(t/use-fixtures :each each) +(t/deftest foo) +(t/deftest bar) +(t/run-tests)")] + (is (str/includes? output (str/trim " +:once-before +:each-before +:each-after +:each-before +:each-after +:once-after"))))) + +(deftest with-test + (let [output (bb " +(require '[clojure.test :as t]) +(t/with-test + (defn my-function [x y] + (+ x y)) + (t/is (= 4 (my-function 2 2))) + (t/is (= 7 (my-function 3 4)))) +(t/run-tests)")] + (is (str/includes? output "Ran 1 tests containing 2 assertions.")))) + +(deftest testing-test + (is (str/includes? (bb "(require '[clojure.test :as t]) (t/testing \"foo\" (t/is (= 4 5)))") + "foo"))) + +(deftest are-test + (is (str/includes? (bb "(require '[clojure.test :as t]) (t/are [x y] (= x y) 2 (+ 1 2))") + "expected: (= 2 (+ 1 2))"))) + +(deftest assert-expr-test + (is (str/includes? (bb (.getPath (io/file "test-resources" "babashka" "assert_expr.clj"))) + "3.14 should be roughly 3.141592653589793"))) diff --git a/test/babashka/test_utils.clj b/test/babashka/test_utils.clj index 071e263c..b5bfd387 100644 --- a/test/babashka/test_utils.clj +++ b/test/babashka/test_utils.clj @@ -8,6 +8,7 @@ (set! *warn-on-reflection* true) (defn bb-jvm [input & args] + (reset! main/cp-state nil) (let [os (java.io.StringWriter.) es (java.io.StringWriter.) is (when input