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)))