Add an async and async testing framework.

This is a well-worn idea: use a `promise-channel` of `[result nil]` or
`[nil error]` pairs.  The `go-pair` and `<?` macros handle catching
exceptions (important, given that synchronous CLJ code expects to throw
rather than return an error promise or similar), allowing code like:
```
(go-pair
  (let [result (<? (pair-chan-fn))]
    (when (not result)
      (throw (Exception. "No result!")))
    (transform result)))
```
to be expressed naturally.  These are the equivalents of `async` and
`await` in JS.

The implementation is complicated by significant incompatibilities
between CLJ and CLJS.  The solution presented here takes care to
separate the macro definitions into CLJ.  Sadly, this requires
namespacing the per-environment symbols explicitly; but we hope to
minimize such code in files like this.

The most significant restriction to this approach is that consumers must
require the transitive dependencies of the macro-defining modules.  See
the included tests (both CLJ and CLJS) for the appropriate
incantations (for pair-chan, core.async, and test).
This commit is contained in:
Nick Alexander 2016-07-11 16:48:40 -07:00
parent ca62b7b5d2
commit 0a312b4f40
7 changed files with 134 additions and 12 deletions

View file

@ -4,7 +4,9 @@
:license {:name "Mozilla Public License Version 2.0" :license {:name "Mozilla Public License Version 2.0"
:url "https://github.com/mozilla/datomish/blob/master/LICENSE"} :url "https://github.com/mozilla/datomish/blob/master/LICENSE"}
:dependencies [[org.clojure/clojurescript "1.9.89"] :dependencies [[org.clojure/clojurescript "1.9.89"]
[org.clojure/clojure "1.8.0"]] [org.clojure/clojure "1.8.0"]
[org.clojure/core.async "0.2.385"]
[jamesmacaulay/cljs-promises "0.1.0"]]
:cljsbuild {:builds {:release { :cljsbuild {:builds {:release {
:source-paths ["src"] :source-paths ["src"]

View file

@ -0,0 +1,57 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
(ns datomish.pair-chan)
;; From https://github.com/plumatic/schema/blob/bf469889b730feb09448fd085be5828f28425b41/src/clj/schema/macros.clj#L10-L19.
(defn cljs-env?
"Take the &env from a macro, and tell whether we are expanding into cljs."
[env]
(boolean (:ns env)))
(defmacro if-cljs
"Return then if we are generating cljs code and else for Clojure code.
https://groups.google.com/d/msg/clojurescript/iBY5HaQda4A/w1lAQi9_AwsJ"
[then else]
(if (cljs-env? &env) then else))
;; It's a huge pain to declare cross-environment macros. This is awful, but making the namespace a
;; parameter appears to be *even worse*. Note also that `go` is not in a consistent namespace...
(defmacro go-pair [& body]
"Evaluate `body` forms in a `go` block. Catch errors and return a
pair chan (a promise channel resolving to `[result error]`)."
`(if-cljs
(let [pc-chan# (cljs.core.async/promise-chan)]
(cljs.core.async.macros/go
(try
(cljs.core.async/>! pc-chan# [(do ~@body) nil])
(catch js/Error ex#
(cljs.core.async/>! pc-chan# [nil ex#]))))
pc-chan#)
(let [pc-chan# (clojure.core.async/promise-chan)]
(clojure.core.async/go
(try
(clojure.core.async/>! pc-chan# [(do ~@body) nil])
(catch Exception ex#
(clojure.core.async/>! pc-chan# [nil ex#]))))
pc-chan#)))
;; Thanks to David Nolen for the name of this macro! http://swannodette.github.io/2013/08/31/asynchronous-error-handling/.
;; This version works a bit differently, though. This must be a macro, so that the enclosed <!
;; symbols are processed by any enclosing go blocks.
(defmacro <?
"Expects `pc-chan` to be a channel or ReadPort which produces [value nil] or
[nil error] pairs, and returns values and throws errors as per `consume-pair`."
[pc-chan]
`(if-cljs
(consume-pair (cljs.core.async/<! ~pc-chan))
(consume-pair (clojure.core.async/<! ~pc-chan))))
(defn consume-pair
"When passed a [value nil] pair, returns value. When passed a [nil error] pair,
throws error. See also `<?`."
[[val err]]
(if err
(throw err)
val))

View file

@ -0,0 +1,37 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
(ns datomish.test-macros
(:require
[datomish.pair-chan]))
;; From https://github.com/plumatic/schema/blob/bf469889b730feb09448fd085be5828f28425b41/src/clj/schema/macros.clj#L10-L19.
(defn cljs-env?
"Take the &env from a macro, and tell whether we are expanding into cljs."
[env]
(boolean (:ns env)))
(defmacro if-cljs
"Return then if we are generating cljs code and else for Clojure code.
https://groups.google.com/d/msg/clojurescript/iBY5HaQda4A/w1lAQi9_AwsJ"
[then else]
(if (cljs-env? &env) then else))
;; It's a huge pain to declare cross-environment macros. This is awful, but making the namespace a
;; parameter appears to be *even worse*.
(defmacro deftest-async
[name & body]
`(if-cljs
(cljs.test/deftest
~(with-meta name {:async true})
(cljs.test/async done#
(->
(datomish.pair-chan/go-pair ~@body)
(cljs.core.async/take! (fn [v# e#]
(cljs.test/is (= e# nil))
(done#))))))
(clojure.test/deftest
~(with-meta name {:async true})
(let [[v# e#] (clojure.core.async/<!! (datomish.pair-chan/go-pair ~@body))]
(clojure.test/is (= e# nil))))))

View file

@ -1,7 +1,7 @@
(ns datomish.test (ns datomish.test
(:require (:require
[doo.runner :refer-macros [doo-tests]] [doo.runner :refer-macros [doo-tests doo-all-tests]]
[cljs.test :as t :refer-macros [is are deftest testing]] [cljs.test :as t :refer-macros [is are deftest testing]]
datomish.test.core)) datomish.test-macros-test))
(doo-tests 'datomish.test.core) (doo-tests 'datomish.test-macros-test)

View file

@ -1,7 +0,0 @@
(ns datomish.test.core
(:require
[cljs.test :as t :refer-macros [is are deftest testing]]
[datomish.core :as d]))
(deftest test-core
(is (= 1 1)))

View file

@ -0,0 +1,16 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
(ns datomish.test-macros-test
(:require
[datomish.pair-chan :refer [go-pair]]
[datomish.test-macros :refer [deftest-async]]
[clojure.core.async :as a]
[clojure.test :as t :refer [is are deftest testing]]))
(deftest sync-test
(is (= 1 1)))
(deftest-async async-test
(is (= 1 1)))

View file

@ -0,0 +1,17 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
(ns datomish.test-macros-test
(:require-macros
[datomish.pair-chan :refer [go-pair]]
[datomish.test-macros :refer [deftest-async]]
[cljs.core.async.macros])
(:require [cljs.core.async]
[cljs.test :refer-macros [is are deftest testing async]]))
(deftest sync-test
(is (= 1 1)))
(deftest-async async-test
(is (= 1 1)))