├── README.md ├── base ├── .gitignore ├── LICENSE ├── project.clj ├── src │ └── wingman │ │ ├── base.clj │ │ └── base │ │ └── ScopeResult.java └── test │ └── wingman │ └── base_test.clj ├── cider-wingman.el ├── install-all.sh ├── nrepl ├── .gitignore ├── LICENSE ├── project.clj └── src │ └── wingman │ ├── nrepl.clj │ └── nrepl │ └── plugin.clj ├── test-all.sh └── wingman ├── .gitignore ├── LICENSE ├── project.clj ├── src └── wingman │ └── core.clj └── test └── wingman └── core_test.clj /README.md: -------------------------------------------------------------------------------- 1 | # wingman [![wingman](https://img.shields.io/clojars/v/wingman.svg)](https://clojars.org/wingman) [![wingman/base](https://img.shields.io/clojars/v/wingman/base.svg)](https://clojars.org/wingman/base) [![wingman/wingman.nrepl](https://img.shields.io/clojars/v/wingman/wingman.nrepl.svg)](https://clojars.org/wingman/wingman.nrepl) 2 | 3 | Restartable exception handling for Clojure, allowing you to recover from exceptions without unwinding the stack. 4 | 5 | `wingman` tries hard to interoperate with the existing JVM exception system, to enable code using restarts to easily interoperate with code that uses plain exceptions. Libraries writers can add restarts, and applications can ignore them (using try/catch as usual), or use them (by registering a handler and invoking restarts) as they see fit. Adding restarts to a library does not _require_ applications to change their exception handling strategy, but it will provide them with more options for how to deal with errors. 6 | 7 | ## Setup 8 | 9 | Add `[wingman "0.3.0"]` to your dependency vector. 10 | 11 | To get the most out of `wingman`, install the CIDER support by loading `cider-wingman.el` in Emacs. 12 | 13 | ## Usage 14 | 15 | Register restarts with the `with-restarts` macro. This example wraps `inc` into a function which allows us to recover if we have accidentally passed it a non-number value. 16 | 17 | ```clojure 18 | (require '[wingman.core :refer [with-restarts with-handlers invoke-restart]]) 19 | (defn restartable-inc [x] 20 | (with-restarts [(:use-value [value] value)] 21 | (inc x))) 22 | ;;=> #'user/restartable-inc 23 | ``` 24 | 25 | Now, we can map this function over a list with some non-number values: 26 | 27 | ```clojure 28 | (into [] (map restartable-inc [1 2 3 :a :b nil])) 29 | ;;=> ClassCastException: clojure.lang.Keyword cannot be cast to java.lang.Number 30 | ``` 31 | 32 | Note that the behaviour of the function is unchanged when there is no appropriate handler established. Adding restarts does nothing if there aren't any appropriate handlers registered. However, if we wrap it in a `with-handlers` form: 33 | 34 | ```clojure 35 | (with-handlers [(Exception ex (invoke-restart :use-value nil))] 36 | (into [] (map restartable-inc [1 2 3 :a :b nil 10 11 12]))) 37 | ;;=> [2 3 4 nil nil nil 11 12 13] 38 | ``` 39 | 40 | When an error is encountered, the handler provided by `with-handlers` is called to decide on a course of action. In this case, it always decides to invoke the `:use-value` restart with a value of `nil`. This results in each of the error cases being added into the list as a `nil`. 41 | 42 | It is also possible to have multiple layers of restarts to choose from. For example, we might define our own `restartable-map`, which lets us skip items that throw exceptions: 43 | 44 | ```clojure 45 | (defn restartable-map [f s] 46 | (lazy-seq 47 | (when (seq s) 48 | (with-restarts [(:skip [] (restartable-map f (rest s)))] 49 | (cons (f (first s)) (restartable-map f (rest s))))))) 50 | ;;=> #'user/restartable-map 51 | ``` 52 | 53 | Now we can run the same example as before: 54 | 55 | ```clojure 56 | (with-handlers [(Exception ex (invoke-restart :use-value nil))] 57 | (into [] (restartable-map restartable-inc [1 2 3 :a :b nil 10 11 12]))) 58 | ;;=> [2 3 4 nil nil nil 11 12 13] 59 | ``` 60 | 61 | Or, we can change our strategy and decide to skip failing values: 62 | 63 | ```clojure 64 | (with-handlers [(Exception ex (invoke-restart :skip))] 65 | (into [] (restartable-map restartable-inc [1 2 3 :a :b nil 10 11 12]))) 66 | ;;=> [2 3 4 11 12 13] 67 | ``` 68 | 69 | Or we can decide that we want to replace `nil` with `0`, and skip everything else: 70 | 71 | ```clojure 72 | (with-handlers [(NullPointerException ex (invoke-restart :use-value 0)) 73 | (Exception ex (invoke-restart :skip))] 74 | (into [] (restartable-map restartable-inc [1 2 3 :a :b nil 10 11 12]))) 75 | ;;=> [2 3 4 11 12 13] 76 | ``` 77 | 78 | In this way, restarts allow us to separate the _decision_ about how to recover from an error from the _mechanics_ of actually recovering from the error. This enables higher-level code to make decisions about how lower level functions should recover from their errors, without unwinding the stack. 79 | 80 | ## Why restarts? 81 | 82 | Why should we want to use restarts in Clojure? [Chris Houser already gave us a great model for error handling in Clojure](https://www.youtube.com/watch?v=zp0OEDcAro0), why should I use `wingman`? The answer to this question is really about _interactivity_. 83 | 84 | The method of binding dynamic variables for error handling is roughly equivalent to what `wingman` does, but where the plain dynamic-variables approach fails is tool support. There is no way for our tooling to find out what the options are to restart execution, and to present that choice to the user in an interactive session. From the start, the focus in `wingman` has been on the REPL experience. It is primarily about recovering from errors in the REPL, and only then making that same functionality available in code. 85 | 86 | ## What about Exceptions? 87 | 88 | Obviously, Clojure executes on a host which doesn't natively support restarts. As a result, restarts have been implemented using JVM Exceptions to manipulate the normal control flow of the program. There are a few edge-cases, but for the most part this should interoperate with native JVM Exceptions, allowing them to pass through uninterrupted if no handlers have been established. This means that adding restarts to a library should have _no effect_ on a program unless that program opts-in to using them by installing handlers. 89 | 90 | There is the potential for a library/application to break `wingman` by catching things that should be allowed through. All the internal types derive from `java.lang.Throwable`, so as long as you don't catch `Throwable` you should be fine. If you do catch `Throwable`, please ensure that `wingman.base.ScopeResult` is re-thrown. 91 | 92 | ## Writing restarts 93 | 94 | Restarts allow a piece of code to specify reasonable strategies to deal with errors that occur within them. They may allow you to simply use a specified value, or they may allow you to do complex actions like restart an agent, or reconnect a socket. 95 | 96 | As an example, a simple restart to use a provided value would look like this: 97 | 98 | ```clojure 99 | (with-restarts [(:use-value [value] value)] 100 | (/ 1 0)) 101 | ``` 102 | 103 | This would allow a handler to invoke `(invoke-restart :use-value 10)` to recover from this exception, and to return `10` as the result of the `with-restarts` form. 104 | 105 | In addition, restarts can have three extra attributes defined: 106 | 107 | 1. `:applicable?` specifies a predicate which tests whether this restart is applicable to this exception type. It defaults to `(constantly true)`, under the assumption that restarts are always applicable. 108 | 109 | 2. `:describe` specifies a function which will convert the exception into an explanation of what this restart will do. As a shortcut, you may use a string literal instead, which will be converted into a function returning that string. It defaults to `(constantly "")`. 110 | 111 | 3. `:arguments` specifies a function which will return arguments for this restart. This function is only ever used interactively, and thus should prompt the user for any necessary information to invoke this restart. It defaults to `(constantly nil)`. 112 | 113 | Here is an example of the above restart using these attributes: 114 | 115 | ```clojure 116 | (with-restarts [(:use-value [value] 117 | :describe "Provide a value to use." 118 | :arguments #'read-unevaluated-value 119 | value)] 120 | (/ 1 0)) 121 | ``` 122 | 123 | Restarts are invoked in the same dynamic context in which they were defined. The stack is unwound to the level of the `with-restarts` form, and the restart is invoked. 124 | 125 | Multiple restarts with the same name can be defined, but the "deepest" one will be invoked by a call to `invoke-restart`. You can use `find-restarts`, or even `list-restarts`, if you would like to introspect the available restarts. 126 | 127 | Restart names can be any value that is not an instance of `wingman.base.Restart`, but it is recommended to use keywords as names. 128 | 129 | ## Writing handlers 130 | 131 | Handlers are conceptually similar to try/catch, but they are invoked without unwinding the stack. This gives them greater scope to make decisions about how to recover from errors. Ultimately, though, they can only recover in ways that have registered restarts. 132 | 133 | For example, here is how to use `with-handlers` to replace try/catch: 134 | 135 | ```clojure 136 | (with-handlers [(Exception ex (.getMessage ex))] 137 | (/ 1 0)) 138 | ;;=> "Divide by zero" 139 | ``` 140 | 141 | Similarly to try/catch, multiple handlers can be defined for different exception types, and the first matching handler will be run to handle the exception. 142 | 143 | There are five possible outcomes for a handler: 144 | 145 | 1. Return a value normally: the `call-with-handler` form will return that value. 146 | 147 | 2. Throw an exception: the `call-with-handler` form will throw that exception. 148 | 149 | 3. Invoke a restart, using `invoke-restart`: the handler will cease executing, and the code of the restart will be invoked, continuing to execute from that point. 150 | 151 | 4. Invoke `unhandle-exception` on an exception: the exception will be thrown from the point where this handler was invoked. This should be used with care. 152 | 153 | 5. Invoke `rethrow` on an exception: defer the decision to a handler higher in the call-stack. If there is no such handler, the exception will be thrown (which will appear the same as option 2). 154 | 155 | Conceptually, options 1 and 2 unwind the stack up until the handler, option 3 unwinds the stack to the appropriate restart, option 4 hands back to JVM exception handling, and option 5 delegates to another handler without unwinding the stack at all. 156 | 157 | Invoking `unhandle-exception` is primarily useful when working with code that uses exceptions to provide fallback behaviour. The restart handler mechanism can, in some cases, cause catch clauses to be \"skipped\", bypassing exception-based mechanisms. If possible, avoid using `unhandle-exception`, as it can result in restart handlers firing multiple times for the same exception. 158 | 159 | ## License 160 | 161 | Copyright © 2018 Carlo Zancanaro 162 | 163 | Distributed under the MIT License. 164 | -------------------------------------------------------------------------------- /base/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | -------------------------------------------------------------------------------- /base/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Carlo Zancanaro 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 | -------------------------------------------------------------------------------- /base/project.clj: -------------------------------------------------------------------------------- 1 | (defproject wingman/base "0.3.1-SNAPSHOT" 2 | :description "Restartable exception handling for Clojure" 3 | :url "https://github.com/czan/wingman" 4 | :license {:name "MIT" 5 | :url "https://opensource.org/licenses/MIT"} 6 | :dependencies [[org.clojure/clojure "1.8.0"]] 7 | :source-paths ["src"] 8 | :java-source-paths ["src"] 9 | :repositories [["releases" {:url "https://clojars.org/repo" 10 | :creds :gpg}]]) 11 | -------------------------------------------------------------------------------- /base/src/wingman/base.clj: -------------------------------------------------------------------------------- 1 | (ns wingman.base 2 | (:refer-clojure :exclude [eval]) 3 | (:import (wingman.base ScopeResult))) 4 | 5 | (def ^:private ^:dynamic *handlers* nil) 6 | (def ^:private ^:dynamic *make-restarts* nil) 7 | (def ^:private ^:dynamic *restarts* nil) 8 | 9 | (defn call-without-handling 10 | "Call `thunk` with no active handlers and no current restarts. Any 11 | exceptions raised by `thunk` will propagate normally. Note that an 12 | exception raised by this call will be handled normally." 13 | [thunk] 14 | (binding [*handlers* nil 15 | *make-restarts* nil 16 | *restarts* nil] 17 | (thunk))) 18 | 19 | (defrecord Restart [name description make-arguments behaviour]) 20 | 21 | (defn restart? 22 | "Returns true if a given object represents a restart. Otherwise, 23 | returns false." 24 | [obj] 25 | (instance? Restart obj)) 26 | 27 | (defn make-restart 28 | "Create an object representing a restart with the given name, 29 | description, interactive prompt function, and behaviour when 30 | invoked." 31 | [name description make-arguments behaviour] 32 | (->Restart name description make-arguments behaviour)) 33 | 34 | (defn rethrow 35 | "Rethrow an exception, without unwinding the stack any further. This 36 | will invoke the nearest handler to handle the error. If no handlers 37 | are available then this is equivalent to `throw`, and the stack will 38 | be unwound." 39 | [ex] 40 | (if (seq *handlers*) 41 | ((first *handlers*) ex) 42 | (throw ex))) 43 | 44 | (defn unhandle-exception 45 | "Rethrow an exception, unwinding the stack and propagating it as 46 | normal. This makes it seem as if the exception was never caught, but 47 | it may still be caught by handlers/restarts higher in the stack." 48 | [ex] 49 | (throw (ScopeResult. nil #(throw ex)))) 50 | 51 | (defn list-restarts 52 | "Return a list of all current restarts. This function must only be 53 | called within the dynamic extent of a handler execution." 54 | [] 55 | *restarts*) 56 | 57 | (defn invoke-restart 58 | "Use the provided restart, with the given arguments. No attempt is 59 | made to validate that the provided restart is current. 60 | 61 | Always throws an exception, will never return normally." 62 | [restart & args] 63 | (throw (ScopeResult. (:id restart) #(apply (:behaviour restart) args)))) 64 | 65 | (defn- handled-value [id value] 66 | (throw (ScopeResult. id (constantly value)))) 67 | 68 | (defn- thrown-value [id value] 69 | (throw (ScopeResult. id #(throw value)))) 70 | 71 | (defn run-or-throw [id ^ScopeResult result] 72 | (if (= (.-scopeId result) id) 73 | ((.-thunk result)) 74 | (throw result))) 75 | 76 | (defn- wrapped-handler [id handler] 77 | (fn [ex] 78 | (let [restarts (or *restarts* 79 | (vec (mapcat #(% ex) *make-restarts*)))] 80 | (try (binding [*restarts* restarts 81 | *handlers* (next *handlers*)] 82 | (handled-value id (handler ex))) 83 | (catch ScopeResult t 84 | (run-or-throw nil t)) 85 | (catch Throwable t 86 | (thrown-value id t)))))) 87 | 88 | (def ^:private next-id (volatile! 0)) 89 | 90 | (defn call-with-handler 91 | "Run `thunk`, using `handler` to handle any exceptions raised. 92 | 93 | There are five possible outcomes for your handler: 94 | 95 | 1. Return a value normally: the `call-with-handler` form will return 96 | that value. 97 | 98 | 2. Throw an exception: the `call-with-handler` form will throw that 99 | exception. 100 | 101 | 3. Invoke a restart, using `invoke-restart`: the handler will cease 102 | executing, and the code of the restart will be invoked, continuing 103 | to execute from that point. 104 | 105 | 4. Invoke `unhandle-exception` on an exception: the exception will 106 | be thrown from the point where this handler was invoked. This should 107 | be used with care. 108 | 109 | 5. Invoke `rethrow` on an exception: defer the decision to a handler 110 | higher in the call-stack. If there is no such handler, the exception 111 | will be thrown (which will appear the same as option 2). 112 | 113 | Notes: 114 | 115 | - The handler will be used for *all* exceptions, so you must be 116 | careful to `rethrow` exceptions that you are unable to handle. 117 | 118 | - Invoking `unhandle-exception` is primarily useful when working 119 | with code that uses exceptions to provide fallback behaviour. The 120 | restart handler mechanism can, in some cases, cause catch clauses to 121 | be \"skipped\", bypassing exception-based mechanisms. If possible, 122 | avoid using `unhandle-exception`, as it can result in restart 123 | handlers firing multiple times for the same exception. 124 | 125 | Examples: 126 | 127 | (call-with-handler #(str \"Caught an exception: \" (.getMessage %)) 128 | #(/ 1 0)) 129 | ;;=>\"Caught an exception: Divide by zero\"" 130 | {:style/indent [1]} 131 | [handler thunk] 132 | (let [id (vswap! next-id inc)] 133 | (try 134 | (binding [*handlers* (cons (wrapped-handler id handler) *handlers*)] 135 | (try 136 | (thunk) 137 | (catch ScopeResult t 138 | (throw t)) 139 | (catch Throwable t 140 | (rethrow t)))) 141 | (catch ScopeResult t 142 | (run-or-throw id t))))) 143 | 144 | (defn call-with-restarts 145 | "Run `thunk`, using `make-restarts` to create restarts for exceptions. 146 | 147 | Run `thunk` within a dynamic extent in which `make-restarts` adds to 148 | the list of current restarts. If an exception is thrown, then 149 | `make-restarts` will be invoked, and must return a list of restarts 150 | applicable to this exception. If no exception is thrown, then 151 | `make-restarts` will not be invoked. 152 | 153 | For example: 154 | 155 | (call-with-restarts 156 | (fn [ex] [(make-restart :use-value 157 | (str \"Use this string instead of \" (.getMessage ex)) 158 | #(prompt-user \"Raw string to use: \") 159 | identity)]) 160 | #(/ 1 0))" 161 | {:style/indent [1]} 162 | [make-restarts thunk] 163 | (let [id (vswap! next-id inc)] 164 | (try 165 | (binding [*make-restarts* (cons (fn [ex] 166 | (map #(assoc % :id id) 167 | (make-restarts ex))) 168 | *make-restarts*)] 169 | (try 170 | (thunk) 171 | (catch ScopeResult t 172 | (throw t)) 173 | (catch Throwable t 174 | (rethrow t)))) 175 | (catch ScopeResult t 176 | (run-or-throw id t))))) 177 | 178 | (defn ^:dynamic prompt-user 179 | "Prompt the user for some input, in whatever way you can. 180 | 181 | This function will be dynamically rebound by whatever tooling is 182 | currently active, to prompt the user appropriately. 183 | 184 | Provide a `type` in order to hint to tooling what kind of thing you 185 | want to read. Legal values of `type` are implementation dependent, 186 | depending on the tooling in use. Tools should support a minimum of 187 | `:form` (to read a Clojure form), `:file` (to read a filename), and 188 | `:options` (to choose an option from a list of options, provided as 189 | the first argument after `type`)." 190 | ([prompt] 191 | (throw (IllegalStateException. "In order to prompt the user, a tool must bind #'wingman.base/prompt-user."))) 192 | ([prompt type & args] 193 | (throw (IllegalStateException. "In order to prompt the user, a tool must bind #'wingman.base/prompt-user.")))) 194 | -------------------------------------------------------------------------------- /base/src/wingman/base/ScopeResult.java: -------------------------------------------------------------------------------- 1 | package wingman.base; 2 | 3 | public class ScopeResult extends Throwable { 4 | public final Object scopeId; 5 | public final Object thunk; 6 | public ScopeResult(Object scopeId, Object thunk) { 7 | super("This exception is internal to wingman. Ideally, you would never see it!"); 8 | this.scopeId = scopeId; 9 | this.thunk = thunk; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /base/test/wingman/base_test.clj: -------------------------------------------------------------------------------- 1 | (ns wingman.base-test 2 | (:require [clojure.test :refer :all] 3 | [wingman.base :refer :all])) 4 | 5 | ;; Handlers have five basic behaviours that we want to test: 6 | 7 | ;; 1. The handler's return value is used as the value of the 8 | ;; `call-with-handler` form. 9 | 10 | (deftest handler-return-values-are-used-directly 11 | (is (= (call-with-handler (fn [ex] :a) 12 | #(/ 1 0)) 13 | :a)) 14 | (is (= (call-with-handler (fn [ex] :b) 15 | #(call-with-restarts (fn [ex]) 16 | (fn [] (/ 1 0)))) 17 | :b)) 18 | (is (= (call-with-handler (fn [ex] :c) 19 | #(+ (/ 1 0) 20 | 3)) 21 | :c))) 22 | 23 | ;; 2. If the handler throws an exception, it is propagated as the 24 | ;; result of the `call-with-handler` form. 25 | 26 | (deftest handler-thrown-exceptions-are-thrown 27 | (is (thrown-with-msg? 28 | Exception #"THIS IS A TEST EXCEPTION" 29 | (call-with-handler (fn [ex] (throw (Exception. "THIS IS A TEST EXCEPTION"))) 30 | #(/ 1 0)))) 31 | (is (thrown-with-msg? 32 | Exception #"THIS IS A TEST EXCEPTION" 33 | (call-with-handler (fn [ex] (throw (Exception. "THIS IS A TEST EXCEPTION"))) 34 | #(call-with-restarts (fn [ex]) 35 | (fn [] (/ 1 0))))))) 36 | 37 | ;; 3. If the handler invokes a restart, the computation continues from 38 | ;; the point of that `call-with-restarts` form. 39 | 40 | (deftest handler-invoking-restart-restarts-computation 41 | (is (= (call-with-handler (fn [ex] 42 | (let [[r] (list-restarts)] 43 | (invoke-restart r))) 44 | #(call-with-restarts (fn [ex] 45 | [(make-restart :use-a "" (constantly nil) (constantly :a))]) 46 | (fn [] 47 | (/ 1 0)))) 48 | :a)) 49 | (is (= (call-with-handler (fn [ex] 50 | (let [[r] (list-restarts)] 51 | (invoke-restart r :b :c))) 52 | #(call-with-restarts (fn [ex] 53 | [(make-restart :use-a "" (constantly nil) (fn [x y] [x y]))]) 54 | (fn [] 55 | (/ 1 0)))) 56 | [:b :c]))) 57 | 58 | ;; 4. If the handler invokes `unhandle-exception`, an exception is 59 | ;; thrown from the point where it was first seen. 60 | 61 | (deftest handler-unhandling-exception-throws 62 | ;; Note that in this case the exception never actually enters our 63 | ;; framework, because it is caught and dealt with before it 64 | ;; propagates high enough. 65 | (is (= (call-with-handler (fn [ex] (unhandle-exception ex)) 66 | (fn [] 67 | (try 68 | (/ 1 0) 69 | (catch Exception ex 70 | :a)))) 71 | :a)) 72 | (is (= (call-with-handler (fn [ex] (unhandle-exception ex)) 73 | #(try 74 | (call-with-restarts (fn [ex] []) 75 | (fn [] 76 | (/ 1 0))) 77 | (catch Exception ex 78 | :b))) 79 | :b))) 80 | 81 | ;; 5. If the handler can't handle something, it `rethrow`s it to a 82 | ;; higher handler to make a decision (which might then do any of the 83 | ;; above). 84 | 85 | (deftest handler-rethrowing-defers-to-higher-handler 86 | ;; This is just a sanity-check that the handlers get called in the 87 | ;; right order. 88 | (is (= (call-with-handler (fn [ex] :a) 89 | #(call-with-handler (fn [ex] :b) 90 | (fn [] (/ 1 0)))) 91 | :b)) 92 | (is (= (call-with-handler (fn [ex] :a) 93 | #(call-with-handler (fn [ex] (rethrow ex)) 94 | (fn [] (/ 1 0)))) 95 | :a)) 96 | (is (thrown-with-msg? 97 | ArithmeticException #"Divide by zero" 98 | (call-with-handler (fn [ex] (rethrow ex)) 99 | #(/ 1 0))))) 100 | 101 | ;; And some other miscellaneous tests. 102 | 103 | (deftest list-restarts-returns-empty-outside-of-handlers 104 | (call-with-restarts (fn [ex] 105 | [(make-restart :r1 nil nil (constantly nil))]) 106 | #(is (= (map :name (list-restarts)) 107 | [])))) 108 | 109 | (deftest list-restarts-returns-current-restarts-inside-handlers 110 | (call-with-handler (fn [ex] 111 | (is (= (map :name (list-restarts)) 112 | [:r1]))) 113 | #(call-with-restarts (fn [ex] 114 | [(make-restart :r1 nil nil (constantly nil))]) 115 | (fn [] 116 | (/ 1 0))))) 117 | 118 | (deftest rethrow-throws-exceptions 119 | (is (thrown? Exception 120 | (rethrow (Exception.)))) 121 | (is (thrown? Exception 122 | (call-with-handler (fn [ex] 123 | (rethrow ex)) 124 | #(throw (Exception.))))) 125 | (is (thrown? Exception 126 | (call-with-handler (fn [ex] 127 | (rethrow ex)) 128 | #(call-with-restarts (fn [ex] []) 129 | (fn [] 130 | (throw (Exception.)))))))) 131 | 132 | (deftest docstring-examples 133 | (is (= (call-with-handler #(str "Caught an exception: " (.getMessage %)) 134 | #(/ 1 0)) 135 | "Caught an exception: Divide by zero")) 136 | (let [[{:keys [name description behaviour]}] 137 | (call-with-handler (fn [ex] (list-restarts)) 138 | (fn [] 139 | (call-with-restarts 140 | (fn [ex] [(make-restart :use-value 141 | (str "Use this string instead of " (.getMessage ex)) 142 | #(prompt-user "Raw string to use: ") 143 | identity)]) 144 | #(/ 1 0))))] 145 | (is (= name :use-value)) 146 | (is (= description "Use this string instead of Divide by zero")) 147 | ;; we can't test make-behaviour, because the instance will be different 148 | (is (= behaviour identity)))) 149 | -------------------------------------------------------------------------------- /cider-wingman.el: -------------------------------------------------------------------------------- 1 | ;;; cider-wingman.el --- Handle and restart exceptions in Clojure. -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2018 Carlo Zancanaro 4 | 5 | ;; Author: Carlo Zancanaro 6 | ;; Maintainer: Carlo Zancanaro 7 | ;; Created: 12 Jun 2018 8 | ;; Keywords: cider clojure wingman exception 9 | ;; Homepage: https://github.com/czan/wingman 10 | 11 | ;; This file is not part of GNU Emacs. 12 | 13 | ;;; The MIT License: 14 | 15 | ;; Copyright (c) 2018 Carlo Zancanaro 16 | 17 | ;; Permission is hereby granted, free of charge, to any person obtaining a copy 18 | ;; of this software and associated documentation files (the "Software"), to deal 19 | ;; in the Software without restriction, including without limitation the rights 20 | ;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 21 | ;; copies of the Software, and to permit persons to whom the Software is 22 | ;; furnished to do so, subject to the following conditions: 23 | 24 | ;; The above copyright notice and this permission notice shall be included in all 25 | ;; copies or substantial portions of the Software. 26 | 27 | ;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | ;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | ;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | ;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | ;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 32 | ;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 33 | ;; SOFTWARE. 34 | 35 | ;;; Code: 36 | 37 | (require 'cider) 38 | 39 | (defvar-local cider-wingman-restart-request-id nil) 40 | (defvar-local cider-wingman-restarts nil) 41 | 42 | (defun cider-wingman-nrepl-send-response-to (id response connection &optional tooling) 43 | (with-current-buffer connection 44 | (when-let ((session (if tooling nrepl-tooling-session nrepl-session))) 45 | (setq response (append response `("session" ,session)))) 46 | (let* ((response (cons 'dict response)) 47 | (message (nrepl-bencode response))) 48 | (nrepl-log-message response 'response) 49 | (puthash id (lambda (&rest ignored)) nrepl-pending-requests) 50 | (process-send-string nil message)))) 51 | 52 | ;;;###autoload 53 | (defun cider-wingman-handle-nrepl-response (response) 54 | (nrepl-dbind-response response (id type) 55 | (cond 56 | ((equal type "restart/prompt") 57 | (nrepl-dbind-response response (error restarts causes) 58 | (when restarts 59 | (puthash id (lambda (&rest ignored)) nrepl-pending-requests) 60 | (cider-wingman-prompt-user id error causes restarts)))) 61 | ((equal type "restart/ask") 62 | (nrepl-dbind-response response (prompt options) 63 | (puthash id (lambda (&rest ignored)) nrepl-pending-requests) 64 | (cider-wingman-ask-user id prompt options (cider-current-connection))))))) 65 | 66 | (define-derived-mode cider-wingman-restart-prompt-mode cider-stacktrace-mode "Wingman") 67 | 68 | (defun cider-wingman-choose-restart (&optional index) 69 | (interactive) 70 | (let ((choice (or index 71 | (ignore-errors (string-to-number (this-command-keys)))))) 72 | (when (<= 1 choice (length cider-wingman-restarts)) 73 | (cider-wingman-send-restart-choice cider-wingman-restart-request-id 74 | (1- choice) 75 | (cider-current-connection))))) 76 | 77 | (defun cider-wingman-choose-unhandled () 78 | (interactive) 79 | (cider-wingman-send-restart-choice cider-wingman-restart-request-id 80 | "unhandled" 81 | (cider-current-connection))) 82 | 83 | (defun cider-wingman-choose-abort () 84 | (interactive) 85 | (cider-wingman-send-restart-choice cider-wingman-restart-request-id 86 | "abort" 87 | (cider-current-connection))) 88 | 89 | (define-key cider-wingman-restart-prompt-mode-map (kbd "1") #'cider-wingman-choose-restart) 90 | (define-key cider-wingman-restart-prompt-mode-map (kbd "2") #'cider-wingman-choose-restart) 91 | (define-key cider-wingman-restart-prompt-mode-map (kbd "3") #'cider-wingman-choose-restart) 92 | (define-key cider-wingman-restart-prompt-mode-map (kbd "4") #'cider-wingman-choose-restart) 93 | (define-key cider-wingman-restart-prompt-mode-map (kbd "5") #'cider-wingman-choose-restart) 94 | (define-key cider-wingman-restart-prompt-mode-map (kbd "6") #'cider-wingman-choose-restart) 95 | (define-key cider-wingman-restart-prompt-mode-map (kbd "7") #'cider-wingman-choose-restart) 96 | (define-key cider-wingman-restart-prompt-mode-map (kbd "8") #'cider-wingman-choose-restart) 97 | (define-key cider-wingman-restart-prompt-mode-map (kbd "9") #'cider-wingman-choose-restart) 98 | (define-key cider-wingman-restart-prompt-mode-map (kbd "u") #'cider-wingman-choose-unhandled) 99 | (define-key cider-wingman-restart-prompt-mode-map (kbd "q") #'cider-wingman-choose-abort) 100 | 101 | (defun cider-wingman-send-restart-choice (id restart connection) 102 | (cider-wingman-nrepl-send-response-to id 103 | `("op" "restart/choose" 104 | "restart" ,restart 105 | "id" ,id) 106 | connection) 107 | (cider-popup-buffer-quit :kill)) 108 | 109 | (defun cider-wingman-insert-bounds (&rest args) 110 | (let ((start (point))) 111 | (apply #'insert args) 112 | (cons start (point)))) 113 | 114 | (defun cider-wingman-insert-restart-prompt (index name description) 115 | (insert " ") 116 | (let ((clickable-start (point)) 117 | prompt-bounds 118 | name-bounds) 119 | (setq prompt-bounds (cider-wingman-insert-bounds 120 | "[" 121 | (cond 122 | ((eq :abort index) 123 | "q") 124 | ((eq :unhandled index) 125 | "u") 126 | (:else 127 | (number-to-string index))) 128 | "]")) 129 | (insert " ") 130 | (setq name-bounds (cider-wingman-insert-bounds name)) 131 | (unless (equal description "") 132 | (insert " ") 133 | (cider-wingman-insert-bounds description)) 134 | (let ((map (make-sparse-keymap))) 135 | (cond 136 | ((eq :abort index) 137 | (define-key map [mouse-2] #'cider-wingman-choose-abort) 138 | (define-key map (kbd "") #'cider-wingman-choose-abort)) 139 | ((eq :unhandled index) 140 | (define-key map [mouse-2] #'cider-wingman-choose-unhandled) 141 | (define-key map (kbd "") #'cider-wingman-choose-unhandled)) 142 | (:else 143 | (define-key map [mouse-2] (lambda () 144 | (interactive) 145 | (cider-wingman-choose-restart index))) 146 | (define-key map (kbd "") (lambda () 147 | (interactive) 148 | (cider-wingman-choose-restart index))))) 149 | (add-text-properties clickable-start (point) 150 | `(keymap ,map 151 | follow-link t 152 | mouse-face highlight 153 | help-echo "mouse-2: use this restart"))) 154 | (put-text-property (car prompt-bounds) (cdr prompt-bounds) 155 | 'face 'cider-debug-prompt-face) 156 | (put-text-property (car name-bounds) (cdr name-bounds) 157 | 'face 'cider-stacktrace-error-class-face) 158 | (insert "\n"))) 159 | 160 | (defun cider-wingman-prompt-user (id error causes restarts) 161 | (with-current-buffer (cider-popup-buffer (generate-new-buffer-name "*wingman-prompt*") :select) 162 | (cider-wingman-restart-prompt-mode) 163 | ;; cider-stacktrace relies on this pointing to the right buffer, 164 | ;; so we just set it right away 165 | (setq-local cider-error-buffer (current-buffer)) 166 | (let ((inhibit-read-only t) 167 | error-bounds) 168 | (cider-stacktrace-render (current-buffer) causes) 169 | (goto-char (point-min)) 170 | (setq error-bounds (cider-wingman-insert-bounds error)) 171 | (insert "\n") 172 | (insert "\n") 173 | (insert "The following restarts are available:\n") 174 | (let ((index 1)) 175 | (dolist (restart restarts) 176 | (cider-wingman-insert-restart-prompt index (car restart) (cadr restart)) 177 | (setq index (1+ index)))) 178 | (cider-wingman-insert-restart-prompt :unhandled "unhandled" "Rethrow the exception.") 179 | (cider-wingman-insert-restart-prompt :abort "abort" "Abort this evaluation.") 180 | (insert "\n") 181 | (goto-char (point-min)) 182 | (when error-bounds 183 | (put-text-property (car error-bounds) (cdr error-bounds) 184 | 'face 'cider-stacktrace-error-message-face)) 185 | (setq-local cider-wingman-restart-request-id id) 186 | (setq-local cider-wingman-restarts restarts)))) 187 | 188 | (defun cider-wingman-answer (id answer connection) 189 | (cider-wingman-nrepl-send-response-to id 190 | `("op" "restart/answer" 191 | "input" ,answer 192 | "id" ,id) 193 | connection)) 194 | 195 | (defun cider-wingman-cancel (id connection) 196 | (cider-wingman-nrepl-send-response-to id 197 | `("op" "restart/answer" 198 | "error" "cancel" 199 | "id" ,id) 200 | connection)) 201 | 202 | (defun cider-wingman-read-form (prompt &optional value) 203 | (cider-read-from-minibuffer prompt value)) 204 | 205 | (defun cider-wingman-read-file (prompt &optional value) 206 | (read-file-name prompt nil value)) 207 | 208 | (defun cider-wingman-read-options (prompt &optional options value) 209 | (completing-read prompt options nil nil value)) 210 | 211 | (defun cider-wingman-read-fallback (prompt &rest args) 212 | (read-string prompt)) 213 | 214 | (defvar cider-wingman-prompt-handlers 215 | `((form . cider-wingman-read-form) 216 | (file . cider-wingman-read-file) 217 | (options . cider-wingman-read-options))) 218 | 219 | (defun cider-wingman-ask-user (id prompt options connection) 220 | (condition-case _ 221 | (nrepl-dbind-response options (type args) 222 | (let ((handler (alist-get (intern-soft type) 223 | cider-wingman-prompt-handlers 224 | #'cider-wingman-read-fallback))) 225 | (cider-wingman-answer id (apply handler prompt args) connection))) 226 | (quit 227 | (cider-wingman-cancel id connection)))) 228 | 229 | ;;;###autoload 230 | (define-minor-mode cider-wingman-minor-mode 231 | "Support nrepl responses from the wingman nrepl middleware. 232 | When an exception occurs, the user will be prompted to ask how to 233 | proceed." 234 | :global t 235 | (let ((hook-fn (if cider-wingman-minor-mode 236 | #'add-hook 237 | #'remove-hook)) 238 | (list-fn (if cider-wingman-minor-mode 239 | #'add-to-list 240 | #'(lambda (lsym obj) 241 | (set lsym (remove obj (symbol-value lsym))))))) 242 | (funcall hook-fn 'nrepl-response-handler-functions #'cider-wingman-handle-nrepl-response) 243 | (funcall list-fn 244 | 'cider-jack-in-dependencies 245 | '("wingman/wingman.nrepl" "0.3.1")) 246 | (funcall list-fn 247 | 'cider-jack-in-dependencies-exclusions 248 | '("wingman/wingman.nrepl" ("org.clojure/clojure" "org.clojure/tools.nrepl"))) 249 | (funcall list-fn 250 | 'cider-jack-in-nrepl-middlewares 251 | "wingman.nrepl/middleware") 252 | (funcall list-fn 253 | 'cider-jack-in-lein-plugins 254 | '("wingman/wingman.nrepl" "0.3.1")))) 255 | 256 | ;;;###autoload 257 | (with-eval-after-load 'cider 258 | (cider-wingman-minor-mode 1)) 259 | 260 | ;;; cider-wingman.el ends here 261 | -------------------------------------------------------------------------------- /install-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd base; lein install; cd ..; 3 | cd wingman; lein install; cd ..; 4 | cd nrepl; lein install; cd ..; 5 | -------------------------------------------------------------------------------- /nrepl/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | -------------------------------------------------------------------------------- /nrepl/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Carlo Zancanaro 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 | -------------------------------------------------------------------------------- /nrepl/project.clj: -------------------------------------------------------------------------------- 1 | (defproject wingman/wingman.nrepl "0.3.1" 2 | :description "Restartable exception handling for Clojure" 3 | :url "https://github.com/czan/wingman" 4 | :license {:name "MIT" 5 | :url "https://opensource.org/licenses/MIT"} 6 | :dependencies [[org.clojure/clojure "1.8.0"] 7 | [org.clojure/tools.nrepl "0.2.12"] 8 | [wingman/base "0.3.0"] 9 | [wingman "0.3.0"]] 10 | :repositories [["releases" {:url "https://clojars.org/repo" 11 | :creds :gpg}]]) 12 | -------------------------------------------------------------------------------- /nrepl/src/wingman/nrepl.clj: -------------------------------------------------------------------------------- 1 | (ns wingman.nrepl 2 | (:require [wingman.core :as dgu :refer [with-handlers with-restarts without-handling]] 3 | [wingman.base :refer [make-restart call-with-restarts prompt-user]] 4 | [clojure.tools.nrepl.transport :as t] 5 | [clojure.tools.nrepl.misc :refer (response-for uuid)] 6 | [clojure.tools.nrepl.middleware.session :refer (session)] 7 | [clojure.tools.nrepl.middleware :refer (set-descriptor!)] 8 | [clojure.tools.nrepl.middleware.interruptible-eval :as e] 9 | [clojure.pprint :refer [pprint]]) 10 | (:import java.util.concurrent.Executor)) 11 | 12 | (def awaiting-restarts (atom {})) 13 | (def awaiting-prompts (atom {})) 14 | 15 | (defn analyze-causes [ex pprint] 16 | (when-let [f (ns-resolve 'cider.nrepl.middleware.stacktrace 'analyze-causes)] 17 | (f ex pprint))) 18 | 19 | (def unhandled (make-restart :unhandled 20 | "Leave the exception unhandled, and propagate it from the throw site" 21 | (constantly nil) 22 | #(assert false "This should never run"))) 23 | 24 | (def abort (make-restart :abort 25 | "Abort this evaluation." 26 | (constantly nil) 27 | #(assert false "This should never run"))) 28 | 29 | (defn prompt-for-restarts [ex restarts] 30 | (if (seq restarts) 31 | (let [index (promise) 32 | id (uuid) 33 | {:keys [transport session] :as msg} e/*msg*] 34 | (swap! awaiting-restarts assoc id index) 35 | (try 36 | (t/send transport 37 | (response-for msg 38 | :id id 39 | :type "restart/prompt" 40 | :error (loop [ex ex] 41 | (if-let [cause (.getCause ex)] 42 | (recur cause) 43 | (or (.getMessage ex) 44 | (.getSimpleName (.getClass ex))))) 45 | :causes (analyze-causes ex pprint) 46 | :restarts (mapv (fn [{:keys [name description]}] 47 | [(pr-str name) description]) 48 | restarts))) 49 | (loop [] 50 | (let [idx (deref index 100 :timeout)] 51 | (cond 52 | (Thread/interrupted) (throw (InterruptedException.)) 53 | (= idx :timeout) (recur) 54 | :else (or (get restarts idx) 55 | (if (= idx "unhandled") unhandled) 56 | (if (= idx "abort") abort))))) 57 | (finally 58 | (swap! awaiting-restarts dissoc id)))) 59 | nil)) 60 | 61 | (defn prompt-for-input 62 | ([prompt] 63 | (prompt-for-input prompt nil)) 64 | ([prompt type & args] 65 | (let [timeout (Object.) 66 | input (promise) 67 | id (uuid) 68 | {:keys [transport session] :as msg} e/*msg*] 69 | (swap! awaiting-prompts assoc id input) 70 | (try 71 | (t/send transport 72 | (response-for msg 73 | :id id 74 | :type "restart/ask" 75 | :prompt prompt 76 | :options {:type type 77 | :args args})) 78 | (loop [] 79 | (let [value (deref input 100 timeout)] 80 | (cond 81 | (Thread/interrupted) (throw (InterruptedException.)) 82 | (= value timeout) (recur) 83 | :else (value)))) 84 | (finally 85 | (swap! awaiting-prompts dissoc id)))))) 86 | 87 | (def unbound-var-messages ["Unable to resolve symbol: " 88 | "Unable to resolve var: " 89 | "No such var: "]) 90 | (defn extract-unbound-var-name [ex] 91 | (and (instance? clojure.lang.Compiler$CompilerException ex) 92 | (let [message (.getMessage (.getCause ex))] 93 | (some #(when (.startsWith message %) 94 | (read-string (.substring message (count %)))) 95 | unbound-var-messages)))) 96 | 97 | (def missing-class-messages ["Unable to resolve classname: "]) 98 | (defn extract-missing-class-name [ex] 99 | (and (instance? clojure.lang.Compiler$CompilerException ex) 100 | (let [message (.getMessage (.getCause ex))] 101 | (some #(when (.startsWith message %) 102 | (read-string (.substring message (count %)))) 103 | missing-class-messages)))) 104 | 105 | (def unknown-ns-messages ["No such namespace: "]) 106 | (defn extract-ns-name [ex] 107 | (and (instance? clojure.lang.Compiler$CompilerException ex) 108 | (let [message (.getMessage (.getCause ex))] 109 | (some #(when (.startsWith message %) 110 | (read-string (.substring message (count %)))) 111 | unknown-ns-messages)))) 112 | 113 | (def non-dynamic-var-messages ["Can't dynamically bind non-dynamic var: "]) 114 | (defn extract-non-dynamic-var-name [ex] 115 | (and (instance? IllegalStateException ex) 116 | (let [message (.getMessage ex)] 117 | (some #(when (.startsWith message %) 118 | (read-string (.substring message (count %)))) 119 | non-dynamic-var-messages)))) 120 | 121 | (defn namespaces-with-var [sym] 122 | (for [namespace (all-ns) 123 | :let [v (ns-resolve namespace sym)] 124 | :when (-> v meta :ns (= namespace))] 125 | namespace)) 126 | 127 | (defn make-restarts [run retry-msg] 128 | (fn [ex] 129 | (concat 130 | (when-let [var (extract-unbound-var-name ex)] 131 | (concat 132 | [(make-restart :define 133 | (str "Provide a value for " (pr-str var) " and retry the evaluation.") 134 | #(dgu/read-and-eval-form ex) 135 | (fn [value] 136 | (if-let [ns (namespace var)] 137 | (intern (find-ns (symbol ns)) 138 | (symbol (name var)) 139 | value) 140 | (intern *ns* var value)) 141 | (run)))] 142 | (if-let [alias (namespace var)] 143 | [(make-restart :refer 144 | (str "Provide a namespace to refer as " (str alias) " and retry the evaluation.") 145 | #(list (read-string (prompt-user "Provide a namespace name: " 146 | :options 147 | (map (comp str ns-name) 148 | (namespaces-with-var (symbol (name var))))))) 149 | (fn [ns] 150 | (require [ns :as (symbol alias)]) 151 | (run)))] 152 | [(make-restart :refer 153 | (str "Provide a namespace to refer " (str var) " from and retry the evaluation.") 154 | #(list (read-string (prompt-user "Provide a namespace name: " 155 | :options 156 | (map (comp str ns-name) 157 | (namespaces-with-var var))))) 158 | (fn [ns] 159 | (require [ns :refer [var]]) 160 | (run)))]))) 161 | (when-let [class (extract-missing-class-name ex)] 162 | [(make-restart :import 163 | "Provide a package to import the class from and retry the evaluation." 164 | #(dgu/read-form ex) 165 | (fn [package] 166 | (.importClass *ns* (clojure.lang.RT/classForName (str (name package) "." (name class)))) 167 | (run)))]) 168 | (when-let [ns (extract-ns-name ex)] 169 | [(make-restart :require 170 | (str "Require the " (pr-str ns) " namespace and retry the evaluation.") 171 | (constantly nil) 172 | #(do (require ns) 173 | (run))) 174 | (make-restart :require-alias 175 | (str "Provide a namespace name, alias it as " (pr-str ns) " and retry the evaluation.") 176 | #(dgu/read-form ex) 177 | (fn [orig-ns] 178 | (require [orig-ns :as ns]) 179 | (run))) 180 | (make-restart :create 181 | (str "Create the " (pr-str ns) " namespace and retry the evaluation.") 182 | (constantly nil) 183 | #(do (create-ns ns) 184 | (run)))]) 185 | [(make-restart :retry 186 | retry-msg 187 | (constantly nil) 188 | run)]))) 189 | 190 | (defmacro with-recursive-body [name bindings & body] 191 | `(letfn [(~name ~(mapv first (partition 2 bindings)) 192 | ~@body)] 193 | (~name ~@(map second (partition 2 bindings))))) 194 | 195 | (defn- prompt [ex] 196 | (if-let [restart (prompt-for-restarts ex (dgu/list-restarts))] 197 | (cond 198 | (= restart unhandled) (dgu/unhandle-exception ex) 199 | (= restart abort) (throw (ThreadDeath.)) 200 | :else (try 201 | (apply dgu/invoke-restart restart 202 | (binding [prompt-user prompt-for-input] 203 | ((:make-arguments restart)))) 204 | (catch Exception _ 205 | (prompt ex)))) 206 | (throw ex))) 207 | 208 | (defn call-with-interactive-handler [body-fn] 209 | (dgu/without-handling 210 | (with-handlers [(Throwable ex (prompt ex))] 211 | (with-recursive-body retry [] 212 | (call-with-restarts (make-restarts retry "Retry the evaluation.") body-fn))))) 213 | 214 | (defmacro with-interactive-handler [& body] 215 | `(call-with-interactive-handler (^:once fn [] ~@body))) 216 | 217 | (defn handled-eval [form] 218 | (let [eval (if (::eval e/*msg*) 219 | (resolve (symbol (::eval e/*msg*))) 220 | clojure.core/eval)] 221 | (with-interactive-handler (eval form)))) 222 | 223 | (defmacro wrapper 224 | {:style/indent [1]} 225 | [& bodies] 226 | (let [bodies (if (vector? (first bodies)) 227 | (list bodies) 228 | bodies) 229 | wrapped (gensym "wrapped")] 230 | `(fn [f#] 231 | (let [~wrapped (or (::original (meta f#)) 232 | f#)] 233 | (with-meta (fn 234 | ~@(for [[args & body] bodies] 235 | (list (vec (next args)) 236 | `(let [~(first args) ~wrapped] 237 | ~@body)))) 238 | {::original ~wrapped}))))) 239 | 240 | (defmacro defwrapper [name & bodies] 241 | {:style/indent [:defn]} 242 | `(def ~name (wrapper ~@bodies))) 243 | 244 | (defwrapper handled-future-call [future-call f] 245 | (if e/*msg* 246 | (future-call #(call-with-interactive-handler f)) 247 | (future-call f))) 248 | 249 | (defn handled-agent-fn [f] 250 | (fn [state & args] 251 | (with-interactive-handler 252 | (with-recursive-body retry [state state, args args] 253 | (with-restarts [(:ignore [] 254 | :describe "Ignore this action and leave the agent's state unchanged." 255 | state) 256 | (:ignore-and-replace [state] 257 | :describe "Ignore this action and provide a new state for the agent." 258 | :arguments #'dgu/read-form 259 | state) 260 | (:replace [state] 261 | :describe "Provide a new state for the agent, then retry the action." 262 | :arguments #'dgu/read-form 263 | (retry state args))] 264 | (apply f state args)))))) 265 | 266 | (defwrapper handled-send-via [send-via executor agent f & args] 267 | (with-recursive-body retry [] 268 | (with-restarts [(:restart [] 269 | :applicable? #(.startsWith (.getMessage %) "Agent is failed") 270 | :describe "Restart the agent and retry this action dispatch." 271 | (restart-agent agent @agent) 272 | (retry)) 273 | (:restart-with-state [state] 274 | :applicable? #(.startsWith (.getMessage %) "Agent is failed") 275 | :describe "Provide a new state to restart the agent and retry this action dispatch." 276 | :arguments #'dgu/read-form 277 | (restart-agent agent state) 278 | (retry))] 279 | (apply send-via 280 | executor 281 | agent 282 | (handled-agent-fn f) 283 | args)))) 284 | 285 | (defwrapper restartable-reader [reader filename & opts] 286 | (with-recursive-body run [filename filename] 287 | (with-restarts [(:filename [filename] 288 | :describe "Provide a filename to open." 289 | :applicable? #(instance? java.io.FileNotFoundException %) 290 | :arguments (fn [ex] (list (prompt-user "Filename to open: " :file))) 291 | (run filename))] 292 | (apply reader filename opts)))) 293 | 294 | (defwrapper restartable-ns-resolve 295 | ([ns-resolve ns sym] 296 | (clojure.core/ns-resolve ns nil sym)) 297 | ([ns-resolve ns env sym] 298 | (with-restarts [(:require [] 299 | :describe (str "Require " ns " and retry.") 300 | (require ns) 301 | (clojure.core/ns-resolve ns env sym))] 302 | (ns-resolve ns env sym)))) 303 | 304 | (defwrapper restartable-push-thread-bindings [push-thread-bindings bindings] 305 | ;; This function is pretty specialised, because it needs to be aware 306 | ;; of some internals that usually wouldn't be a concern. Wrapping 307 | ;; this function affects every time dynamic vars are bound, which is 308 | ;; something at dgu does a lot of. So, we can't dynamically bind our 309 | ;; restarts until we've already failed. This should work fine in 310 | ;; practice, because push-thread-bindings has no restarts of its 311 | ;; own. 312 | (try 313 | (push-thread-bindings bindings) 314 | (catch IllegalStateException ex 315 | (if-let [var (extract-non-dynamic-var-name ex)] 316 | (with-restarts [(:make-dynamic [] 317 | :describe (str "Make " (pr-str var) " dynamic and retry the evaluation.") 318 | (.setDynamic (resolve var) true) 319 | (clojure.core/push-thread-bindings bindings))] 320 | (throw ex)) 321 | (throw ex))))) 322 | 323 | (defn run-with-restart-stuff [h {:keys [op code eval] :as msg}] 324 | (with-redefs [e/queue-eval (fn [session ^Executor executor f] 325 | (.execute executor #(f)))] 326 | (h (assoc msg 327 | :eval "wingman.nrepl/handled-eval" 328 | ::eval (:eval msg))))) 329 | 330 | (defn choose-restart [{:keys [id restart transport] :as msg}] 331 | (let [promise (get (deref awaiting-restarts) id)] 332 | (if promise 333 | (do (deliver promise restart) 334 | (t/send transport (response-for msg :status :done))) 335 | (t/send transport (response-for msg :status :error))))) 336 | 337 | (defn answer-prompt [{:keys [id input error transport] :as msg}] 338 | (let [promise (get (deref awaiting-prompts) id)] 339 | (if promise 340 | (do (deliver promise (if input 341 | #(do input) 342 | #(throw (Exception. "Input cancelled")))) 343 | (t/send transport (response-for msg :status :done))) 344 | (t/send transport (response-for msg :status :error))))) 345 | 346 | (defwrapper unhandled-test-var [test-var v] 347 | (dgu/without-handling 348 | (test-var v))) 349 | 350 | (def wrappers 351 | {#'clojure.core/future-call handled-future-call 352 | #'clojure.core/send-via handled-send-via 353 | #'clojure.core/ns-resolve restartable-ns-resolve 354 | #'clojure.core/push-thread-bindings restartable-push-thread-bindings 355 | #'clojure.java.io/reader restartable-reader}) 356 | 357 | (defn wrap-core! [] 358 | (doseq [[v w] wrappers] 359 | (alter-var-root v w))) 360 | 361 | (defn unwrap-core! [] 362 | (doseq [[v _] wrappers] 363 | (alter-var-root v (comp ::original meta)))) 364 | 365 | (when-let [ns (find-ns 'user)] 366 | (binding [*ns* ns] 367 | (refer 'wingman.nrepl 368 | :only ['wrap-core! 'unwrap-core!] 369 | :rename {'wrap-core! 'dgu-wrap-core! 370 | 'unwrap-core! 'dgu-unwrap-core!}))) 371 | 372 | ;; This one we do automatically, because it's ensuring that we don't 373 | ;; interact with it. If we don't have this, then tests can trigger 374 | ;; restarts, which feels wrong. Automated tests shouldn't be 375 | ;; interactive. 376 | (alter-var-root #'clojure.test/test-var unhandled-test-var) 377 | 378 | (defn handle-restarts [h] 379 | (fn [msg] 380 | (case (:op msg) 381 | "eval" (run-with-restart-stuff h msg) 382 | "restart/choose" (choose-restart msg) 383 | "restart/answer" (answer-prompt msg) 384 | (h msg)))) 385 | 386 | (def middleware `[handle-restarts]) 387 | 388 | (set-descriptor! #'handle-restarts 389 | {:requires #{#'session} 390 | :expects #{"eval"} 391 | :handles {"restart/choose" {:doc "Select a restart" 392 | :requires {"index" "The index of the reset to choose"} 393 | :optional {} 394 | :returns {}} 395 | "restart/answer" {:doc "Provide input to a restart prompt" 396 | :requires {"input" "The input provided to the restart handler"} 397 | :optional {} 398 | :returns {}}}}) 399 | -------------------------------------------------------------------------------- /nrepl/src/wingman/nrepl/plugin.clj: -------------------------------------------------------------------------------- 1 | (ns wingman.nrepl.plugin) 2 | 3 | (defn middleware [project] 4 | (-> project 5 | (update-in [:dependencies] 6 | (fnil into []) 7 | [['wingman/wingman.nrepl "0.3.1"]]) 8 | (update-in [:repl-options :nrepl-middleware] 9 | (fnil into []) 10 | (do (require 'wingman.nrepl) 11 | @(resolve 'wingman.nrepl/middleware))))) 12 | -------------------------------------------------------------------------------- /test-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd base; lein test; cd ..; 3 | cd wingman; lein test; cd ..; 4 | cd nrepl; lein test; cd ..; 5 | -------------------------------------------------------------------------------- /wingman/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | -------------------------------------------------------------------------------- /wingman/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Carlo Zancanaro 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 | -------------------------------------------------------------------------------- /wingman/project.clj: -------------------------------------------------------------------------------- 1 | (defproject wingman "0.3.1-SNAPSHOT" 2 | :description "Restartable exception handling for Clojure" 3 | :url "https://github.com/czan/wingman" 4 | :license {:name "MIT" 5 | :url "https://opensource.org/licenses/MIT"} 6 | :dependencies [[org.clojure/clojure "1.8.0"] 7 | [wingman/base "0.3.0"]] 8 | :repositories [["releases" {:url "https://clojars.org/repo" 9 | :creds :gpg}]]) 10 | -------------------------------------------------------------------------------- /wingman/src/wingman/core.clj: -------------------------------------------------------------------------------- 1 | (ns wingman.core 2 | (:require [wingman.base :as dgu])) 3 | 4 | (defn list-restarts 5 | {:doc (:doc (meta #'dgu/list-restarts))} 6 | [] 7 | (dgu/list-restarts)) 8 | 9 | (defn unhandle-exception 10 | {:doc (:doc (meta #'dgu/unhandle-exception))} 11 | [ex] 12 | (dgu/unhandle-exception ex)) 13 | 14 | (defn rethrow 15 | {:doc (:doc (meta #'dgu/rethrow))} 16 | [ex] 17 | (dgu/rethrow ex)) 18 | 19 | (defn find-restarts 20 | "Return a list of all dynamically-bound restarts with the provided 21 | name. If passed an instance of `Restart`, search by equality." 22 | [restart] 23 | (if (dgu/restart? restart) 24 | (filter #(= % restart) (dgu/list-restarts)) 25 | (filter #(= (:name %) restart) (dgu/list-restarts)))) 26 | 27 | (defn find-restart 28 | "Return the first dynamically-bound restart with the provided name. 29 | If passed an instance of `Restart`, search by equality." 30 | [restart] 31 | (first (find-restarts restart))) 32 | 33 | (defn invoke-restart 34 | "Invoke the given restart, after validating that it is active. If an 35 | object satisfying `dgu/restart?` is passed, check that it is active, 36 | otherwise invoke the most recently defined restart with the given 37 | name. 38 | 39 | This function will never return a value. This function will always 40 | throw an exception." 41 | [name & args] 42 | (if-let [restart (find-restart name)] 43 | (apply dgu/invoke-restart restart args) 44 | (throw (IllegalArgumentException. (str "No restart registered for " name))))) 45 | 46 | (defn read-form 47 | "Read an unevaluated form from the user, and return it for use as a 48 | restart's arguments;" 49 | [ex] 50 | [(try (read-string (dgu/prompt-user "Enter a value to be used (unevaluated): " :form)) 51 | (catch Exception _ 52 | (throw ex)))]) 53 | 54 | (defn read-and-eval-form 55 | "Read a form from the user, and return the evaluated result for use 56 | as a restart's arguments." 57 | [ex] 58 | [(eval (try (read-string (dgu/prompt-user "Enter a value to be used (evaluated): " :form)) 59 | (catch Exception _ 60 | (throw ex))))]) 61 | 62 | (defmacro without-handling 63 | "Run `body` with no active handlers and no current restarts. Any 64 | exceptions raised by `thunk` will propagate normally. Note that an 65 | exception raised by this call will be handled normally." 66 | [& body] 67 | `(dgu/call-without-handling (fn [] ~@body))) 68 | 69 | (defmacro with-restarts 70 | "Run `body`, providing `restarts` as dynamic restarts to handle 71 | errors which occur while executing `body`. 72 | 73 | For example, a simple restart to use a provided value would look 74 | like this: 75 | 76 | (with-restarts [(:use-value [value] value)] 77 | (/ 1 0)) 78 | 79 | This would allow a handler to invoke `(invoke-restart :use-value 10)` 80 | to recover from this exception, and to return `10` as the result of 81 | the `with-restarts` form. 82 | 83 | In addition, restarts can have three extra attributes defined: 84 | 85 | 1. `:applicable?` specifies a predicate which tests whether this 86 | restart is applicable to this exception type. It defaults 87 | to `(fn [ex] true)`. 88 | 89 | 2. `:describe` specifies a function which will convert the exception 90 | into an explanation of what this restart will do. As a shortcut, you 91 | may use a string literal instead, which will be converted into a 92 | function returning that string. It defaults to `(fn [ex] \"\")`. 93 | 94 | 3. `:arguments` specifies a function which will return arguments for 95 | this restart. This function is only ever used interactively, and 96 | thus should prompt the user for any necessary information to invoke 97 | this restart. It defaults to `(fn [ex] nil)`. 98 | 99 | Here is an example of the above restart using these attributes: 100 | 101 | (with-restarts [(:use-value [value] 102 | :describe \"Provide a value to use.\" 103 | :arguments #'read-unevaluated-value 104 | value)] 105 | (/ 1 0)) 106 | 107 | Restarts are invoked in the same dynamic context in which they were 108 | defined. The stack is unwound to the level of the `with-restarts` 109 | form, and the restart is invoked. 110 | 111 | Multiple restarts with the same name can be defined, but the 112 | \"closest\" one will be invoked by a call to `invoke-restart`. 113 | 114 | Restart names can be any value that is not an instance of 115 | `wingman.base.Restart`, but it is recommended to use keywords 116 | as names." 117 | {:style/indent [1 [[:defn]] :form]} 118 | [restarts & body] 119 | (let [ex (gensym "ex")] 120 | `(dgu/call-with-restarts 121 | (fn [~ex] 122 | (remove nil? 123 | ~(mapv (fn [restart] 124 | (if (symbol? restart) 125 | restart 126 | (let [[name args & body] restart] 127 | (loop [body body 128 | describe `(constantly "") 129 | applicable? `(constantly true) 130 | make-arguments `(constantly nil)] 131 | (cond 132 | (= (first body) :describe) 133 | (recur (nnext body) 134 | (second body) 135 | applicable? 136 | make-arguments) 137 | 138 | (= (first body) :applicable?) 139 | (recur (nnext body) 140 | describe 141 | (second body) 142 | make-arguments) 143 | 144 | (= (first body) :arguments) 145 | (recur (nnext body) 146 | describe 147 | applicable? 148 | (second body)) 149 | 150 | :else 151 | `(when (~applicable? ~ex) 152 | (dgu/make-restart 153 | ~name 154 | (let [d# ~describe] 155 | (if (string? d#) 156 | d# 157 | (d# ~ex))) 158 | (fn [] 159 | (~make-arguments ~ex)) 160 | (fn ~(vec args) 161 | ~@body)))))))) 162 | restarts))) 163 | (^:once fn [] ~@body)))) 164 | 165 | (defmacro with-handlers 166 | "Run `body`, using `handlers` to handle any exceptions which are 167 | raised during `body`'s execution. 168 | 169 | For example, here is how to use `with-handlers` to replace 170 | try/catch: 171 | 172 | (with-handlers [(Exception ex (.getMessage ex))] 173 | (/ 1 0)) 174 | ;; => \"Divide by zero\" 175 | 176 | Similarly to try/catch, multiple handlers can be defined for 177 | different exception types, and the first matching handler will be 178 | run to handle the exception. 179 | 180 | See `wingman.base/call-with-handler` for more details about 181 | handler functions." 182 | {:style/indent [1 [[:defn]] :form]} 183 | [handlers & body] 184 | (let [ex-sym (gensym "ex")] 185 | `(dgu/call-with-handler 186 | (fn [~ex-sym] 187 | (cond 188 | ~@(mapcat (fn [[type arg & body]] 189 | (if (seq body) 190 | `((instance? ~type ~ex-sym) 191 | (let [~arg ~ex-sym] 192 | ~@body)) 193 | `((instance? ~type ~ex-sym) 194 | nil))) 195 | handlers) 196 | :else (dgu/rethrow ~ex-sym))) 197 | (^:once fn [] ~@body)))) 198 | 199 | (defmacro try' 200 | "Like `try`, but registers handlers instead of normal catch clauses. 201 | Restarts can be invoked from the defined handlers. 202 | 203 | For example: 204 | 205 | (try' 206 | (send a conj 30) 207 | (catch Exception ex 208 | (invoke-restart :restart-with-state nil)) 209 | (finally 210 | (send a conj 40))) 211 | 212 | See `with-handlers` for more detail." 213 | {:style/indent [0]} 214 | [& body-and-clauses] 215 | (letfn [(has-head? [head form] 216 | (and (seq? form) 217 | (= (first form) head))) 218 | (wrap-finally [form finally] 219 | (if finally 220 | `(try ~form ~finally) 221 | form))] 222 | (loop [stage :body 223 | body [] 224 | clauses [] 225 | finally nil 226 | forms body-and-clauses] 227 | (case stage 228 | :body (if (empty? forms) 229 | `(do ~@body) 230 | (condp has-head? (first forms) 231 | 'catch (recur :catch body clauses finally forms) 232 | (recur :body (conj body (first forms)) clauses finally (next forms)))) 233 | :catch (if (empty? forms) 234 | (recur :done body clauses finally forms) 235 | (condp has-head? (first forms) 236 | 'catch (recur :catch body (conj clauses (next (first forms))) finally (next forms)) 237 | 'finally (recur :done body clauses (first forms) (next forms)) 238 | (throw (IllegalArgumentException. 239 | "After the first catch, everything must be a catch or a finally in a try' form.")))) 240 | :done (if (empty? forms) 241 | (wrap-finally 242 | `(with-handlers ~clauses 243 | ~@body) 244 | finally) 245 | (throw (IllegalArgumentException. 246 | "Can't have anything following the finally clause in a try' form."))))))) 247 | -------------------------------------------------------------------------------- /wingman/test/wingman/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns wingman.core-test 2 | (:require [clojure.test :refer :all] 3 | [wingman.core :refer :all])) 4 | 5 | (use-fixtures :once (fn [f] 6 | (without-handling 7 | (f)))) 8 | 9 | (deftest handlers-should-use-the-first-matching-clause 10 | (is (= :value 11 | (with-handlers [(ArithmeticException ex 12 | :value) 13 | (Exception ex)] 14 | (/ 1 0)))) 15 | (is (= nil 16 | (with-handlers [(Exception ex) 17 | (ArithmeticException ex 18 | :value)] 19 | (/ 1 0))))) 20 | 21 | (deftest handlers-should-work-even-above-try-catch 22 | (is (= 11 23 | (with-handlers [(Exception ex 24 | (invoke-restart :use-value 10))] 25 | (try 26 | (+ (with-restarts [(:use-value [value] 27 | value)] 28 | (throw (Exception.))) 29 | 1) 30 | (catch Exception ex)))))) 31 | 32 | (deftest restarts-from-exceptions-should-work 33 | (is (= 10 34 | (with-handlers [(Exception ex 35 | (invoke-restart :use-value 10))] 36 | (with-restarts [(:use-value [value] value)] 37 | (throw (RuntimeException.))))))) 38 | 39 | (deftest restarts-should-bubble-up-if-unhandled 40 | (is (= 10 41 | (with-handlers [(Exception ex (invoke-restart :use-value 10))] 42 | (with-handlers [(ArithmeticException ex)] 43 | (with-restarts [(:use-value [value] value)] 44 | (throw (RuntimeException.))))))) 45 | (is (= 10 46 | (with-handlers [(Exception ex (invoke-restart :use-value 10))] 47 | (with-handlers [(ArithmeticException ex (rethrow ex))] 48 | (with-restarts [(:use-value [value] value)] 49 | (throw (ArithmeticException.)))))))) 50 | 51 | (deftest restarts-should-use-the-most-specific-named-restart 52 | (is (= 10 53 | (with-handlers [(Exception ex (invoke-restart :use-default))] 54 | (with-restarts [(:use-default [] 13)] 55 | (with-restarts [(:use-default [] 10)] 56 | (throw (RuntimeException.)))))))) 57 | 58 | (deftest restarts-should-restart-from-the-right-point 59 | (is (= 0 60 | (with-handlers [(Exception ex (invoke-restart :use-zero))] 61 | (with-restarts [(:use-zero [] 0)] 62 | (inc (with-restarts [(:use-one [] 1)] 63 | (throw (Exception.))))))))) 64 | 65 | (deftest handlers-returning-values-should-return-at-the-right-place 66 | (is (= 2 67 | (with-handlers [(RuntimeException ex 2)] 68 | (+ 1 (with-handlers [(ArithmeticException ex)] 69 | (throw (RuntimeException.)))))))) 70 | 71 | (deftest handlers-should-not-modify-exceptions-when-not-handling 72 | (let [ex (Exception.)] 73 | (is (= ex 74 | (try 75 | (with-handlers [(ArithmeticException ex 10)] 76 | (throw ex)) 77 | (catch Exception e e)))) 78 | (is (= ex 79 | (try 80 | (with-handlers [(ArithmeticException ex 10)] 81 | (throw ex)) 82 | (catch Exception e e)))))) 83 | 84 | (deftest restarts-should-not-modify-exceptions-when-not-handling 85 | (let [ex (Exception.)] 86 | (is (= ex 87 | (try 88 | (with-restarts [(:use-value [value] value)] 89 | (throw ex)) 90 | (catch Exception e e)))) 91 | (is (= ex 92 | (try 93 | (with-restarts [(:use-value [value] value)] 94 | (with-restarts [(:use-value [value] value)] 95 | (throw ex))) 96 | (catch Exception e e)))))) 97 | 98 | (deftest handlers-throwing-exceptions-should-be-catchable 99 | (is (= Exception 100 | (try 101 | (with-handlers [(RuntimeException ex (throw (Exception.)))] 102 | (throw (RuntimeException.))) 103 | (catch Exception ex 104 | (.getClass ex))))) 105 | (is (= Exception 106 | (try 107 | (with-handlers [(RuntimeException ex (throw (Exception.)))] 108 | (throw (RuntimeException.))) 109 | (catch Exception ex 110 | (.getClass ex))))) 111 | (is (= Exception 112 | (try 113 | (with-handlers [(RuntimeException ex (throw (Exception.)))] 114 | (with-restarts [(:use-value [value] value)] 115 | (throw (RuntimeException.)))) 116 | (catch Exception ex 117 | (.getClass ex))))) 118 | (is (= Exception 119 | (try 120 | (with-handlers [(RuntimeException ex (throw (Exception.)))] 121 | (with-restarts [(:use-value [value] value)] 122 | (throw (RuntimeException.)))) 123 | (catch Exception ex 124 | (.getClass ex))))) 125 | (is (= Exception 126 | (with-handlers [(Exception ex 10)] 127 | (try 128 | (with-handlers [(RuntimeException ex (throw (Exception.)))] 129 | (with-restarts [(:use-value [value] value)] 130 | (throw (RuntimeException.)))) 131 | (catch Exception ex 132 | (.getClass ex))))))) 133 | 134 | (deftest restarts-should-go-away-during-handler 135 | (is (= 0 136 | (with-handlers [(Exception ex 137 | (invoke-restart :try-1))] 138 | (with-restarts [(:try-1 [] 139 | (count (list-restarts)))] 140 | (throw (RuntimeException.))))))) 141 | --------------------------------------------------------------------------------