├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── project.clj ├── src └── opticlj │ ├── core.cljc │ ├── file.cljc │ └── writer.cljc └── test ├── __optic__ └── opticlj │ └── core_test │ ├── defoptic.clj │ ├── err_filename.clj │ ├── fibonacci.clj │ └── form_output_stream.clj ├── __optic_cljs__ └── opticlj │ └── cljs │ └── core_test │ └── two_plus_two.cljs └── opticlj ├── cljs ├── core_test.cljs └── runner.cljs └── core_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | pom.xml.asc 3 | *.jar 4 | *.class 5 | /lib/ 6 | /target 7 | /classes 8 | /checkouts 9 | pom.xml 10 | pom.xml.asc 11 | *.jar 12 | *.class 13 | /.lein-* 14 | /.nrepl-port 15 | .hgignore 16 | .hg/ 17 | .nrepl-history 18 | node_modules/ 19 | .cljs_rhino_repl/ 20 | figwheel_server.log 21 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at lambdahands@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Philip Joseph 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # opticlj 2 | 3 | opticlj is a Clojure(Script) expectation testing (also known as snapshot testing) 4 | library. 5 | 6 | ## Rationale 7 | 8 | Expectations, or snapshots, is an automated testing strategy that captures the 9 | output of a program as a reference to its correctness. In contrast to unit 10 | testing, snapshots don't require the programmer to _specify_ the correct output 11 | of their program but instead to _verify_ the output. 12 | 13 | `opticlj` let's you define these snapshots and automatically generate the 14 | outputs into files. If you change the implementation of your program, the output 15 | files may be checked against the new behavior for differences. 16 | 17 | I was inspired by this testing strategy because it navigates elegantly between 18 | REPL driven development and testing. Unit testing is often cumbersome, but I've 19 | found it to be even more so while writing Clojure code: I often _verify_ the 20 | correctness of my functions by simply evaluating them, but that output doesn't 21 | persist outside of my own machine. 22 | 23 | Snapshot testing may be a way for Clojure developers to cast a wide net over 24 | the correctness of their programs while staying close to the REPL. 25 | 26 | ### Use Cases 27 | 28 | Snapshot testing is often a great substitute to unit testing, but it in no way 29 | has the power to verify programs as thoroughly as property-based testing. 30 | Snapshot tests are best used for _pure functions_, and aren't recommended in 31 | cases where correctness must be _"proven"_ (big air quotes). 32 | 33 | **Inspirations** 34 | 35 | - [Snapshot Testing in Swift](http://www.stephencelis.com/2017/09/snapshot-testing-in-swift) 36 | - [Testing with expectations](https://blog.janestreet.com/testing-with-expectations/) 37 | - [Mercurial: adding 'unified' tests](https://www.selenic.com/blog/?p=663) 38 | - [Jest: Snapshot Testing](https://facebook.github.io/jest/docs/en/snapshot-testing.html) 39 | 40 | ## Installation 41 | 42 | ``` 43 | [opticlj "1.0.0-alpha10"] 44 | ``` 45 | 46 | [See on Clojars](https://clojars.org/opticlj) 47 | 48 | 49 | **Disclaimer** 50 | 51 | opticlj is alpha software, and its API is likely subject to change. 52 | 53 | ## Usage 54 | 55 | The below example is a way to get started with opticlj in Clojure. 56 | 57 | Require the `opticlj.core` namespace to get started: 58 | 59 | ```clj 60 | (ns my-project.core-test 61 | (:require [opticlj.core :as optic :refer [defoptic]])) 62 | ``` 63 | 64 | Let's define a function to test: 65 | 66 | ```clj 67 | (defn add [x y] 68 | (+ x y)) 69 | ``` 70 | 71 | Define an `optic` like so: 72 | 73 | ```clj 74 | (defoptic ::one-plus-one (add 1 1)) 75 | ``` 76 | 77 | This does two things: 78 | 79 | - Defines "runner" function that can be accessed with `opticlj.core/run` 80 | - Writes an output file in `test/__optic__/my_project/core_test/one_plus_one.clj` 81 | 82 | Here's what `one_plus_one.clj` looks like: 83 | 84 | ```clj 85 | (in-ns 'my-project.core-test) 86 | 87 | (add 1 1) 88 | 89 | 2 90 | ``` 91 | 92 | The `in-ns` expression allows us to evaluate this file, which is especially 93 | useful if your editor integrates with the REPL. 94 | 95 | Next, if we change the implementation of `add` and re-run the optic, we get 96 | output confirming the snapshot was checked: 97 | 98 | ```clj 99 | (defn add [x y] 100 | (+ x y 2)) 101 | 102 | (run ::one-plus-one) 103 | 104 | ; outputs 105 | {:file "test/__optic__/my_project/core_test/one_plus_one.clj" 106 | :err-file "test/__optic__/my_project/core_test/one_plus_one.err.clj" 107 | :passing? false 108 | :diff {:string ""} 109 | :form (add 1 1) 110 | :result 4 111 | :kw :my-project.core-test/one-plus-one} 112 | ``` 113 | 114 | A new file was created: `test/__optic__/my_project/core_test/one_plus_one.err.clj` 115 | 116 | ```clj 117 | (in-ns 'my-project.core-test) 118 | 119 | (add 1 1) 120 | 121 | 4 122 | ``` 123 | 124 | Also, note how the `:passing?` key is `false`. We can view our error diff by 125 | calling `optic/errors`: 126 | 127 | ```clj 128 | (optic/errors) 129 | ; prints 130 | --- test/__optic__/my_project/core_test/one_plus_one.clj 2017-09-22 16:03:38.000000000 -0500 131 | +++ - 2017-09-22 16:04:38.000000000 -0500 132 | @@ -2,4 +2,4 @@ 133 | 134 | (add 1 1) 135 | 136 | -2 137 | +4 138 | ``` 139 | 140 | What we get back is essentially the output of running: 141 | 142 | ``` 143 | echo "...my new output..." | diff -u - 144 | ``` 145 | 146 | Let's say we wanted to change the rules of our universe and make the addition 147 | of one and one equal to four. We can `adjust!` our optic to accept these new rules: 148 | 149 | ```clj 150 | (optic/adjust! ::one-plus-one) 151 | 152 | ; outputs 153 | {:adjusted {:file "test/__optic__/my_project/core_test/one_plus_one.clj" 154 | :passing? true 155 | :diff nil 156 | :err-file nil 157 | :form (add 1 1) 158 | :result 4 159 | :kw :my-project.core-test/one-plus-one}} 160 | ``` 161 | 162 | Now when we check for errors, we see we have resolved our new form of arithmetic: 163 | 164 | ```clj 165 | (optic/errors) 166 | 167 | ; outputs 168 | nil 169 | ``` 170 | 171 | ## ClojureScript 172 | 173 | opticlj supports ClojureScript with a few caveats, namely that in order to run 174 | ClojureScript tests, you must output your test code using `:target :nodejs` in 175 | your compiler options. See the [test/opticlj/cljs](test/opticlj/cljs/) directory 176 | for an example of using opticlj with the [doo](https://github.com/bensu/doo) 177 | test runner. 178 | 179 | A convenience function, `opticlj.core/ok?`, exists to wrap opticlj's tests 180 | in a `cljs.test/deftest` expression. For example: 181 | 182 | ```clj 183 | (ns my-project.cljs.core-test 184 | (:require [cljs.test :as test :refer-macros [deftest]] 185 | [opticlj.core :as optic :refer-macros [defoptic]])) 186 | 187 | (defoptic ::two-plus-two (+ 2 2)) 188 | 189 | (deftest optics 190 | (test/is (optic/passing? (optic/review!)))) 191 | ``` 192 | 193 | ## Todo 194 | 195 | - [x] Warn if optics is undefined in the program yet exists in a file 196 | - [x] Add a `clean!` method to remove unused optics 197 | - [x] Use `defoptic` on `defoptic` _(Inception noise)_ 198 | - [ ] Complete API documentation 199 | - [ ] Reimplement core API with stateless methods 200 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "async-limiter": { 6 | "version": "1.0.1", 7 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 8 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 9 | }, 10 | "safe-buffer": { 11 | "version": "5.1.2", 12 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 13 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 14 | }, 15 | "ultron": { 16 | "version": "1.1.1", 17 | "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", 18 | "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" 19 | }, 20 | "ws": { 21 | "version": "3.3.1", 22 | "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.1.tgz", 23 | "integrity": "sha512-8A/uRMnQy8KCQsmep1m7Bk+z/+LIkeF7w+TDMLtX1iZm5Hq9HsUDmgFGaW1ACW5Cj0b2Qo7wCvRhYN2ErUVp/A==", 24 | "requires": { 25 | "async-limiter": "~1.0.0", 26 | "safe-buffer": "~5.1.0", 27 | "ultron": "~1.1.0" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "dependencies": { "ws": "3.3.1" } } 2 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject opticlj "1.0.0-alpha10" 2 | :description "A Clojure expectation/snapshot testing library, inspired by cram, ppx_expect, and jest" 3 | :url "http://github.com/lambdahands/opticlj" 4 | :license {:name "MIT"} 5 | :dependencies [;; Clojure Deps 6 | [org.clojure/clojure "1.8.0"] 7 | [com.googlecode.java-diff-utils/diffutils "1.3.0"] 8 | ;; ClojureScript Deps 9 | [org.clojure/clojurescript "1.9.908"] 10 | [cljsjs/jsdiff "3.1.0-0"] 11 | [figwheel-sidecar "0.5.13"] 12 | [com.cemerick/piggieback "0.2.1"] 13 | [doo "0.1.7"] 14 | [zprint "0.4.9"] 15 | [clojure-future-spec "1.9.0-alpha17"]] 16 | :plugins [[lein-cljsbuild "1.1.7"] 17 | [lein-figwheel "0.5.13"] 18 | [lein-doo "0.1.7"]] 19 | :repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]} 20 | :clean-targets ^{:protect false} ["target"] 21 | :cljsbuild {:builds [{:id "test" 22 | :source-paths ["src/opticlj/cljs/" "test/opticlj/cljs/"] 23 | :compiler {:main opticlj.cljs.runner 24 | :output-to "target/test/main.js" 25 | :output-dir "target/test" 26 | :target :nodejs 27 | ;:source-map true 28 | :optimizations :none}} 29 | {:id "test-dev" 30 | :source-paths ["src/opticlj/cljs/" "test/opticlj/cljs/"] 31 | :figwheel true 32 | :compiler {:main opticlj.cljs.core-test 33 | :output-to "target/test-dev/main.js" 34 | :output-dir "target/test-dev" 35 | :target :nodejs 36 | ;:source-map true 37 | :optimizations :none}}]} 38 | :figwheel {}) 39 | -------------------------------------------------------------------------------- /src/opticlj/core.cljc: -------------------------------------------------------------------------------- 1 | (ns opticlj.core 2 | (:require [opticlj.file :as file] 3 | [opticlj.writer :as writer])) 4 | 5 | ;; System 6 | 7 | (def system* 8 | (atom {:dir #?(:clj "test/__optic__" 9 | :cljs "test/__optic_cljs__") 10 | :optics {} 11 | :optic-fns {}})) 12 | 13 | ;; Library exports 14 | 15 | (defmacro defoptic [kw form & {:keys [dir system]}] 16 | `(let [dir# (or ~dir (some-> ~system deref :dir) (:dir @system*)) 17 | path# (file/stage dir# (file/sym->filepath ~kw)) 18 | run# (fn [] 19 | (let [optic# (writer/write path# ~kw '~form ~form)] 20 | (swap! (or ~system system*) update :optics assoc ~kw optic#) 21 | optic#))] 22 | (swap! (or ~system system*) update :optic-fns assoc ~kw run#) 23 | (run#) 24 | ~kw)) 25 | 26 | (defn run 27 | ([kw] (run kw system*)) 28 | ([kw system] 29 | (if-let [f (get-in @system [:optic-fns kw])] 30 | (f) (str "Optic " kw " not found in system")))) 31 | 32 | (defn error 33 | ([kw] (error system* kw)) 34 | ([system kw] 35 | (some-> (get-in @system [:optics kw]) :diff :string println))) 36 | 37 | (defn errors 38 | ([] (errors system*)) 39 | ([system] (run! error (keys (:optics @system))))) 40 | 41 | (defn check [{:keys [kw passing?]}] 42 | (when-not passing? (error kw)) 43 | passing?) 44 | 45 | (defn adjust!* [system kw] 46 | (if-let [optic (get-in system [:optics kw])] 47 | (when (and (:err-file optic) (:file optic)) 48 | (file/rename (:err-file optic) (:file optic)) 49 | {:adjusted (when-let [f (get-in system [:optic-fns kw])] (f))}) 50 | {:failure (str "Could not find `" kw "` in defined optics")})) 51 | 52 | (defn adjust! 53 | ([kw] (adjust!* @system* kw)) 54 | ([system kw] (adjust!* @system kw))) 55 | 56 | (defn adjust-all! 57 | ([] (adjust-all! system*)) 58 | ([system] 59 | (filter identity (map adjust! (keys (:optics @system)))))) 60 | 61 | (defn review!* [optic-fn exceptions] 62 | (try (optic-fn) 63 | #?(:clj (catch Exception e (swap! exceptions conj e)) 64 | :cljs (catch js/Error e (swap! exceptions conj e))))) 65 | 66 | (defn review! 67 | ([] (review! system*)) 68 | ([system] 69 | (let [exceptions (atom [])] 70 | (run! #(review!* % exceptions) (vals (:optic-fns @system))) 71 | (let [optics (:optics @system) 72 | passed (count (filter :passing? (vals optics))) 73 | failed (- (count optics) passed)] 74 | (errors) 75 | {:passed passed :failed failed :exceptions (count @exceptions)})))) 76 | 77 | (defn atom? [x] 78 | #?(:clj (instance? clojure.lang.Atom x) 79 | :cljs (instance? cljs.core/Atom x))) 80 | 81 | (defn remove! [& kws] 82 | (let [kws' (if (atom? (first kws)) (rest kws) kws) 83 | system (if (atom? (first kws)) (first kws) system*)] 84 | (doseq [sym kws'] 85 | (let [{:keys [file err-file]} (get-in @system [:optics sym])] 86 | (when file (file/delete file)) 87 | (when err-file (file/delete err-file)))) 88 | (apply swap! system dissoc :optics kws'))) 89 | 90 | (defn clear! 91 | ([] (clear! system*)) 92 | ([system] 93 | (apply remove! system (keys (:optics @system))))) 94 | 95 | (defn set-dir! 96 | ([dir] (set-dir! system* dir)) 97 | ([system dir] (swap! system assoc :dir dir))) 98 | 99 | (defn clean-msg [dir kws k] 100 | (cond 101 | (empty? kws) (println "Directory" dir "is clean.") 102 | (= k :confirm) (println "Deleting files...") 103 | :else (println "The below files aren't defined in the system with" 104 | "the :dir" dir ". Run with :confirm to delete."))) 105 | 106 | (defn ok? [{:keys [passed failed exceptions] :as review-result}] 107 | (zero? (+ failed exceptions))) 108 | 109 | ;; TODO: Implement these utility functions in ClojureScript 110 | 111 | #?(:clj 112 | (defn filtered-syms [dir optics] 113 | (->> (into [] (file/dir-syms dir)) 114 | (remove (fn [[sym _]] (get optics sym)))))) 115 | 116 | #?(:clj 117 | (defn clean! 118 | ([] (clean! system* nil)) 119 | ([k] (clean! system* k)) 120 | ([system k] 121 | (let [{:keys [optics dir]} @system 122 | syms (filtered-syms dir optics)] 123 | (clean-msg dir syms k) 124 | (doseq [[sym path] syms] 125 | (when (= k :confirm) 126 | (file/delete path)) 127 | (println path)))))) 128 | -------------------------------------------------------------------------------- /src/opticlj/file.cljc: -------------------------------------------------------------------------------- 1 | (ns opticlj.file 2 | (:require #?(:clj [clojure.java.io :as io]) 3 | #?(:cljs [cljsjs.jsdiff]) 4 | [clojure.string :as str]) 5 | #?(:clj (:import [java.io BufferedReader StringReader FileReader] 6 | [difflib DiffUtils]))) 7 | 8 | #?(:cljs (def fs (js/require "fs"))) 9 | #?(:cljs (def node-path (js/require "path"))) 10 | 11 | ;; File utils 12 | 13 | (def file-match #?(:clj #"(\.err\.clj$|\.clj$)" 14 | :cljs #"(\.err\.cljs$|\.cljs$)")) 15 | 16 | (defn sym->filepath [sym] 17 | (let [ns-str (str/replace (namespace sym) #"-" "_") 18 | sym-file (str/replace (name sym) #"-" "_") 19 | path-vec (str/split ns-str #"\.")] 20 | (str/join "/" (conj path-vec (str sym-file #?(:clj ".clj" 21 | :cljs ".cljs")))))) 22 | 23 | (defn filepath->sym [filepath prefix] 24 | (let [subpath (str/replace filepath (re-pattern (str "^" prefix "?/")) "") 25 | tokens (str/split (str/replace subpath #"_" "-") #"/") 26 | symname (str/replace (last tokens) file-match "") 27 | ns-path (str/join "." (butlast tokens))] 28 | (symbol (str ns-path "/" symname)))) 29 | 30 | (defn dir-optics [dir] 31 | (filter #(re-find file-match %) 32 | #?(:clj (map str (file-seq (io/file dir))) 33 | :cljs '()))) 34 | 35 | (defn dir-syms [dir] 36 | (into {} (map #(vector (filepath->sym % dir) %) (dir-optics dir)))) 37 | 38 | #?(:cljs 39 | (defn mkdir [parent child] 40 | (let [curdir (node-path.resolve parent child)] 41 | (when-not (fs.existsSync curdir) 42 | (fs.mkdirSync curdir)) 43 | curdir))) 44 | 45 | (defn stage [dir path] 46 | #?(:clj (str (doto (io/file dir path) (.. getParentFile mkdirs))) 47 | :cljs (let [path' (node-path.join dir path)] 48 | (reduce mkdir (str/split (node-path.dirname path') #"/")) 49 | path'))) 50 | 51 | (defn exists [path] 52 | #?(:clj (.exists (io/file path)) 53 | :cljs (fs.existsSync path))) 54 | 55 | (defn rename [from-path to-path] 56 | #?(:clj (.renameTo (io/file to-path) (io/file from-path)) 57 | :cljs (when (exists from-path) 58 | (fs.rename from-path to-path (constantly nil))))) 59 | 60 | (defn delete [path] 61 | #?(:clj (.delete (io/file path)) 62 | :cljs (when (exists path) (fs.unlink path (constantly nil))))) 63 | 64 | (defn path [file] 65 | #?(:clj (.getPath file) 66 | :cljs file)) 67 | 68 | (defn write [file output] 69 | #?(:clj (spit file output) 70 | :cljs (fs.writeFileSync file output))) 71 | 72 | (defn err-path [path] 73 | #?(:clj (str/replace path #"\.clj$" ".err.clj") 74 | :cljs (str/replace path #"\.cljs$" ".err.cljs"))) 75 | 76 | ;; diff 77 | (defn diff [path err-path output] 78 | #?(:clj (let [f-lines (line-seq (BufferedReader. (FileReader. (io/file path)))) 79 | o-lines (line-seq (BufferedReader. (StringReader. output))) 80 | f-diff (DiffUtils/diff f-lines o-lines) 81 | unified (DiffUtils/generateUnifiedDiff path err-path f-lines f-diff 3)] 82 | (when (seq unified) 83 | (str/join "\n" unified))) 84 | :cljs (let [file-str (.toString (fs.readFileSync path))] 85 | (when-not (= file-str output) 86 | (js/JsDiff.createTwoFilesPatch path err-path file-str output))))) 87 | -------------------------------------------------------------------------------- /src/opticlj/writer.cljc: -------------------------------------------------------------------------------- 1 | (ns opticlj.writer 2 | (:require [clojure.string :as str] 3 | [opticlj.file :as file] 4 | [zprint.core :refer [zprint-str]]) 5 | #?(:clj (:import [java.io Writer]))) 6 | 7 | ;; Manage diff display 8 | 9 | (defrecord Diff [string]) 10 | 11 | #?(:clj (defmethod print-method Diff [v ^Writer w] 12 | (.write w "{:string }"))) 13 | 14 | #?(:cljs (extend-protocol IPrintWithWriter 15 | Diff 16 | (-pr-writer [_ w _] 17 | (write-all w "{:string }")))) 18 | 19 | ;; Output stream writer 20 | 21 | (defn fmt-result [result] 22 | (if (string? result) 23 | (str/split (zprint-str result) #"\\n") 24 | [(zprint-str result)])) 25 | 26 | (defn form-output-stream [kw form result] 27 | (str/join "\n" (concat [(str "(in-ns '" (namespace kw) ")") "" 28 | (zprint-str form) ""] 29 | (fmt-result result) 30 | [""]))) 31 | 32 | ;; Optic data 33 | 34 | (defn err-optic [path err-path diff] 35 | {:file path 36 | :err-file err-path 37 | :diff (->Diff diff) 38 | :passing? false}) 39 | 40 | (defn optic [path] 41 | {:file path 42 | :err-file nil 43 | :diff nil 44 | :passing? true}) 45 | 46 | ;; Test checker 47 | 48 | (defn write [path kw form result] 49 | (let [output (form-output-stream kw form result) 50 | err-path (file/err-path path)] 51 | (merge {:form form :result result :kw kw} 52 | (if-let [diff (and (file/exists path) (file/diff path err-path output))] 53 | (do (file/write err-path output) 54 | (err-optic path err-path diff)) 55 | (do (file/write path output) 56 | (file/delete err-path) 57 | (optic path)))))) 58 | -------------------------------------------------------------------------------- /test/__optic__/opticlj/core_test/defoptic.clj: -------------------------------------------------------------------------------- 1 | (in-ns 'opticlj.core-test) 2 | 3 | (let 4 | [system (atom {:optics {}, :dir "test/__optic__"})] 5 | (optic/defoptic :opticlj.core-test/fibonacci (fib 10) :system system) 6 | (get-in @system [:optics :opticlj.core-test/fibonacci])) 7 | 8 | {:form (fib 10), 9 | :result 10 | ([1 1] 11 | [1 2] 12 | [2 3] 13 | [3 5] 14 | [5 8] 15 | [8 13] 16 | [13 21] 17 | [21 34] 18 | [34 55] 19 | [55 89]), 20 | :kw :opticlj.core-test/fibonacci, 21 | :file "test/__optic__/opticlj/core_test/fibonacci.clj", 22 | :err-file nil, 23 | :diff nil, 24 | :passing? true} 25 | -------------------------------------------------------------------------------- /test/__optic__/opticlj/core_test/err_filename.clj: -------------------------------------------------------------------------------- 1 | (in-ns 'opticlj.core-test) 2 | 3 | [(file/err-path "foo.clj") (file/err-path "foo-bar-baz..clj")] 4 | 5 | ["foo.err.clj" "foo-bar-baz..err.clj"] 6 | -------------------------------------------------------------------------------- /test/__optic__/opticlj/core_test/fibonacci.clj: -------------------------------------------------------------------------------- 1 | (in-ns 'opticlj.core-test) 2 | 3 | (fib 10) 4 | 5 | ([1 1] [1 2] [2 3] [3 5] [5 8] [8 13] [13 21] [21 34] [34 55] [55 89]) 6 | -------------------------------------------------------------------------------- /test/__optic__/opticlj/core_test/form_output_stream.clj: -------------------------------------------------------------------------------- 1 | (in-ns 'opticlj.core-test) 2 | 3 | (map 4 | (fn 5 | [[form result]] 6 | (writer/form-output-stream 7 | 'opticlj.writer/form-output-stream 8 | form 9 | result)) 10 | '[[(+ 1 1) 2] [(map inc (range 10)) (1 2 3 4 5 6 7 8 9 10)]]) 11 | 12 | ("(in-ns 'opticlj.writer)\n\n(+ 1 1)\n\n2\n" 13 | "(in-ns 'opticlj.writer)\n\n(map inc (range 10))\n\n(1 2 3 4 5 6 7 8 9 10)\n") 14 | -------------------------------------------------------------------------------- /test/__optic_cljs__/opticlj/cljs/core_test/two_plus_two.cljs: -------------------------------------------------------------------------------- 1 | (in-ns 'opticlj.cljs.core-test) 2 | 3 | (+ 2 2) 4 | 5 | 4 6 | -------------------------------------------------------------------------------- /test/opticlj/cljs/core_test.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:figwheel-always opticlj.cljs.core-test 2 | (:require [clojure.test :as test :refer-macros [deftest]] 3 | [opticlj.core :as optic :refer-macros [defoptic]])) 4 | 5 | (defoptic ::two-plus-two (+ 2 2)) 6 | 7 | (deftest optics 8 | (test/is (optic/ok? (optic/review!)))) 9 | -------------------------------------------------------------------------------- /test/opticlj/cljs/runner.cljs: -------------------------------------------------------------------------------- 1 | (ns opticlj.cljs.runner 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | [opticlj.cljs.core-test])) 4 | 5 | (doo-tests 'opticlj.cljs.core-test) 6 | -------------------------------------------------------------------------------- /test/opticlj/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns opticlj.core-test 2 | (:require [clojure.java.io :as io] 3 | [opticlj.core :as optic] 4 | [opticlj.file :as file] 5 | [opticlj.writer :as writer] 6 | [clojure.test :as test :refer [deftest]])) 7 | 8 | (optic/defoptic ::form-output-stream 9 | (map (fn [[form result]] 10 | (writer/form-output-stream `writer/form-output-stream form result)) 11 | '[[(+ 1 1) 2] 12 | [(map inc (range 10)) (1 2 3 4 5 6 7 8 9 10)]])) 13 | 14 | (optic/defoptic ::err-filename 15 | [(file/err-path "foo.clj") (file/err-path "foo-bar-baz..clj")]) 16 | 17 | (defn fib [n] 18 | (take n (iterate (fn [[a b]] [b (+ a b)]) [1 1]))) 19 | 20 | (optic/defoptic ::defoptic 21 | (let [system (atom {:optics {} :dir "test/__optic__"})] 22 | (optic/defoptic ::fibonacci (fib 10) :system system) 23 | (get-in @system [:optics ::fibonacci]))) 24 | 25 | (deftest optics 26 | (test/is (optic/ok? (optic/review!)))) 27 | --------------------------------------------------------------------------------