diff --git a/.gitignore b/.gitignore index e86df66..1f14e58 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,9 @@ pom.xml pom.xml.asc .repl* -dev-resources/ +dev/resources/public/js/* figwheel_server.log +build.xml *.jar *.class /.lein-* diff --git a/README.md b/README.md index 2777984..d0160b9 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,9 @@ _**Alan J. Perlis** from [Structure and Interpretation of Computer Programs](htt - [Plugging into (reset)](#plugging-into-reset) - [Suspendable Example Application](#suspendable-example-application) - [Affected States](#affected-states) + +- [ClojureScript is Clojure](doc/clojurescript.md) + - [Logging](#logging) - [Mount and Develop!](#mount-and-develop) - [Running New York Stock Exchange](#running-new-york-stock-exchange) @@ -152,7 +155,7 @@ this `app-config`, being top level, can be used in other namespaces, including t (defstate conn :start (create-connection app-config)) ``` -[here](https://github.com/tolitius/mount/blob/master/test/app/nyse.clj) +[here](dev/clj/app/nyse.clj) is an example of a Datomic connection that "depends" on a similar `app-config`. ## Value of values @@ -191,7 +194,7 @@ Besides scalar values, lifecycle functions can take anonymous functions, partial (defstate private-f :start pf) ``` -Check out [fun-with-values-test](https://github.com/tolitius/mount/blob/0.1.5/test/check/fun_with_values_test.clj) for more details. +Check out [fun-with-values-test](test/mount/test/fun_with_values.cljc) for more details. ## The Importance of Being Reloadable @@ -207,7 +210,7 @@ dev=> (mount/start) While it is not always necessary, mount lificycle can be easily hooked up to [tools.namespace](https://github.com/clojure/tools.namespace), to make the whole application reloadable with refreshing the app namespaces. -Here is a [dev.clj](https://github.com/tolitius/mount/blob/master/dev/dev.clj) as an example, that sums up to: +Here is a [dev.clj](dev/dev.clj) as an example, that sums up to: ```clojure (defn go [] @@ -244,7 +247,7 @@ dev=> (reset) :ready ``` -You can see examples of start and stop flows in the [example app](https://github.com/tolitius/mount#mount-and-develop). +You can see examples of start and stop flows in the [example app](README.md#mount-and-develop). ## Start and Stop Parts of Application @@ -260,7 +263,7 @@ In REPL or during testing it is often very useful to work with / start / stop _o which will only start/stop `app-config` and `conn` (won't start any other states). -Here is an [example](test/check/parts_test.clj) test that uses only two namespaces checking that the third one is not started. +Here is an [example](test/mount/test/parts.cljc) test that uses only two namespaces checking that the third one is not started. ## Start an Application Without Certain States @@ -275,7 +278,7 @@ The `start-without` function can do just that: which will start an application without starting `feed-listener` and `nrepl` states. -Here is an [example](test/check/start_without_test.clj) test that excludes Datomic connection and nREPL from an application on start. +Here is an [example](test/mount/test/start_without.cljc) test that excludes Datomic connection and nREPL from an application on start. ## Swapping Alternate Implementations @@ -298,7 +301,7 @@ One thing to note, whenever is run after `start-with`, it rolls back to an original "state of states", i.e. `#'app.nyse/db` is `#'app.nyse/db` again. So subsequent calls to `(mount/start)` or even to `(mount/start-with {something else})` will start from a clean slate. -Here is an [example](test/check/start_with_test.clj) test that starts an app with mocking Datomic connection and nREPL. +Here is an [example](test/mount/test/start_with.cljc) test that starts an app with mocking Datomic connection and nREPL. ## Stop an Application Except Certain States @@ -449,7 +452,7 @@ The way this is done is via an excellent [robert hooke](https://github.com/techn ## Mount and Develop! -`mount` comes with an example [app](https://github.com/tolitius/mount/tree/master/test/app) +`mount` comes with an example [app](dev/clj/app) that has 3 states: * `config`, loaded from the files and refreshed on each `(reset)` diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..a07a0ea --- /dev/null +++ b/circle.yml @@ -0,0 +1,10 @@ +machine: + java: + version: oraclejdk8 + +test: + override: + - lein do clean, test + - lein do clean, doo phantom test once + - lein do clean, cljsbuild once prod + - lein test2junit diff --git a/test/app/config.clj b/dev/clj/app/conf.clj similarity index 73% rename from test/app/config.clj rename to dev/clj/app/conf.clj index 1e18a59..3273a95 100644 --- a/test/app/config.clj +++ b/dev/clj/app/conf.clj @@ -1,4 +1,4 @@ -(ns app.config +(ns app.conf (:require [mount.core :as mount :refer [defstate]] [clojure.edn :as edn] [clojure.tools.logging :refer [info]])) @@ -9,5 +9,5 @@ slurp edn/read-string)) -(defstate app-config - :start (load-config "test/resources/config.edn")) +(defstate config + :start (load-config "dev/resources/config.edn")) diff --git a/test/app/app.clj b/dev/clj/app/example.clj similarity index 95% rename from test/app/app.clj rename to dev/clj/app/example.clj index f529c95..81ffb49 100644 --- a/test/app/app.clj +++ b/dev/clj/app/example.clj @@ -1,9 +1,9 @@ -(ns app +(ns app.example (:require [datomic.api :as d] [clojure.tools.nrepl.server :refer [start-server stop-server]] [mount.core :as mount :refer [defstate]] [app.utils.datomic :refer [touch]] - [app.config :refer [app-config]] + [app.conf :refer [config]] [app.nyse :as nyse])) ;; example on creating a network REPL @@ -11,7 +11,7 @@ (start-server :bind host :port port)) ;; nREPL is just another simple state -(defstate nrepl :start (start-nrepl (:nrepl app-config)) +(defstate nrepl :start (start-nrepl (:nrepl config)) :stop (stop-server nrepl)) ;; datomic schema diff --git a/test/app/nyse.clj b/dev/clj/app/nyse.clj similarity index 80% rename from test/app/nyse.clj rename to dev/clj/app/nyse.clj index 649dfad..f6a8ede 100644 --- a/test/app/nyse.clj +++ b/dev/clj/app/nyse.clj @@ -2,7 +2,7 @@ (:require [mount.core :as mount :refer [defstate]] [datomic.api :as d] [clojure.tools.logging :refer [info]] - [app.config :refer [app-config]])) + [app.conf :refer [config]])) (defn- new-connection [conf] (info "conf: " conf) @@ -17,5 +17,5 @@ (.release conn) ;; usually it's not released, here just to illustrate the access to connection on (stop) (d/delete-database uri))) -(defstate conn :start (new-connection app-config) - :stop (disconnect app-config conn)) +(defstate conn :start (new-connection config) + :stop (disconnect config conn)) diff --git a/test/app/utils/datomic.clj b/dev/clj/app/utils/datomic.clj similarity index 100% rename from test/app/utils/datomic.clj rename to dev/clj/app/utils/datomic.clj diff --git a/test/app/utils/logging.clj b/dev/clj/app/utils/logging.clj similarity index 91% rename from test/app/utils/logging.clj rename to dev/clj/app/utils/logging.clj index dc0216c..2d8c64d 100644 --- a/test/app/utils/logging.clj +++ b/dev/clj/app/utils/logging.clj @@ -26,10 +26,10 @@ :resume (if (status :suspended) ">> resuming"))) (defn log-status [f & args] - (let [{:keys [ns name] :as state} (second args) + (let [{:keys [var] :as state} (second args) action (f-to-action f)] (when-let [taking-over-the-world (whatcha-doing? state action)] - (info (str taking-over-the-world ".. " (ns-resolve ns name)))) + (info (str taking-over-the-world ".. " var))) (apply f args))) (defonce lifecycle-fns diff --git a/dev/cljs/app/audit_log.cljs b/dev/cljs/app/audit_log.cljs new file mode 100644 index 0000000..9ce586c --- /dev/null +++ b/dev/cljs/app/audit_log.cljs @@ -0,0 +1,25 @@ +(ns app.audit-log + (:require [datascript.core :as d] + [cljs-time.core :refer [now]]) + (:require-macros [mount.core :refer [defstate]])) + +(defstate log :start (d/create-conn {})) + +(defn audit [db source & msg] + (d/transact! @db [{:db/id -1 + :source source + :timestamp (now) + :msg (apply str msg)}])) + +(defn find-source-logs [db source] + (d/q '{:find [?t ?msg] + :in [$ ?s] + :where [[?e :source ?s] + [?e :timestamp ?t] + [?e :msg ?msg]]} + @@db source)) + +(defn find-all-logs [db] + (->> (map :e (d/datoms @@db :aevt :timestamp)) + dedupe + (d/pull-many @@db '[:timestamp :source :msg]))) diff --git a/dev/cljs/app/conf.cljs b/dev/cljs/app/conf.cljs new file mode 100644 index 0000000..14d0adb --- /dev/null +++ b/dev/cljs/app/conf.cljs @@ -0,0 +1,9 @@ +(ns app.conf + (:require [app.audit-log :refer [audit log]]) + (:require-macros [mount.core :refer [defstate]])) + +(defn load-config [path] + (audit log :app-conf "loading config from '" path "' (at least pretending)") + {:system-a {:uri "ws://echo.websocket.org/"}}) + +(defstate config :start (load-config "resources/config.end")) diff --git a/dev/cljs/app/example.cljs b/dev/cljs/app/example.cljs new file mode 100644 index 0000000..2b9b475 --- /dev/null +++ b/dev/cljs/app/example.cljs @@ -0,0 +1,26 @@ +(ns app.example + (:require [mount.core :as mount] + [app.conf] + [app.websockets] + [app.audit-log :refer [log find-all-logs]] + [cljs-time.format :refer [unparse formatters]] + [hiccups.runtime :as hiccupsrt]) + (:require-macros [hiccups.core :as hiccups :refer [html]])) + +(defn format-log-event [{:keys [timestamp source msg]}] + (str (unparse (formatters :date-hour-minute-second-fraction) timestamp) + " → [" (name source) "]: " msg)) + +(defn show-log [] + (.write js/document + (html [:ul (doall (for [e (find-all-logs log)] + [:li (format-log-event e)]))]))) + +(mount/start) + +;; time to establish a websocket connection before disconnecting +(js/setTimeout #(mount/stop-except "#'app.audit-log/log") 500) + +;; time to close a connection to show it in audit +(js/setTimeout #(show-log) 1000) + diff --git a/dev/cljs/app/websockets.cljs b/dev/cljs/app/websockets.cljs new file mode 100644 index 0000000..ae0dc4c --- /dev/null +++ b/dev/cljs/app/websockets.cljs @@ -0,0 +1,22 @@ +(ns app.websockets + (:require [app.conf :refer [config]] + [app.audit-log :refer [audit log]]) + (:require-macros [mount.core :refer [defstate]])) + +(defn ws-status [ws] + {:url (.-url ws) :ready-state (.-readyState ws)}) + +(defn connect [uri] + (let [ws (js/WebSocket. uri)] + (audit log :system-a "connecting to " (ws-status ws)) + (set! (.-onopen ws) #(audit log :system-a "opened " (ws-status ws))) + (set! (.-onclose ws) #(audit log :system-a "closed " (ws-status ws))) + ws)) + +(defn disconnect [ws] + (audit log :system-a "closing " (ws-status @ws)) + (.close @ws) + (audit log :system-a "disconnecting " (ws-status @ws))) + +(defstate system-a :start (connect (get-in @config [:system-a :uri])) + :stop (disconnect system-a)) diff --git a/dev/dev.clj b/dev/dev.clj index e74d05b..d9c493e 100644 --- a/dev/dev.clj +++ b/dev/dev.clj @@ -10,18 +10,15 @@ [clojure.string :as str] [clojure.test :as test] [clojure.tools.namespace.repl :as tn] - [check.parts-test] - [check.start-with-test] - [check.suspend-resume-test] [mount.core :as mount] [app.utils.logging :refer [with-logging-status]] - [app :refer [create-nyse-schema find-orders add-order]])) ;; <<<< replace this your "app" namespace(s) you want to be available at REPL time + [app.example :refer [create-nyse-schema find-orders add-order]])) ;; <<<< replace this your "app" namespace(s) you want to be available at REPL time (defn start [] (with-logging-status) - (mount/start #'app.config/app-config + (mount/start #'app.conf/config #'app.nyse/conn - #'app/nrepl)) ;; example on how to start app with certain states + #'app.example/nrepl)) ;; example on how to start app with certain states (defn stop [] (mount/stop)) @@ -45,3 +42,5 @@ [] (stop) (tn/refresh :after 'dev/go)) + +(mount/in-clj-mode) diff --git a/test/resources/config.edn b/dev/resources/config.edn similarity index 100% rename from test/resources/config.edn rename to dev/resources/config.edn diff --git a/dev/resources/public/index.html b/dev/resources/public/index.html new file mode 100644 index 0000000..f7ca4d9 --- /dev/null +++ b/dev/resources/public/index.html @@ -0,0 +1,6 @@ + + +
+ + + diff --git a/dev/user.clj b/dev/user.clj index a5693f3..058241e 100644 --- a/dev/user.clj +++ b/dev/user.clj @@ -1,4 +1,3 @@ -(defn dev - [] +(defn dev [] (require 'dev) (in-ns 'dev)) diff --git a/doc/clojurescript.md b/doc/clojurescript.md new file mode 100644 index 0000000..54b199c --- /dev/null +++ b/doc/clojurescript.md @@ -0,0 +1,120 @@ +## Managing state in ClojureScript + +- [The "Why"](#the-why) +- [Mount Modes](#mount-modes) + - [Just Clojure Mode](#just-clojure-mode) + - [Clojure and ClojureScript Mode](#clojure-_and_-clojurescript-mode) +- [Mounting that ClojureScript](#mounting-that-clojurescript) + - [Using States](#using-states) +- [Thanks](#thanks) + +In case you need to manage state in ClojureScript using mount, _all_ the mount Clojure features are supported in ClojureScript. +Which means all the mount Clojure [documentation](../README.md) is the mount ClojureScript documentation. + +With a slight change in [_mode_](clojurescript.md#mount-modes) ( no change in _mood_ though, just the _mode_ :)). + +### The "Why" + +Since [reader conditionals](http://clojure.org/reader#The%20Reader--Reader%20Conditionals) were added in Clojure 1.7, +it became a lot easier to target both platforms with lots of code reuse. You might have noticed +that most of mount code lives in `.cljc` files. + +The way mount is designed it "mounts" itself to a solid Clojure [namespace API](http://clojure.org/namespaces), +and while `.cljc` helps a lot with targeting Clojure and ClojureScript, JavaScript VM is vastly different from JVM. +Since JavaScript mostly tagrets browsers, mobile devices and IoT, +it is quite importand to [compress](https://github.com/clojure/clojurescript/wiki/Advanced-Compilation) the final result. + +Which means that Clojure namespaces API are not that well supported in ClojureScript, since they get renamed and optimized +during compilation + of course no native namespace support on the JavaScript side +(but that is somewhat solved with [Google Closure](https://closure-library.googlecode.com/git-history/docs/local_closure_goog_base.js.source.html#line428)). + +But. When developing an application in Clojure and ClojureScript, it would only make sense if the API for any library +would be _identical_ for both platforms. It should be transparent for developers whether they use a library in Clojure or ClojureScript. +It is not possible for all libraries (i.e. concurrency, reified Vars, etc.), but we should try to make it possible for most. + +### Mount Modes + +Mount has two modes `clj` and `cljc`. + +#### Just Clojure Mode + +`clj` mode is _default_, and all the APIs are exactly the same as they are in the mount Clojure [documentation](../README.md). + +#### Clojure _and_ ClojureScript Mode + +`cljc` mode is is not default, but it is easy to switch to it: + +```clojure +(mount/in-cljc-mode) +``` + +this sets mount into the `cljc` mode. In this mode mount supports _both_: Clojure and ClojureScript with one difference +from the default `clj` mode: + +> all states are "_derefable_" + +which means in order to use them, you'd need to `@` it. That's where the difference between two modes end. + +Again, `cljc` mode API is _consistent across both_ Clojure and ClojureScript. + +While initially it may sound strange, this approach has very nice properties: + +* Mentally something that you defer (`@`) is associated with a state behind it +* The whole system may start lazily without an explicit call `(mount/start)` +* States may have watchers which is just an idea at this point, but it could be quite useful + +Now as the theory is laid out... + +### Mounting that ClojureScript + +Let's look at the example [ClojureScript app](../dev/cljs/app) that uses mount to manage several states: + +* [Datascript](https://github.com/tonsky/datascript) Database +* Websocket Connection +* Configuration + +In order to run it, just compile `cljs` (in `:advanced` mode, because why not? :)) with: + +```clojure +$ lein do clean, cljsbuild once prod +Compiling ClojureScript... +Compiling "dev/resources/public/js/compiled/mount.js" from ["src" "dev/cljs"]... +Successfully compiled "dev/resources/public/js/compiled/mount.js" in 23.966 seconds. +``` + +And just open a browser at `file:///[path-to-mount]/mount/dev/resources/public/index.html`: + +
+
+The flow behind the app is quite simple:
+
+* load config
+* open a WebSocket connection
+* keep an audit log in Datascript
+* call `(mount/stop)` to disconnect
+
+#### Using States
+
+A good example of derefing state is here in [websockets.cljs](https://github.com/tolitius/mount/blob/0825ad2ed085b73b7ae989b4382ce4e0376e4be3/dev/cljs/app/websockets.cljs#L21):
+
+```clojure
+
+(ns app.websockets
+ (:require [app.conf :refer [config]]
+ [app.audit-log :refer [audit log]])
+ (:require-macros [mount.core :refer [defstate]]))
+
+;; ...
+
+(defstate system-a :start (connect (get-in @config [:system-a :uri]))
+ :stop (disconnect system-a))
+```
+
+notice how config is deferef'ed `@config` in order to use its state. It of course does not have to be deref'ed here, and
+can be just passed along to the `connect` function to be `@`ed there instead.
+
+### Thanks
+
+I'd like to thank these good people for brainstorming and supporting the idea of Mount in ClojureScript universe:
+
+[@DomKM](https://github.com/DomKM), [@yogthos](https://github.com/yogthos) and [@edvorg](https://github.com/edvorg)
diff --git a/doc/img/mount.cljs.example.png b/doc/img/mount.cljs.example.png
new file mode 100644
index 0000000..5edad72
Binary files /dev/null and b/doc/img/mount.cljs.example.png differ
diff --git a/project.clj b/project.clj
index 451b12e..1c039ba 100644
--- a/project.clj
+++ b/project.clj
@@ -6,13 +6,56 @@
:source-paths ["src"]
- :dependencies [[org.clojure/clojure "1.7.0"]]
+ :dependencies [] ;; for visual clarity
- :profiles {:dev {:source-paths ["dev" "test/app"]
- :dependencies [[yesql "0.5.1"]
+ :profiles {:dev {:source-paths ["dev" "dev/clj"]
+ :dependencies [[org.clojure/clojure "1.7.0"]
+ [org.clojure/clojurescript "1.7.170"]
+ [datascript "0.13.3"]
+ [hiccups "0.3.0"]
+ [com.andrewmcveigh/cljs-time "0.3.14"]
[ch.qos.logback/logback-classic "1.1.3"]
[org.clojure/tools.logging "0.3.1"]
[robert/hooke "1.3.0"]
[org.clojure/tools.namespace "0.2.11"]
[org.clojure/tools.nrepl "0.2.11"]
- [com.datomic/datomic-free "0.9.5327" :exclusions [joda-time]]]}})
+ [com.datomic/datomic-free "0.9.5327" :exclusions [joda-time]]]
+
+ :plugins [[lein-cljsbuild "1.1.1"]
+ [lein-doo "0.1.6"]
+ [lein-figwheel "0.5.0-2"]
+ [test2junit "1.1.3"]]
+
+ :test2junit-output-dir ~(or (System/getenv "CIRCLE_TEST_REPORTS") "target/test2junit")
+
+ :clean-targets ^{:protect false} [:target-path
+ [:cljsbuild :builds :dev :compiler :output-dir]
+ [:cljsbuild :builds :prod :compiler :output-to]]
+ :cljsbuild {
+ :builds {:dev
+ {:source-paths ["src" "dev/cljs"]
+ :figwheel true
+
+ :compiler {:main app.example
+ :asset-path "js/compiled/out"
+ :output-to "dev/resources/public/js/compiled/mount.js"
+ :output-dir "dev/resources/public/js/compiled/out"
+ :optimizations :none
+ :source-map true
+ :source-map-timestamp true}}
+ :test
+ {:source-paths ["src" "dev/cljs" "test"]
+ :compiler {:main mount.test
+ ;; :asset-path "js/compiled/out"
+ :output-to "dev/resources/public/js/compiled/mount.js"
+ :output-dir "dev/resources/public/js/compiled/test"
+ :optimizations :none
+ :source-map true
+ :source-map-timestamp true}}
+ :prod
+ {:source-paths ["src" "dev/cljs"]
+ :compiler {:output-to "dev/resources/public/js/compiled/mount.js"
+ :optimizations :advanced
+ :pretty-print false}}}}}
+
+ :test {:source-paths ["dev" "test/clj" "test"]}})
diff --git a/src/mount/core.clj b/src/mount/core.clj
deleted file mode 100644
index aa7b7f2..0000000
--- a/src/mount/core.clj
+++ /dev/null
@@ -1,220 +0,0 @@
-(ns mount.core
- (:require [mount.tools.macro :as macro]))
-
-(defonce ^:private mount-state 42)
-(defonce ^:private -args (atom :no-args)) ;; mostly for command line args and external files
-(defonce ^:private state-seq (atom 0))
-(defonce ^:private state-order (atom {}))
-(defonce ^:private running (atom {})) ;; to clean dirty states on redefs
-
-;; supporting tools.namespace: (disable-reload!)
-(alter-meta! *ns* assoc ::load false) ;; to exclude the dependency
-
-(defn- make-state-seq [state]
- (or (@state-order state)
- (let [nseq (swap! state-seq inc)]
- (swap! state-order assoc state nseq)
- nseq)))
-
-(deftype NotStartedState [state]
- Object
- (toString [this]
- (str "'" state "' is not started (to start all the states call mount/start)")))
-
-;;TODO validate the whole lifecycle
-(defn- validate [{:keys [start stop suspend resume] :as lifecycle}]
- (cond
- (not start) (throw
- (IllegalArgumentException. "can't start a stateful thing without a start function. (i.e. missing :start fn)"))
- (and suspend (not resume)) (throw
- (IllegalArgumentException. "suspendable state should have a resume function (i.e. missing :resume fn)"))))
-
-(defn- with-ns [ns name]
- (str ns "/" name))
-
-(defn- pounded? [f]
- (let [pound "(fn* [] "] ;;TODO: think of a better (i.e. typed) way to distinguish #(f params) from (fn [params] (...)))
- (.startsWith (str f) pound)))
-
-(defn- unpound [f]
- (if (pounded? f)
- (nth f 2) ;; magic 2 is to get the body => ["fn*" "[]" "(fn body)"]
- f))
-
-(defn- cleanup-if-dirty
- "in case a namespace is recompiled without calling (mount/stop),
- a running state instance will still be running.
- this function stops this 'lost' state instance.
- it is meant to be called by defstate before defining a new state"
- [state]
- (when-let [stop (@running state)]
- (stop)))
-
-(defmacro defstate [state & body]
- (let [[state params] (macro/name-with-attributes state body)
- {:keys [start stop suspend resume] :as lifecycle} (apply hash-map params)]
- (validate lifecycle)
- (cleanup-if-dirty (with-ns *ns* state))
- (let [s-meta (cond-> {:mount-state mount-state
- :order (make-state-seq (with-ns *ns* state))
- :start `(fn [] ~start)
- :status #{:stopped}}
- stop (assoc :stop `(fn [] ~(unpound stop)))
- suspend (assoc :suspend `(fn [] ~suspend))
- resume (assoc :resume `(fn [] ~resume)))]
- `(defonce ~(with-meta state (merge (meta state) s-meta))
- (NotStartedState. ~(str state))))))
-
-(defn- record! [{:keys [ns name]} f done]
- (let [state (f)]
- (swap! done conj (ns-resolve ns name))
- state))
-
-(defn- up [var {:keys [ns name start stop resume status] :as state} done]
- (when-not (:started status)
- (let [s (try (if (:suspended status)
- (record! state resume done)
- (record! state start done))
- (catch Throwable t
- (throw (RuntimeException. (str "could not start [" name "] due to") t))))]
- (intern ns (symbol name) s)
- (swap! running assoc (with-ns ns name) stop)
- (alter-meta! var assoc :status #{:started}))))
-
-(defn- down [var {:keys [ns name stop status] :as state} done]
- (when (some status #{:started :suspended})
- (when stop
- (try
- (record! state stop done)
- (catch Throwable t
- (throw (RuntimeException. (str "could not stop [" name "] due to") t)))))
- (intern ns (symbol name) (NotStartedState. name)) ;; (!) if a state does not have :stop when _should_ this might leak
- (swap! running dissoc (with-ns ns name))
- (alter-meta! var assoc :status #{:stopped})))
-
-(defn- sigstop [var {:keys [ns name suspend resume status] :as state} done]
- (when (and (:started status) resume) ;; can't have suspend without resume, but the reverse is possible
- (when suspend ;; don't suspend if there is only resume function (just mark it :suspended?)
- (let [s (try (record! state suspend done)
- (catch Throwable t
- (throw (RuntimeException. (str "could not suspend [" name "] due to") t))))]
- (intern ns (symbol name) s)))
- (alter-meta! var assoc :status #{:suspended})))
-
-(defn- sigcont [var {:keys [ns name start resume status] :as state} done]
- (when (instance? NotStartedState var)
- (throw (RuntimeException. (str "could not resume [" name "] since it is stoppped (i.e. not suspended)"))))
- (when (:suspended status)
- (let [s (try (record! state resume done)
- (catch Throwable t
- (throw (RuntimeException. (str "could not resume [" name "] due to") t))))]
- (intern ns (symbol name) s)
- (alter-meta! var assoc :status #{:started}))))
-
-;;TODO args might need more thinking
-(defn args [] @-args)
-
-(defn mount-state? [var]
- (= (-> var meta :mount-state)
- mount-state))
-
-(defn find-all-states []
- (->> (all-ns)
- (mapcat ns-interns)
- (map second)
- (filter mount-state?)))
-
-;;TODO ns based for now. need to be _state_ based
-(defn- add-deps [{:keys [ns] :as state} all]
- (let [refers (ns-refers ns)
- any (set all)
- deps (filter (comp any val) refers)]
- (assoc state :deps deps)))
-
-(defn states-with-deps []
- (let [all (find-all-states)]
- (->> (map (comp #(add-deps % all)
- #(select-keys % [:name :order :ns :status])
- meta)
- all)
- (sort-by :order))))
-
-(defn- bring [states fun order]
- (let [done (atom [])]
- (->> states
- (sort-by (comp :order meta) order)
- (map #(fun % (meta %) done))
- dorun)
- @done))
-
-(defn- merge-lifecycles
- "merges with overriding _certain_ non existing keys.
- i.e. :suspend is in a 'state', but not in a 'substitute': it should be overriden with nil
- however other keys of 'state' (such as :ns,:name,:order) should not be overriden"
- ([state sub]
- (merge-lifecycles state nil sub))
- ([state origin {:keys [start stop suspend resume status]}]
- (assoc state :origin origin
- :status status
- :start start :stop stop :suspend suspend :resume resume)))
-
-(defn- rollback! [state]
- (let [{:keys [origin]} (meta state)]
- (when origin
- (alter-meta! state #(merge-lifecycles % origin)))))
-
-(defn- substitute! [state with]
- (let [lifecycle-fns #(select-keys % [:start :stop :suspend :resume :status])
- origin (meta state)
- sub (meta with)]
- (alter-meta! with assoc :sub? true)
- (alter-meta! state #(merge-lifecycles % (lifecycle-fns origin) sub))))
-
-(defn- unsub [state]
- (when (-> (meta state) :sub?)
- (alter-meta! state dissoc :sub?)))
-
-(defn- all-without-subs []
- (remove (comp :sub? meta) (find-all-states)))
-
-(defn start [& states]
- (let [states (or (seq states) (all-without-subs))]
- {:started (bring states up <)}))
-
-(defn stop [& states]
- (let [states (or states (find-all-states))
- _ (dorun (map unsub states)) ;; unmark substitutions marked by "start-with"
- stopped (bring states down >)]
- (dorun (map rollback! states)) ;; restore to origin from "start-with"
- {:stopped stopped}))
-
-(defn stop-except [& states]
- (let [all (set (find-all-states))
- states (remove (set states) all)]
- (apply stop states)))
-
-(defn start-with-args [xs & states]
- (reset! -args xs)
- (if (first states)
- (start states)
- (start)))
-
-(defn start-with [with]
- (doseq [[from to] with]
- (substitute! from to))
- (start))
-
-(defn start-without [& states]
- (if (first states)
- (let [app (set (all-without-subs))
- without (remove (set states) app)]
- (apply start without))
- (start)))
-
-(defn suspend [& states]
- (let [states (or (seq states) (all-without-subs))]
- {:suspended (bring states sigstop <)}))
-
-(defn resume [& states]
- (let [states (or (seq states) (all-without-subs))]
- {:resumed (bring states sigcont <)}))
diff --git a/src/mount/core.cljc b/src/mount/core.cljc
new file mode 100644
index 0000000..59da9ec
--- /dev/null
+++ b/src/mount/core.cljc
@@ -0,0 +1,271 @@
+(ns mount.core
+ #?(:clj (:require [mount.tools.macro :refer [on-error throw-runtime] :as macro])
+ :cljs (:require [mount.tools.macro :as macro]
+ [mount.tools.cljs :as cljs]))
+ #?(:cljs (:require-macros [mount.tools.macro :refer [on-error throw-runtime]])))
+
+(defonce ^:private -args (atom :no-args)) ;; mostly for command line args and external files
+(defonce ^:private state-seq (atom 0))
+(defonce ^:private mode (atom :clj))
+(defonce ^:private meta-state (atom {}))
+(defonce ^:private running (atom {})) ;; to clean dirty states on redefs
+
+;; supporting tools.namespace: (disable-reload!)
+#?(:clj
+ (alter-meta! *ns* assoc ::load false)) ;; to exclude the dependency
+
+(defn- make-state-seq [state]
+ (or (:order (@meta-state state))
+ (swap! state-seq inc)))
+
+(deftype NotStartedState [state]
+ Object
+ (toString [this]
+ (str "'" state "' is not started (to start all the states call mount/start)")))
+
+;;TODO validate the whole lifecycle
+(defn- validate [{:keys [start stop suspend resume] :as lifecycle}]
+ (cond
+ (not start) (throw-runtime "can't start a stateful thing without a start function. (i.e. missing :start fn)")
+ (and suspend
+ (not resume)) (throw-runtime "suspendable state should have a resume function (i.e. missing :resume fn)")))
+
+(defn- with-ns [ns name]
+ (str "#'" ns "/" name))
+
+(defn- pounded? [f]
+ (let [pound "(fn* [] "] ;;TODO: think of a better (i.e. typed) way to distinguish #(f params) from (fn [params] (...)))
+ #?(:clj (.startsWith (str f) pound)
+ :cljs (cljs/starts-with? (str f) pound))))
+
+(defn unpound [f]
+ (if (pounded? f)
+ (nth f 2) ;; magic 2 is to get the body => ["fn*" "[]" "(fn body)"]
+ f))
+
+(defn- cleanup-if-dirty
+ "in case a namespace is recompiled without calling (mount/stop),
+ a running state instance will still be running.
+ this function stops this 'lost' state instance.
+ it is meant to be called by defstate before defining a new state"
+ [state]
+ (when-let [stop (@running state)]
+ (stop)))
+
+#?(:clj
+ (defn current-state [state]
+ (let [{:keys [inst var]} (@meta-state state)]
+ (if (= @mode :cljc)
+ @inst
+ (var-get var))))
+
+ :cljs
+ (defn current-state [state]
+ (-> (@meta-state state) :inst deref)))
+
+#?(:clj
+ (defn alter-state! [{:keys [var inst]} value]
+ (if (= @mode :cljc)
+ (reset! inst value)
+ (alter-var-root var (constantly value))))
+
+ :cljs
+ (defn alter-state! [{:keys [inst]} value]
+ (reset! inst value)))
+
+(defn- update-meta! [path v]
+ (swap! meta-state assoc-in path v))
+
+(defn- record! [state-name f done]
+ (let [state (f)]
+ (swap! done conj state-name)
+ state))
+
+(defn- up [state {:keys [start stop resume status] :as current} done]
+ (when-not (:started status)
+ (let [s (on-error (str "could not start [" state "] due to")
+ (if (:suspended status)
+ (record! state resume done)
+ (record! state start done)))]
+ (alter-state! current s)
+ (swap! running assoc state stop)
+ (update-meta! [state :status] #{:started}))))
+
+(defn- down [state {:keys [stop status] :as current} done]
+ (when (some status #{:started :suspended})
+ (when stop
+ (on-error (str "could not stop [" state "] due to")
+ (record! state stop done)))
+ (alter-state! current (NotStartedState. state)) ;; (!) if a state does not have :stop when _should_ this might leak
+ (swap! running dissoc state)
+ (update-meta! [state :status] #{:stopped})))
+
+(defn- sigstop [state {:keys [resume suspend status] :as current} done]
+ (when (and (:started status) resume) ;; can't have suspend without resume, but the reverse is possible
+ (when suspend ;; don't suspend if there is only resume function (just mark it :suspended?)
+ (let [s (on-error (str "could not suspend [" state "] due to")
+ (record! state suspend done))]
+ (alter-state! current s)))
+ (update-meta! [state :status] #{:suspended})))
+
+(defn- sigcont [state {:keys [resume status] :as current} done]
+ (when (:suspended status)
+ (let [s (on-error (str "could not resume [" state "] due to")
+ (record! state resume done))]
+ (alter-state! current s)
+ (update-meta! [state :status] #{:started}))))
+
+(deftype DerefableState [name]
+ #?(:clj clojure.lang.IDeref
+ :cljs IDeref)
+ (#?(:clj deref
+ :cljs -deref)
+ [_]
+ (let [{:keys [status inst] :as state} (@meta-state name)]
+ (when-not (:started status)
+ (up name state (atom #{})))
+ @inst)))
+
+(defmacro defstate [state & body]
+ (let [[state params] (macro/name-with-attributes state body)
+ {:keys [start stop suspend resume] :as lifecycle} (apply hash-map params)
+ state-name (with-ns #?(:clj *ns*
+ :cljs (cljs/this-ns)) state) ;; on cljs side (cljs.analyzer/*cljs-ns*) may do it, but still might not be good for :advanced
+ order (make-state-seq state-name)
+ sym (str state)]
+ (validate lifecycle)
+ (cleanup-if-dirty state-name)
+ (let [s-meta (cond-> {:order order
+ :start `(fn [] ~start)
+ :status #{:stopped}}
+ stop (assoc :stop `(fn [] ~stop))
+ suspend (assoc :suspend `(fn [] ~suspend))
+ resume (assoc :resume `(fn [] ~resume)))]
+ `(do
+ (def ~state (DerefableState. ~state-name))
+ ((var update-meta!) [~state-name] (assoc ~s-meta :inst (atom (NotStartedState. ~state-name))
+ :var (var ~state)))
+ (var ~state)))))
+
+(defn in-cljc-mode []
+ (reset! mode :cljc))
+
+(defn in-clj-mode []
+ (reset! mode :clj))
+
+;;TODO args might need more thinking
+(defn args [] @-args)
+
+(defn- find-all-states []
+ (keys @meta-state))
+
+;;TODO ns based for now. need to be _state_ based
+#_(defn- add-deps [{:keys [ns] :as state} all]
+ (let [refers (ns-refers ns)
+ any (set all)
+ deps (filter (comp any val) refers)]
+ (assoc state :deps deps)))
+
+#_(defn states-with-deps []
+ (let [all (find-all-states)]
+ (->> (map (comp #(add-deps % all)
+ #(select-keys % [:name :order :ns :status])
+ meta)
+ all)
+ (sort-by :order))))
+
+
+#?(:clj
+ (defn- var-to-str [v]
+ (str v)))
+
+#?(:cljs
+ (defn var-to-str [v]
+ (if (var? v)
+ (let [{:keys [ns name]} (meta v)]
+ (with-ns ns name))
+ v)))
+
+(defn- bring [states fun order]
+ (let [done (atom [])]
+ (as-> states $
+ (map var-to-str $)
+ (select-keys @meta-state $)
+ (sort-by (comp :order val) order $)
+ (doseq [[k v] $] (fun k v done)))
+ @done))
+
+(defn- merge-lifecycles
+ "merges with overriding _certain_ non existing keys.
+ i.e. :suspend is in a 'state', but not in a 'substitute': it should be overriden with nil
+ however other keys of 'state' (such as :ns,:name,:order) should not be overriden"
+ ([state sub]
+ (merge-lifecycles state nil sub))
+ ([state origin {:keys [start stop suspend resume status]}]
+ (assoc state :origin origin
+ :status status
+ :start start :stop stop :suspend suspend :resume resume)))
+
+(defn- rollback! [state]
+ (let [{:keys [origin] :as sub} (@meta-state state)]
+ (when origin
+ (update-meta! [state] (merge-lifecycles sub origin)))))
+
+(defn- substitute! [state with]
+ (let [lifecycle-fns #(select-keys % [:start :stop :suspend :resume :status])
+ origin (@meta-state state)
+ sub (@meta-state with)]
+ (update-meta! [with :sub?] true)
+ (update-meta! [state] (merge-lifecycles origin (lifecycle-fns origin) sub))))
+
+(defn- unsub [state]
+ (when (-> (@meta-state state) :sub?)
+ (update-meta! [state :sub?] nil)))
+
+(defn- all-without-subs []
+ (remove (comp :sub? @meta-state) (find-all-states)))
+
+(defn start [& states]
+ (let [states (or (seq states) (all-without-subs))]
+ {:started (bring states up <)}))
+
+(defn stop [& states]
+ (let [states (or states (find-all-states))
+ _ (dorun (map unsub states)) ;; unmark substitutions marked by "start-with"
+ stopped (bring states down >)]
+ (dorun (map rollback! states)) ;; restore to origin from "start-with"
+ {:stopped stopped}))
+
+(defn stop-except [& states]
+ (let [all (set (find-all-states))
+ states (map var-to-str states)
+ states (remove (set states) all)]
+ (apply stop states)))
+
+(defn start-with-args [xs & states]
+ (reset! -args xs)
+ (if (first states)
+ (start states)
+ (start)))
+
+(defn start-with [with]
+ (doseq [[from to] with]
+ (substitute! (var-to-str from)
+ (var-to-str to)))
+ (start))
+
+(defn start-without [& states]
+ (if (first states)
+ (let [app (set (all-without-subs))
+ states (map var-to-str states)
+ without (remove (set states) app)]
+ (apply start without))
+ (start)))
+
+(defn suspend [& states]
+ (let [states (or (seq states) (all-without-subs))]
+ {:suspended (bring states sigstop <)}))
+
+(defn resume [& states]
+ (let [states (or (seq states) (all-without-subs))]
+ {:resumed (bring states sigcont <)}))
diff --git a/src/mount/tools/cljs.cljs b/src/mount/tools/cljs.cljs
new file mode 100644
index 0000000..35f1fc3
--- /dev/null
+++ b/src/mount/tools/cljs.cljs
@@ -0,0 +1,9 @@
+(ns mount.tools.cljs
+ (:require [cljs.analyzer :as ana]
+ [goog.string :as gstring]))
+
+(defn this-ns []
+ ana/*cljs-ns*)
+
+(defn starts-with? [s pre]
+ (gstring/startsWith s pre))
diff --git a/src/mount/tools/macro.clj b/src/mount/tools/macro.cljc
similarity index 81%
rename from src/mount/tools/macro.clj
rename to src/mount/tools/macro.cljc
index a0b1e1c..22961aa 100644
--- a/src/mount/tools/macro.clj
+++ b/src/mount/tools/macro.cljc
@@ -1,5 +1,17 @@
(ns mount.tools.macro)
+(defmacro on-error [msg f]
+ `(try
+ ~f
+ (catch #?(:clj Throwable
+ :cljs :default) t#
+ (throw #?(:clj (RuntimeException. ~msg t#)
+ :cljs (js/Error (str ~msg (.-stack t#))))))))
+
+(defmacro throw-runtime [msg]
+ `(throw #?(:clj (RuntimeException. ~msg)
+ :cljs (js/Error (str ~msg)))))
+
;; this is a one to one copy from https://github.com/clojure/tools.macro
;; to avoid a lib dependency for a single function
diff --git a/test/check/cleanup_dirty_states_test.clj b/test/check/cleanup_dirty_states_test.clj
deleted file mode 100644
index 73fd0a8..0000000
--- a/test/check/cleanup_dirty_states_test.clj
+++ /dev/null
@@ -1,13 +0,0 @@
-(ns check.cleanup_dirty_states_test
- (:require [mount.core :as mount]
- [app]
- [clojure.test :refer :all]))
-
-(deftest cleanup-dirty-states
- (let [_ (mount/start)]
- (is (not (.isClosed (:server-socket app/nrepl))))
- (require 'app :reload)
- (mount/start) ;; should not result in "BindException Address already in use" since the clean up will stop the previous instance
- (is (not (.isClosed (:server-socket app/nrepl))))
- (mount/stop)
- (is (instance? mount.core.NotStartedState app/nrepl))))
diff --git a/test/check/fun_with_values_test.clj b/test/check/fun_with_values_test.clj
deleted file mode 100644
index f1f4772..0000000
--- a/test/check/fun_with_values_test.clj
+++ /dev/null
@@ -1,55 +0,0 @@
-(ns check.fun-with-values-test
- (:require [mount.core :as mount :refer [defstate]]
- [clojure.test :refer :all]))
-
-(defn f [n]
- (fn [m]
- (+ n m)))
-
-(defn g [a b]
- (+ a b))
-
-(defn- pf [n]
- (+ 41 n))
-
-(defn fna []
- 42)
-
-(defstate scalar :start 42)
-(defstate fun :start #(inc 41))
-(defstate with-fun :start (inc 41))
-(defstate with-partial :start (partial g 41))
-(defstate f-in-f :start (f 41))
-(defstate f-no-args-value :start (fna))
-(defstate f-no-args :start fna)
-(defstate f-args :start g)
-(defstate f-value :start (g 41 1))
-(defstate private-f :start pf)
-
-(defn with-fun-and-values [f]
- (mount/start #'check.fun-with-values-test/scalar
- #'check.fun-with-values-test/fun
- #'check.fun-with-values-test/with-fun
- #'check.fun-with-values-test/with-partial
- #'check.fun-with-values-test/f-in-f
- #'check.fun-with-values-test/f-args
- #'check.fun-with-values-test/f-no-args-value
- #'check.fun-with-values-test/f-no-args
- #'check.fun-with-values-test/private-f
- #'check.fun-with-values-test/f-value)
- (f)
- (mount/stop))
-
-(use-fixtures :each with-fun-and-values)
-
-(deftest fun-with-values
- (is (= scalar 42))
- (is (= (fun) 42))
- (is (= with-fun 42))
- (is (= (with-partial 1) 42))
- (is (= (f-in-f 1) 42))
- (is (= f-no-args-value 42))
- (is (= (f-no-args) 42))
- (is (= (f-args 41 1) 42))
- (is (= (private-f 1) 42))
- (is (= f-value 42)))
diff --git a/test/check/parts_test.clj b/test/check/parts_test.clj
deleted file mode 100644
index 53f4b38..0000000
--- a/test/check/parts_test.clj
+++ /dev/null
@@ -1,17 +0,0 @@
-(ns check.parts-test
- (:require [mount.core :as mount :refer [defstate] :as m]
- [app.nyse :refer [conn]]
- [clojure.test :refer :all]))
-
-(defstate should-not-start :start #(constantly 42))
-
-(defn with-parts [f]
- (m/start #'app.config/app-config #'app.nyse/conn)
- (f)
- (m/stop))
-
-(use-fixtures :each with-parts)
-
-(deftest start-only-parts
- (is (instance? datomic.peer.LocalConnection conn))
- (is (instance? mount.core.NotStartedState should-not-start)))
diff --git a/test/check/private_fun_test.clj b/test/check/private_fun_test.clj
deleted file mode 100644
index 2f2c413..0000000
--- a/test/check/private_fun_test.clj
+++ /dev/null
@@ -1,14 +0,0 @@
-(ns check.private-fun-test
- (:require [mount.core :as mount :refer [defstate]]
- [check.fun-with-values-test :refer [private-f]]
- [clojure.test :refer :all]))
-
-(defn with-fun-and-values [f]
- (mount/start #'check.fun-with-values-test/private-f)
- (f)
- (mount/stop))
-
-(use-fixtures :each with-fun-and-values)
-
-(deftest fun-with-valuesj
- (is (= (private-f 1) 42)))
diff --git a/test/check/start_with_test.clj b/test/check/start_with_test.clj
deleted file mode 100644
index 8e362c0..0000000
--- a/test/check/start_with_test.clj
+++ /dev/null
@@ -1,47 +0,0 @@
-(ns check.start-with-test
- (:require [mount.core :as mount :refer [defstate]]
- [app.config :refer [app-config]]
- [app.nyse :refer [conn]]
- [app :refer [nrepl]]
- [clojure.test :refer :all]))
-
-(defstate test-conn :start 42
- :stop #(constantly 0))
-
-(defstate test-nrepl :start [])
-
-(deftest start-with
-
- (testing "should start with substitutes"
- (let [_ (mount/start-with {#'app.nyse/conn #'check.start-with-test/test-conn
- #'app/nrepl #'check.start-with-test/test-nrepl})]
- (is (map? app-config))
- (is (vector? nrepl))
- (is (= conn 42))
- (mount/stop)))
-
- (testing "should not start the substitute itself"
- (let [_ (mount/start-with {#'app.nyse/conn #'check.start-with-test/test-conn})]
- (is (instance? mount.core.NotStartedState test-conn))
- (is (= conn 42))
- (mount/stop)))
-
- (testing "should start normally after start-with"
- (let [_ (mount/start)]
- (is (map? app-config))
- (is (instance? clojure.tools.nrepl.server.Server nrepl))
- (is (instance? datomic.peer.LocalConnection conn))
- (is (= test-conn 42))
- (is (vector? test-nrepl))
- (mount/stop)))
-
- (testing "should start-without normally after start-with"
- (let [_ (mount/start-without #'check.start-with-test/test-conn
- #'check.start-with-test/test-nrepl)]
- (is (map? app-config))
- (is (instance? clojure.tools.nrepl.server.Server nrepl))
- (is (instance? datomic.peer.LocalConnection conn))
- (is (instance? mount.core.NotStartedState test-conn))
- (is (instance? mount.core.NotStartedState test-nrepl))
- (mount/stop))))
-
diff --git a/test/check/start_without_test.clj b/test/check/start_without_test.clj
deleted file mode 100644
index 1459dae..0000000
--- a/test/check/start_without_test.clj
+++ /dev/null
@@ -1,18 +0,0 @@
-(ns check.start-without-test
- (:require [mount.core :as m]
- [app.config :refer [app-config]]
- [app.nyse :refer [conn]]
- [app :refer [nrepl]]
- [clojure.test :refer :all]))
-
-(defn without [f]
- (m/start-without #'app.nyse/conn #'app/nrepl)
- (f)
- (m/stop))
-
-(use-fixtures :each without)
-
-(deftest start-without-states
- (is (map? app-config))
- (is (instance? mount.core.NotStartedState nrepl))
- (is (instance? mount.core.NotStartedState conn)))
diff --git a/test/check/stop_except_test.clj b/test/check/stop_except_test.clj
deleted file mode 100644
index b314c31..0000000
--- a/test/check/stop_except_test.clj
+++ /dev/null
@@ -1,31 +0,0 @@
-(ns check.stop-except-test
- (:require [mount.core :as mount :refer [defstate]]
- [app.config :refer [app-config]]
- [app.nyse :refer [conn]]
- [app :refer [nrepl]]
- [clojure.test :refer :all]))
-
-(deftest stop-except
-
- (testing "should stop all except nrepl"
- (let [_ (mount/start)
- _ (mount/stop-except #'app.nyse/conn #'app.config/app-config)]
- (is (map? app-config))
- (is (instance? datomic.peer.LocalConnection conn))
- (is (instance? mount.core.NotStartedState nrepl))
- (mount/stop)))
-
- (testing "should start normally after stop-except"
- (let [_ (mount/start)]
- (is (map? app-config))
- (is (instance? clojure.tools.nrepl.server.Server nrepl))
- (is (instance? datomic.peer.LocalConnection conn))
- (mount/stop)))
-
- (testing "should stop all normally after stop-except"
- (let [_ (mount/start)
- _ (mount/stop-except #'app.nyse/conn #'app.config/app-config)
- _ (mount/stop)]
- (is (instance? mount.core.NotStartedState app-config))
- (is (instance? mount.core.NotStartedState conn))
- (is (instance? mount.core.NotStartedState nrepl)))))
diff --git a/test/check/suspend_resume_test.clj b/test/check/suspend_resume_test.clj
deleted file mode 100644
index eb37ace..0000000
--- a/test/check/suspend_resume_test.clj
+++ /dev/null
@@ -1,141 +0,0 @@
-(ns check.suspend-resume-test
- (:require [mount.core :as mount :refer [defstate]]
- [app.config :refer [app-config]]
- [app.nyse :refer [conn]]
- [app :refer [nrepl]]
- [clojure.test :refer :all]))
-
-(defn koncat [k s]
- (-> (name k)
- (str "-" (name s))
- keyword))
-
-(defn start [s] (koncat s :started))
-(defn stop [s] (koncat s :stopped))
-(defn suspend [s] (koncat s :suspended))
-(defn resume [s] (koncat s :resumed))
-
-(defstate web-server :start (start :w)
- :stop (stop :w)
- :suspend (suspend :w)
- :resume (resume :w))
-
-(defstate q-listener :start (start :q)
- :stop (stop :q)
- :suspend (suspend :q)
- :resume (resume :q))
-
-(defstate randomizer :start (rand-int 42))
-
-(deftest suspendable-lifecycle
-
- (testing "should suspend _only suspendable_ states that are currently started"
- (let [_ (mount/start)
- _ (mount/suspend)]
- (is (map? app-config))
- (is (instance? clojure.tools.nrepl.server.Server nrepl))
- (is (instance? datomic.peer.LocalConnection conn))
- (is (= web-server :w-suspended))
- (mount/stop)))
-
- (testing "should resume _only suspendable_ states that are currently suspended"
- (let [_ (mount/start)
- _ (mount/stop #'app/nrepl)
- _ (mount/suspend)
- _ (mount/resume)]
- (is (map? app-config))
- (is (instance? mount.core.NotStartedState nrepl))
- (is (instance? datomic.peer.LocalConnection conn))
- (is (= web-server :w-resumed))
- (mount/stop)))
-
- (testing "should start all the states, except the ones that are currently suspended, should resume them instead"
- (let [_ (mount/start)
- _ (mount/suspend)
- _ (mount/start)]
- (is (map? app-config))
- (is (instance? clojure.tools.nrepl.server.Server nrepl))
- (is (instance? datomic.peer.LocalConnection conn))
- (is (= web-server :w-resumed))
- (mount/stop)))
-
- (testing "should stop all: started and suspended"
- (let [_ (mount/start)
- _ (mount/suspend)
- _ (mount/stop)]
- (is (instance? mount.core.NotStartedState app-config))
- (is (instance? mount.core.NotStartedState nrepl))
- (is (instance? mount.core.NotStartedState conn))
- (is (instance? mount.core.NotStartedState web-server)))))
-
-
-(deftest suspendable-start-with
-
- (testing "when replacing a non suspendable state with a suspendable one,
- the later should be able to suspend/resume,
- the original should not be suspendable after resume and preserve its lifecycle fns after rollback/stop"
- (let [_ (mount/start-with {#'app/nrepl #'check.suspend-resume-test/web-server})
- _ (mount/suspend)]
- (is (= nrepl :w-suspended))
- (is (instance? mount.core.NotStartedState web-server))
- (mount/stop)
- (mount/start)
- (mount/suspend)
- (is (instance? clojure.tools.nrepl.server.Server nrepl))
- (is (= web-server :w-suspended))
- (mount/stop)))
-
- ;; this is a messy use case, but can still happen especially at REPL time
- ;; it also messy, because usually :stop function refers the _original_ state by name (i.e. #(disconnect conn))
- ;; (unchanged/not substituted in its lexical scope), and original state won't be started
- (testing "when replacing a suspendable state with a non suspendable one,
- the later should not be suspendable,
- the original should still be suspendable and preserve its lifecycle fns after the rollback/stop"
- (let [_ (mount/start-with {#'check.suspend-resume-test/web-server #'check.suspend-resume-test/randomizer})
- _ (mount/suspend)]
- (is (integer? web-server))
- (is (instance? mount.core.NotStartedState randomizer))
- (mount/stop)
- (mount/start)
- (mount/suspend)
- (is (integer? randomizer))
- (is (= web-server :w-suspended))
- (mount/stop)))
-
- ;; this is a messy use case, but can still happen especially at REPL time
- (testing "when replacing a suspended state with a non suspendable started one,
- the later should not be suspendable,
- the original should still be suspended and preserve its lifecycle fns after the rollback/stop"
- (let [_ (mount/start)
- _ (mount/suspend)
- _ (mount/start-with {#'check.suspend-resume-test/web-server #'app.nyse/conn}) ;; TODO: good to WARN on started states during "start-with"
- _ (mount/suspend)]
- (is (instance? datomic.peer.LocalConnection conn))
- (is (= web-server :w-suspended)) ;; since the "conn" does not have a resume method, so web-server was not started
- (mount/stop)
- (mount/start)
- (mount/suspend)
- (is (instance? datomic.peer.LocalConnection conn))
- (is (= web-server :w-suspended))
- (mount/stop)))
-
- ;; this is a messy use case, but can still happen especially at REPL time
- (testing "when replacing a suspended state with a suspendable one,
- the later should be suspendable,
- the original should still be suspended and preserve its lifecycle fns after the rollback/stop"
- (let [_ (mount/start)
- _ (mount/suspend)
- _ (mount/start-with {#'check.suspend-resume-test/web-server #'check.suspend-resume-test/q-listener})] ;; TODO: good to WARN on started states during "start-with"
- (is (= q-listener :q-suspended))
- (is (= web-server :q-resumed))
- (mount/suspend)
- (is (= q-listener :q-suspended))
- (is (= web-server :q-suspended))
- (mount/stop)
- (is (instance? mount.core.NotStartedState web-server))
- (is (instance? mount.core.NotStartedState q-listener))
- (mount/start)
- (mount/suspend)
- (is (= q-listener :q-suspended))
- (is (= web-server :w-suspended))
- (mount/stop))))
diff --git a/test/clj/app/conf.clj b/test/clj/app/conf.clj
new file mode 100644
index 0000000..3273a95
--- /dev/null
+++ b/test/clj/app/conf.clj
@@ -0,0 +1,13 @@
+(ns app.conf
+ (:require [mount.core :as mount :refer [defstate]]
+ [clojure.edn :as edn]
+ [clojure.tools.logging :refer [info]]))
+
+(defn load-config [path]
+ (info "loading config from" path)
+ (-> path
+ slurp
+ edn/read-string))
+
+(defstate config
+ :start (load-config "dev/resources/config.edn"))
diff --git a/test/clj/app/example.clj b/test/clj/app/example.clj
new file mode 100644
index 0000000..028d406
--- /dev/null
+++ b/test/clj/app/example.clj
@@ -0,0 +1,65 @@
+(ns app.example
+ (:require [datomic.api :as d]
+ [clojure.tools.nrepl.server :refer [start-server stop-server]]
+ [mount.core :as mount :refer [defstate]]
+ [app.utils.datomic :refer [touch]]
+ [app.conf :refer [config]]
+ [app.nyse :as nyse]))
+
+;; example on creating a network REPL
+(defn- start-nrepl [{:keys [host port]}]
+ (start-server :bind host :port port))
+
+;; nREPL is just another simple state
+(defstate nrepl :start (start-nrepl (:nrepl @config))
+ :stop (stop-server @nrepl))
+
+;; datomic schema
+(defn create-schema [conn]
+ (let [schema [{:db/id #db/id [:db.part/db]
+ :db/ident :order/symbol
+ :db/valueType :db.type/string
+ :db/cardinality :db.cardinality/one
+ :db/index true
+ :db.install/_attribute :db.part/db}
+
+ {:db/id #db/id [:db.part/db]
+ :db/ident :order/bid
+ :db/valueType :db.type/bigdec
+ :db/cardinality :db.cardinality/one
+ :db.install/_attribute :db.part/db}
+
+ {:db/id #db/id [:db.part/db]
+ :db/ident :order/qty
+ :db/valueType :db.type/long
+ :db/cardinality :db.cardinality/one
+ :db.install/_attribute :db.part/db}
+
+ {:db/id #db/id [:db.part/db]
+ :db/ident :order/offer
+ :db/valueType :db.type/bigdec
+ :db/cardinality :db.cardinality/one
+ :db.install/_attribute :db.part/db}]]
+
+ @(d/transact conn schema)))
+
+(defn add-order [ticker bid offer qty] ;; can take connection as param
+ @(d/transact @nyse/conn [{:db/id (d/tempid :db.part/user)
+ :order/symbol ticker
+ :order/bid bid
+ :order/offer offer
+ :order/qty qty}]))
+
+
+(defn find-orders [ticker] ;; can take connection as param
+ (let [orders (d/q '[:find ?e :in $ ?ticker
+ :where [?e :order/symbol ?ticker]]
+ (d/db @nyse/conn) ticker)]
+ (touch @nyse/conn orders)))
+
+(defn create-nyse-schema []
+ (create-schema @nyse/conn))
+
+;; example of an app entry point
+(defn -main [& args]
+ (mount/start))
diff --git a/test/clj/app/nyse.clj b/test/clj/app/nyse.clj
new file mode 100644
index 0000000..374cba7
--- /dev/null
+++ b/test/clj/app/nyse.clj
@@ -0,0 +1,21 @@
+(ns app.nyse
+ (:require [mount.core :as mount :refer [defstate]]
+ [datomic.api :as d]
+ [clojure.tools.logging :refer [info]]
+ [app.conf :refer [config]]))
+
+(defn- new-connection [conf]
+ (info "conf: " conf)
+ (let [uri (get-in @conf [:datomic :uri])]
+ (info "creating a connection to datomic:" uri)
+ (d/create-database uri)
+ (d/connect uri)))
+
+(defn disconnect [conf conn]
+ (let [uri (get-in @conf [:datomic :uri])]
+ (info "disconnecting from " uri)
+ (.release @conn) ;; usually it's not released, here just to illustrate the access to connection on (stop)
+ (d/delete-database uri)))
+
+(defstate conn :start (new-connection config)
+ :stop (disconnect config conn))
diff --git a/test/clj/app/utils/datomic.clj b/test/clj/app/utils/datomic.clj
new file mode 100644
index 0000000..836f9b9
--- /dev/null
+++ b/test/clj/app/utils/datomic.clj
@@ -0,0 +1,11 @@
+(ns app.utils.datomic
+ (:require [datomic.api :as d]))
+
+(defn entity [conn id]
+ (d/entity (d/db conn) id))
+
+(defn touch [conn results]
+ "takes 'entity ids' results from a query
+ e.g. '#{[272678883689461] [272678883689462] [272678883689459] [272678883689457]}'"
+ (let [e (partial entity conn)]
+ (map #(-> % first e d/touch) results)))
diff --git a/test/clj/app/utils/logging.clj b/test/clj/app/utils/logging.clj
new file mode 100644
index 0000000..2d8c64d
--- /dev/null
+++ b/test/clj/app/utils/logging.clj
@@ -0,0 +1,46 @@
+(ns app.utils.logging ;; << change to your namespace/path
+ (:require [mount.core]
+ [robert.hooke :refer [add-hook clear-hooks]]
+ [clojure.string :refer [split]]
+ [clojure.tools.logging :refer [info]]))
+
+(alter-meta! *ns* assoc ::load false)
+
+(defn- f-to-action [f]
+ (let [fname (-> (str f)
+ (split #"@")
+ first)]
+ (case fname
+ "mount.core$up" :up
+ "mount.core$down" :down
+ "mount.core$sigstop" :suspend
+ "mount.core$sigcont" :resume
+ :noop)))
+
+(defn whatcha-doing? [{:keys [status suspend]} action]
+ (case action
+ :up (if (status :suspended) ">> resuming"
+ (if-not (status :started) ">> starting"))
+ :down (if (or (status :started) (status :suspended)) "<< stopping")
+ :suspend (if (and (status :started) suspend) "<< suspending")
+ :resume (if (status :suspended) ">> resuming")))
+
+(defn log-status [f & args]
+ (let [{:keys [var] :as state} (second args)
+ action (f-to-action f)]
+ (when-let [taking-over-the-world (whatcha-doing? state action)]
+ (info (str taking-over-the-world ".. " var)))
+ (apply f args)))
+
+(defonce lifecycle-fns
+ #{#'mount.core/up
+ #'mount.core/down
+ #'mount.core/sigstop
+ #'mount.core/sigcont})
+
+(defn without-logging-status []
+ (doall (map #(clear-hooks %) lifecycle-fns)))
+
+(defn with-logging-status []
+ (without-logging-status)
+ (doall (map #(add-hook % log-status) lifecycle-fns)))
diff --git a/test/mount/test.cljc b/test/mount/test.cljc
new file mode 100644
index 0000000..b24368d
--- /dev/null
+++ b/test/mount/test.cljc
@@ -0,0 +1,35 @@
+(ns mount.test
+ (:require
+ #?@(:cljs [[cljs.test :as t]
+ [doo.runner :refer-macros [doo-tests]]]
+ :clj [[clojure.test :as t]])
+ mount.core
+
+ mount.test.fun-with-values
+ mount.test.private-fun
+ mount.test.parts
+ mount.test.cleanup-dirty-states
+ mount.test.stop-except
+ mount.test.start-without
+ mount.test.start-with
+ mount.test.suspend-resume
+ ))
+
+(mount.core/in-cljc-mode)
+
+#?(:cljs
+
+ ;; (doo.runner/do-all-tests)
+ (doo-tests
+ 'mount.test.fun-with-values
+ 'mount.test.private-fun
+ 'mount.test.parts
+ 'mount.test.cleanup-dirty-states
+ 'mount.test.stop-except
+ 'mount.test.start-without
+ 'mount.test.start-with
+ 'mount.test.suspend-resume
+ ))
+
+(defn run-tests []
+ (t/run-all-tests #"mount.test.*"))
diff --git a/test/mount/test/cleanup_dirty_states.cljc b/test/mount/test/cleanup_dirty_states.cljc
new file mode 100644
index 0000000..c4fced9
--- /dev/null
+++ b/test/mount/test/cleanup_dirty_states.cljc
@@ -0,0 +1,34 @@
+(ns mount.test.cleanup-dirty-states
+ (:require
+ #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
+ [mount.core :as mount :refer-macros [defstate]]
+ [app.websockets :refer [system-a]]
+ [app.conf :refer [config]]
+ [app.audit-log :refer [log]]]
+ :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
+ [mount.core :as mount :refer [defstate]]
+ [app.example]])
+ [mount.test.helper :refer [dval helper forty-two]]))
+
+#?(:clj
+ (deftest cleanup-dirty-states
+ (let [_ (mount/start)]
+ (is (not (.isClosed (:server-socket (dval app.example/nrepl)))))
+ (require 'app.example :reload)
+ (mount/start) ;; should not result in "BindException Address already in use" since the clean up will stop the previous instance
+ (is (not (.isClosed (:server-socket (dval app.example/nrepl)))))
+ (mount/stop)
+ (is (instance? mount.core.NotStartedState (dval app.example/nrepl))))))
+
+#?(:cljs
+ (deftest cleanup-dirty-states
+ (let [_ (mount/start #'mount.test.helper/helper)]
+ (is (= :started (dval helper)))
+ (is (= 42 @forty-two))
+ (.require js/goog "mount.test.helper") ;; should have run :stop of `helper`
+ ;; (is (= :cleaned @forty-two)) ;; TODO: figure out how to reload a namespace properly
+ ;; (is (instance? mount.core.NotStartedState (dval helper)))
+ (mount/start #'mount.test.helper/helper)
+ (is (= :started (dval helper)))
+ (mount/stop)
+ (is (instance? mount.core.NotStartedState (dval helper))))))
diff --git a/test/mount/test/fun_with_values.cljc b/test/mount/test/fun_with_values.cljc
new file mode 100644
index 0000000..61cf200
--- /dev/null
+++ b/test/mount/test/fun_with_values.cljc
@@ -0,0 +1,59 @@
+(ns mount.test.fun-with-values
+ (:require
+ #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
+ [mount.core :as mount :refer-macros [defstate]]]
+ :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
+ [mount.core :as mount :refer [defstate]]])))
+
+(defn f [n]
+ (fn [m]
+ (+ n m)))
+
+(defn g [a b]
+ (+ a b))
+
+(defn- pf [n]
+ (+ 41 n))
+
+(defn fna []
+ 42)
+
+(defstate scalar :start 42)
+(defstate fun :start #(inc 41))
+(defstate with-fun :start (inc 41))
+(defstate with-partial :start (partial g 41))
+(defstate f-in-f :start (f 41))
+(defstate f-no-args-value :start (fna))
+(defstate f-no-args :start fna)
+(defstate f-args :start g)
+(defstate f-value :start (g 41 1))
+(defstate private-f :start pf)
+
+(defn start-states []
+ (mount/start #'mount.test.fun-with-values/scalar
+ #'mount.test.fun-with-values/fun
+ #'mount.test.fun-with-values/with-fun
+ #'mount.test.fun-with-values/with-partial
+ #'mount.test.fun-with-values/f-in-f
+ #'mount.test.fun-with-values/f-args
+ #'mount.test.fun-with-values/f-no-args-value
+ #'mount.test.fun-with-values/f-no-args
+ #'mount.test.fun-with-values/private-f
+ #'mount.test.fun-with-values/f-value))
+
+(use-fixtures :once
+ #?(:cljs {:before start-states
+ :after mount/stop}
+ :clj #((start-states) (%) (mount/stop))))
+
+(deftest fun-with-values
+ (is (= @scalar 42))
+ (is (= (@fun) 42))
+ (is (= @with-fun 42))
+ (is (= (@with-partial 1) 42))
+ (is (= (@f-in-f 1) 42))
+ (is (= @f-no-args-value 42))
+ (is (= (@f-no-args) 42))
+ (is (= (@f-args 41 1) 42))
+ (is (= (@private-f 1) 42))
+ (is (= @f-value 42)))
diff --git a/test/mount/test/helper.cljc b/test/mount/test/helper.cljc
new file mode 100644
index 0000000..ecc0cff
--- /dev/null
+++ b/test/mount/test/helper.cljc
@@ -0,0 +1,18 @@
+(ns mount.test.helper
+ (:require
+ #?@(:cljs [[mount.core :as mount :refer-macros [defstate]]]
+ :clj [[mount.core :as mount :refer [defstate]]])))
+
+(defn dval
+ "returns a value of DerefableState without deref'ing it"
+ [d]
+ (-> (@@(var mount.core/meta-state)
+ #?(:clj (.name d)
+ :cljs (.-name d)))
+ :inst
+ deref))
+
+(def forty-two (atom 42))
+
+(defstate helper :start :started
+ :stop (reset! forty-two :cleaned))
diff --git a/test/mount/test/parts.cljc b/test/mount/test/parts.cljc
new file mode 100644
index 0000000..20a503f
--- /dev/null
+++ b/test/mount/test/parts.cljc
@@ -0,0 +1,36 @@
+(ns mount.test.parts
+ (:require
+ #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
+ [mount.core :as mount :refer-macros [defstate]]
+ [app.websockets :refer [system-a]]
+ [app.conf :refer [config]]
+ [app.audit-log :refer [log]]]
+ :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
+ [mount.core :as mount :refer [defstate]]
+ [app.nyse :refer [conn]]])
+ [mount.test.helper :refer [dval]]))
+
+(defstate should-not-start :start (constantly 42))
+
+#?(:clj
+ (defn with-parts [f]
+ (mount/start #'app.conf/config #'app.nyse/conn)
+ (f)
+ (mount/stop)))
+
+(use-fixtures :once
+ #?(:cljs {:before #(mount/start #'app.conf/config #'app.audit-log/log)
+ :after mount/stop}
+ :clj with-parts))
+
+#?(:clj
+ (deftest start-only-parts
+ (is (instance? datomic.peer.LocalConnection (dval conn)))
+ (is (instance? mount.core.NotStartedState (dval should-not-start)))))
+
+#?(:cljs
+ (deftest start-only-parts
+ (is (instance? datascript.db/DB @(dval log)))
+ (is (map? (dval config)))
+ (is (instance? mount.core.NotStartedState (dval should-not-start)))
+ (is (instance? mount.core.NotStartedState (dval system-a)))))
diff --git a/test/mount/test/private_fun.cljc b/test/mount/test/private_fun.cljc
new file mode 100644
index 0000000..47ef9a4
--- /dev/null
+++ b/test/mount/test/private_fun.cljc
@@ -0,0 +1,18 @@
+(ns mount.test.private-fun
+ (:require
+ #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
+ [mount.core :as mount :refer-macros [defstate]]]
+ :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
+ [mount.core :as mount :refer [defstate]]])
+
+ [mount.test.fun-with-values :refer [private-f]]))
+
+(use-fixtures :once
+ #?(:cljs {:before #(mount/start #'mount.test.fun-with-values/private-f)
+ :after mount/stop}
+ :clj #((mount/start #'mount.test.fun-with-values/private-f)
+ (%)
+ (mount/stop))))
+
+(deftest fun-with-values
+ (is (= (@private-f 1) 42)))
diff --git a/test/mount/test/start_with.cljc b/test/mount/test/start_with.cljc
new file mode 100644
index 0000000..7e7a7bd
--- /dev/null
+++ b/test/mount/test/start_with.cljc
@@ -0,0 +1,93 @@
+(ns mount.test.start-with
+ (:require
+ #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
+ [mount.core :as mount :refer-macros [defstate]]
+ [app.websockets :refer [system-a]]
+ [app.conf :refer [config]]
+ [app.audit-log :refer [log]]]
+ :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
+ [mount.core :as mount :refer [defstate]]
+ [app.conf :refer [config]]
+ [app.nyse :refer [conn]]
+ [app.example :refer [nrepl]]])
+ [mount.test.helper :refer [dval helper]]))
+
+(defstate test-conn :start 42
+ :stop (constantly 0))
+
+(defstate test-nrepl :start [])
+
+#?(:cljs
+ (deftest start-with
+
+ (testing "should start with substitutes"
+ (let [_ (mount/start-with {#'app.websockets/system-a #'mount.test.start-with/test-conn
+ #'mount.test.helper/helper #'mount.test.start-with/test-nrepl})]
+ (is (map? (dval config)))
+ (is (vector? (dval helper)))
+ (is (= (dval system-a) 42))
+ (is (instance? datascript.db/DB @(dval log)))
+ (mount/stop)))
+
+ (testing "should not start the substitute itself"
+ (let [_ (mount/start-with {#'app.websockets/system-a #'mount.test.start-with/test-conn})]
+ (is (instance? mount.core.NotStartedState (dval test-conn)))
+ (is (= 42 (dval system-a)))
+ (mount/stop)))
+
+ (testing "should start normally after start-with"
+ (let [_ (mount/start)]
+ (is (map? (dval config)))
+ (is (instance? datascript.db/DB @(dval log)))
+ (is (instance? js/WebSocket (dval system-a)))
+ (is (= 42 (dval test-conn)))
+ (is (vector? (dval test-nrepl)))
+ (is (= :started (dval helper)))
+ (mount/stop)))
+
+ (testing "should start-without normally after start-with"
+ (let [_ (mount/start-without #'mount.test.start-with/test-conn
+ #'mount.test.start-with/test-nrepl)]
+ (is (map? (dval config)))
+ (is (instance? datascript.db/DB @(dval log)))
+ (is (instance? js/WebSocket (dval system-a)))
+ (is (= :started (dval helper)))
+ (is (instance? mount.core.NotStartedState (dval test-conn)))
+ (is (instance? mount.core.NotStartedState (dval test-nrepl)))
+ (mount/stop)))))
+
+#?(:clj
+ (deftest start-with
+
+ (testing "should start with substitutes"
+ (let [_ (mount/start-with {#'app.nyse/conn #'mount.test.start-with/test-conn
+ #'app.example/nrepl #'mount.test.start-with/test-nrepl})]
+ (is (map? (dval config)))
+ (is (vector? (dval nrepl)))
+ (is (= (dval conn) 42))
+ (mount/stop)))
+
+ (testing "should not start the substitute itself"
+ (let [_ (mount/start-with {#'app.nyse/conn #'mount.test.start-with/test-conn})]
+ (is (instance? mount.core.NotStartedState (dval test-conn)))
+ (is (= (dval conn) 42))
+ (mount/stop)))
+
+ (testing "should start normally after start-with"
+ (let [_ (mount/start)]
+ (is (map? (dval config)))
+ (is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
+ (is (instance? datomic.peer.LocalConnection (dval conn)))
+ (is (= (dval test-conn) 42))
+ (is (vector? (dval test-nrepl)))
+ (mount/stop)))
+
+ (testing "should start-without normally after start-with"
+ (let [_ (mount/start-without #'mount.test.start-with/test-conn
+ #'mount.test.start-with/test-nrepl)]
+ (is (map? (dval config)))
+ (is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
+ (is (instance? datomic.peer.LocalConnection (dval conn)))
+ (is (instance? mount.core.NotStartedState (dval test-conn)))
+ (is (instance? mount.core.NotStartedState (dval test-nrepl)))
+ (mount/stop)))))
diff --git a/test/mount/test/start_without.cljc b/test/mount/test/start_without.cljc
new file mode 100644
index 0000000..f6a68d1
--- /dev/null
+++ b/test/mount/test/start_without.cljc
@@ -0,0 +1,37 @@
+(ns mount.test.start-without
+ (:require
+ #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
+ [mount.core :as mount :refer-macros [defstate]]
+ [app.websockets :refer [system-a]]
+ [app.conf :refer [config]]
+ [app.audit-log :refer [log]]]
+ :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
+ [mount.core :as mount :refer [defstate]]
+ [app.conf :refer [config]]
+ [app.nyse :refer [conn]]
+ [app.example :refer [nrepl]]])
+ [mount.test.helper :refer [dval helper]]))
+
+#?(:clj
+ (defn without [f]
+ (mount/start-without #'app.nyse/conn #'app.example/nrepl)
+ (f)
+ (mount/stop)))
+
+ (use-fixtures :once
+ #?(:cljs {:before #(mount/start-without #'mount.test.helper/helper #'app.websockets/system-a)
+ :after mount/stop}
+ :clj without))
+
+#?(:clj
+ (deftest start-without-states
+ (is (map? (dval config)))
+ (is (instance? mount.core.NotStartedState (dval nrepl)))
+ (is (instance? mount.core.NotStartedState (dval conn)))))
+
+#?(:cljs
+ (deftest start-without-states
+ (is (map? (dval config)))
+ (is (instance? datascript.db/DB @(dval log)))
+ (is (instance? mount.core.NotStartedState (dval helper)))
+ (is (instance? mount.core.NotStartedState (dval system-a)))))
diff --git a/test/mount/test/stop_except.cljc b/test/mount/test/stop_except.cljc
new file mode 100644
index 0000000..3e61f97
--- /dev/null
+++ b/test/mount/test/stop_except.cljc
@@ -0,0 +1,66 @@
+(ns mount.test.stop-except
+ (:require
+ #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
+ [mount.core :as mount :refer-macros [defstate]]
+ [app.websockets :refer [system-a]]
+ [app.conf :refer [config]]
+ [app.audit-log :refer [log]]]
+ :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
+ [mount.core :as mount :refer [defstate]]
+ [app.conf :refer [config]]
+ [app.nyse :refer [conn]]
+ [app.example :refer [nrepl]]])
+ [mount.test.helper :refer [dval helper]]))
+
+#?(:cljs
+ (deftest stop-except
+
+ (testing "should stop all except nrepl"
+ (let [_ (mount/start)
+ _ (mount/stop-except #'app.audit-log/log #'mount.test.helper/helper)]
+ (is (= :started (dval helper)))
+ (is (instance? datascript.db/DB @(dval log)))
+ (is (instance? mount.core.NotStartedState (dval config)))
+ (is (instance? mount.core.NotStartedState (dval system-a)))
+ (mount/stop)))
+
+ (testing "should start normally after stop-except"
+ (let [_ (mount/start)]
+ (is (map? (dval config)))
+ (is (instance? js/WebSocket (dval system-a)))
+ (is (instance? datascript.db/DB @(dval log)))
+ (mount/stop)))
+
+ (testing "should stop all normally after stop-except"
+ (let [_ (mount/start)
+ _ (mount/stop-except #'app.audit-log/log #'mount.test.helper/helper)
+ _ (mount/stop)]
+ (is (instance? mount.core.NotStartedState (dval config)))
+ (is (instance? mount.core.NotStartedState (dval log)))
+ (is (instance? mount.core.NotStartedState (dval system-a)))))))
+
+#?(:clj
+ (deftest stop-except
+
+ (testing "should stop all except nrepl"
+ (let [_ (mount/start)
+ _ (mount/stop-except #'app.nyse/conn #'app.conf/config)]
+ (is (map? (dval config)))
+ (is (instance? datomic.peer.LocalConnection (dval conn)))
+ (is (instance? mount.core.NotStartedState (dval nrepl)))
+ (mount/stop)))
+
+ (testing "should start normally after stop-except"
+ (let [_ (mount/start)]
+ (is (map? (dval config)))
+ (is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
+ (is (instance? datomic.peer.LocalConnection (dval conn)))
+ (mount/stop)))
+
+ (testing "should stop all normally after stop-except"
+ (let [_ (mount/start)
+ _ (mount/stop-except #'app.nyse/conn #'app.conf/config)
+ _ (mount/stop)]
+ (is (instance? mount.core.NotStartedState (dval config)))
+ (is (instance? mount.core.NotStartedState (dval conn)))
+ (is (instance? mount.core.NotStartedState (dval nrepl)))))))
diff --git a/test/mount/test/suspend_resume.cljc b/test/mount/test/suspend_resume.cljc
new file mode 100644
index 0000000..ea1fe36
--- /dev/null
+++ b/test/mount/test/suspend_resume.cljc
@@ -0,0 +1,210 @@
+(ns mount.test.suspend-resume
+ (:require
+ #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
+ [mount.core :as mount :refer-macros [defstate]]
+ [app.websockets :refer [system-a]]
+ [app.conf :refer [config]]
+ [app.audit-log :refer [log]]]
+ :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
+ [mount.core :as mount :refer [defstate]]
+ [app.conf :refer [config]]
+ [app.nyse :refer [conn]]
+ [app.example :refer [nrepl]]])
+ [mount.test.helper :refer [dval]]))
+
+(defn koncat [k s]
+ (-> (name k)
+ (str "-" (name s))
+ keyword))
+
+(defn start [s] (koncat s :started))
+(defn stop [s] (koncat s :stopped))
+(defn suspend [s] (koncat s :suspended))
+(defn resume [s] (koncat s :resumed))
+
+(defstate web-server :start (start :w)
+ :stop (stop :w)
+ :suspend (suspend :w)
+ :resume (resume :w))
+
+(defstate q-listener :start (start :q)
+ :stop (stop :q)
+ :suspend (suspend :q)
+ :resume (resume :q))
+
+(defstate randomizer :start (rand-int 42))
+
+#?(:cljs
+ (deftest suspendable-lifecycle
+
+ (testing "should suspend _only suspendable_ states that are currently started"
+ (let [_ (mount/start)
+ _ (mount/suspend)]
+ (is (map? (dval config)))
+ (is (instance? datascript.db/DB @(dval log)))
+ (is (instance? js/WebSocket (dval system-a)))
+ (is (= (dval web-server) :w-suspended))
+ (mount/stop)))
+
+ (testing "should resume _only suspendable_ states that are currently suspended"
+ (let [_ (mount/start)
+ _ (mount/stop #'app.websockets/system-a)
+ _ (mount/suspend)
+ _ (mount/resume)]
+ (is (map? (dval config)))
+ (is (instance? mount.core.NotStartedState (dval system-a)))
+ (is (instance? datascript.db/DB @(dval log)))
+ (is (= (dval web-server) :w-resumed))
+ (mount/stop)))
+
+ (testing "should start all the states, except the ones that are currently suspended, should resume them instead"
+ (let [_ (mount/start)
+ _ (mount/suspend)
+ _ (mount/start)]
+ (is (map? (dval config)))
+ (is (instance? js/WebSocket (dval system-a)))
+ (is (instance? datascript.db/DB @(dval log)))
+ (is (= (dval web-server) :w-resumed))
+ (mount/stop)))
+
+ (testing "should stop all: started and suspended"
+ (let [_ (mount/start)
+ _ (mount/suspend)
+ _ (mount/stop)]
+ (is (instance? mount.core.NotStartedState (dval config)))
+ (is (instance? mount.core.NotStartedState (dval system-a)))
+ (is (instance? mount.core.NotStartedState (dval log)))
+ (is (instance? mount.core.NotStartedState (dval web-server)))))))
+
+#?(:cljs
+ (deftest suspendable-start-with
+
+ (testing "when replacing a non suspendable state with a suspendable one,
+ the later should be able to suspend/resume,
+ the original should not be suspendable after resume and preserve its lifecycle fns after rollback/stop"
+ (let [_ (mount/start-with {#'app.websockets/system-a #'mount.test.suspend-resume/web-server})
+ _ (mount/suspend)]
+ (is (= (dval system-a) :w-suspended))
+ (is (instance? mount.core.NotStartedState (dval web-server)))
+ (mount/stop)
+ (mount/start)
+ (mount/suspend)
+ (is (instance? js/WebSocket (dval system-a)))
+ (is (= (dval web-server) :w-suspended))
+ (mount/stop)))))
+
+#?(:clj
+ (deftest suspendable-lifecycle
+
+ (testing "should suspend _only suspendable_ states that are currently started"
+ (let [_ (mount/start)
+ _ (mount/suspend)]
+ (is (map? (dval config)))
+ (is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
+ (is (instance? datomic.peer.LocalConnection (dval conn)))
+ (is (= (dval web-server) :w-suspended))
+ (mount/stop)))
+
+ (testing "should resume _only suspendable_ states that are currently suspended"
+ (let [_ (mount/start)
+ _ (mount/stop #'app.example/nrepl)
+ _ (mount/suspend)
+ _ (mount/resume)]
+ (is (map? (dval config)))
+ (is (instance? mount.core.NotStartedState (dval nrepl)))
+ (is (instance? datomic.peer.LocalConnection (dval conn)))
+ (is (= (dval web-server) :w-resumed))
+ (mount/stop)))
+
+ (testing "should start all the states, except the ones that are currently suspended, should resume them instead"
+ (let [_ (mount/start)
+ _ (mount/suspend)
+ _ (mount/start)]
+ (is (map? (dval config)))
+ (is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
+ (is (instance? datomic.peer.LocalConnection (dval conn)))
+ (is (= (dval web-server) :w-resumed))
+ (mount/stop)))
+
+ (testing "should stop all: started and suspended"
+ (let [_ (mount/start)
+ _ (mount/suspend)
+ _ (mount/stop)]
+ (is (instance? mount.core.NotStartedState (dval config)))
+ (is (instance? mount.core.NotStartedState (dval nrepl)))
+ (is (instance? mount.core.NotStartedState (dval conn)))
+ (is (instance? mount.core.NotStartedState (dval web-server)))))))
+
+
+#?(:clj
+ (deftest suspendable-start-with
+
+ (testing "when replacing a non suspendable state with a suspendable one,
+ the later should be able to suspend/resume,
+ the original should not be suspendable after resume and preserve its lifecycle fns after rollback/stop"
+ (let [_ (mount/start-with {#'app.example/nrepl #'mount.test.suspend-resume/web-server})
+ _ (mount/suspend)]
+ (is (= (dval nrepl) :w-suspended))
+ (is (instance? mount.core.NotStartedState (dval web-server)))
+ (mount/stop)
+ (mount/start)
+ (mount/suspend)
+ (is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
+ (is (= (dval web-server) :w-suspended))
+ (mount/stop)))
+
+ ;; this is a messy use case, but can still happen especially at REPL time
+ ;; it also messy, because usually :stop function refers the _original_ state by name (i.e. #(disconnect conn))
+ ;; (unchanged/not substituted in its lexical scope), and original state won't be started
+ (testing "when replacing a suspendable state with a non suspendable one,
+ the later should not be suspendable,
+ the original should still be suspendable and preserve its lifecycle fns after the rollback/stop"
+ (let [_ (mount/start-with {#'mount.test.suspend-resume/web-server #'mount.test.suspend-resume/randomizer})
+ _ (mount/suspend)]
+ (is (integer? (dval web-server)))
+ (is (instance? mount.core.NotStartedState (dval randomizer)))
+ (mount/stop)
+ (mount/start)
+ (mount/suspend)
+ (is (integer? (dval randomizer)))
+ (is (= (dval web-server) :w-suspended))
+ (mount/stop)))
+
+ ;; this is a messy use case, but can still happen especially at REPL time
+ (testing "when replacing a suspended state with a non suspendable started one,
+ the later should not be suspendable,
+ the original should still be suspended and preserve its lifecycle fns after the rollback/stop"
+ (let [_ (mount/start)
+ _ (mount/suspend)
+ _ (mount/start-with {#'mount.test.suspend-resume/web-server #'app.nyse/conn}) ;; TODO: good to WARN on started states during "start-with"
+ _ (mount/suspend)]
+ (is (instance? datomic.peer.LocalConnection (dval conn)))
+ (is (= (dval web-server) :w-suspended)) ;; since the "conn" does not have a resume method, so web-server was not started
+ (mount/stop)
+ (mount/start)
+ (mount/suspend)
+ (is (instance? datomic.peer.LocalConnection (dval conn)))
+ (is (= (dval web-server) :w-suspended))
+ (mount/stop)))
+
+ ;; this is a messy use case, but can still happen especially at REPL time
+ (testing "when replacing a suspended state with a suspendable one,
+ the later should be suspendable,
+ the original should still be suspended and preserve its lifecycle fns after the rollback/stop"
+ (let [_ (mount/start)
+ _ (mount/suspend)
+ _ (mount/start-with {#'mount.test.suspend-resume/web-server
+ #'mount.test.suspend-resume/q-listener})] ;; TODO: good to WARN on started states during "start-with"
+ (is (= (dval q-listener) :q-suspended))
+ (is (= (dval web-server) :q-resumed))
+ (mount/suspend)
+ (is (= (dval q-listener) :q-suspended))
+ (is (= (dval web-server) :q-suspended))
+ (mount/stop)
+ (is (instance? mount.core.NotStartedState (dval web-server)))
+ (is (instance? mount.core.NotStartedState (dval q-listener)))
+ (mount/start)
+ (mount/suspend)
+ (is (= (dval q-listener) :q-suspended))
+ (is (= (dval web-server) :w-suspended))
+ (mount/stop)))))
diff --git a/test/mount/test/var/fun_with_values.clj b/test/mount/test/var/fun_with_values.clj
new file mode 100644
index 0000000..f6a09a1
--- /dev/null
+++ b/test/mount/test/var/fun_with_values.clj
@@ -0,0 +1,59 @@
+(ns mount.test.var.fun-with-values
+ (:require [clojure.test :as t :refer [is are deftest testing use-fixtures]]
+ [mount.core :as mount :refer [defstate]]))
+
+(defn f [n]
+ (fn [m]
+ (+ n m)))
+
+(defn g [a b]
+ (+ a b))
+
+(defn- pf [n]
+ (+ 41 n))
+
+(defn fna []
+ 42)
+
+(defstate scalar :start 42)
+(defstate fun :start #(inc 41))
+(defstate with-fun :start (inc 41))
+(defstate with-partial :start (partial g 41))
+(defstate f-in-f :start (f 41))
+(defstate f-no-args-value :start (fna))
+(defstate f-no-args :start fna)
+(defstate f-args :start g)
+(defstate f-value :start (g 41 1))
+(defstate private-f :start pf)
+
+(defn start-states []
+ (mount/in-clj-mode)
+ (require :reload 'mount.test.var.fun-with-values)
+ (mount/start #'mount.test.var.fun-with-values/scalar
+ #'mount.test.var.fun-with-values/fun
+ #'mount.test.var.fun-with-values/with-fun
+ #'mount.test.var.fun-with-values/with-partial
+ #'mount.test.var.fun-with-values/f-in-f
+ #'mount.test.var.fun-with-values/f-args
+ #'mount.test.var.fun-with-values/f-no-args-value
+ #'mount.test.var.fun-with-values/f-no-args
+ #'mount.test.var.fun-with-values/private-f
+ #'mount.test.var.fun-with-values/f-value))
+
+(defn stop-states []
+ (mount/stop)
+ (mount/in-cljc-mode))
+
+(use-fixtures :once #((start-states) (%) (stop-states)))
+
+(deftest fun-with-values
+ (is (= scalar 42))
+ (is (= (fun) 42))
+ (is (= with-fun 42))
+ (is (= (with-partial 1) 42))
+ (is (= (f-in-f 1) 42))
+ (is (= f-no-args-value 42))
+ (is (= (f-no-args) 42))
+ (is (= (f-args 41 1) 42))
+ (is (= (private-f 1) 42))
+ (is (= f-value 42)))
diff --git a/test/mount/test/var/private_fun.clj b/test/mount/test/var/private_fun.clj
new file mode 100644
index 0000000..f00a37d
--- /dev/null
+++ b/test/mount/test/var/private_fun.clj
@@ -0,0 +1,17 @@
+(ns mount.test.var.private-fun
+ (:require [clojure.test :refer [is are deftest testing use-fixtures]]
+ [mount.core :as mount :refer [defstate]]
+ [mount.test.var.fun-with-values :refer [private-f]]))
+
+(defn in-clj-mode [f]
+ (mount/in-clj-mode)
+ (require :reload 'mount.test.var.fun-with-values 'mount.test.var.private-fun)
+ (mount/start #'mount.test.var.fun-with-values/private-f)
+ (f)
+ (mount/stop)
+ (mount/in-cljc-mode))
+
+(use-fixtures :once in-clj-mode)
+
+(deftest fun-with-values
+ (is (= (private-f 1) 42)))