pax_global_header00006660000000000000000000000064141102541250014505gustar00rootroot0000000000000052 comment=8b1ac896ae975e39ebb7bbf7a6e4d044307027a3 murphy-0.5.2/000077500000000000000000000000001411025412500130355ustar00rootroot00000000000000murphy-0.5.2/.github/000077500000000000000000000000001411025412500143755ustar00rootroot00000000000000murphy-0.5.2/.github/contributing.md000066400000000000000000000001231411025412500174220ustar00rootroot00000000000000 Please see [the Contributing section in the README](../README.org#contributing). murphy-0.5.2/.github/pull_request_template.md000066400000000000000000000007771411025412500213510ustar00rootroot00000000000000 Thanks for your efforts -- please make sure each commit includes a Signed-off-by: Someone line in the commit message that matches the "Author" line so that we'll be able to include your work in the project. Including this header in your patches signifies that you are licensing your changes under the terms described in the [SIGNED-OFF-BY file](../SIGNED-OFF-BY). If you like, git can can add the appropriate pseudo-header for you via the `--signoff` argument to commit, amend, etc. murphy-0.5.2/.github/workflows/000077500000000000000000000000001411025412500164325ustar00rootroot00000000000000murphy-0.5.2/.github/workflows/main.yaml000066400000000000000000000012011411025412500202340ustar00rootroot00000000000000 name: main on: [push, pull_request] defaults: run: shell: bash jobs: main: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 with: persist-credentials: false - run: lein deps - name: Test against JDK 11 run: | set -xue export JAVA_HOME=$(echo /usr/lib/jvm/java-11-*) export PATH="$JAVA_HOME/bin:$PATH" lein do clean, check-all - name: Test against JDK 8 run: | set -xue export JAVA_HOME=$(echo /usr/lib/jvm/java-8-*) export PATH="$JAVA_HOME/bin:$PATH" lein do clean, check-all murphy-0.5.2/.gitignore000066400000000000000000000000731411025412500150250ustar00rootroot00000000000000*~ .hg/ .hgignore /.eastwood /.lein-* /.nrepl-port /target murphy-0.5.2/README.org000066400000000000000000000070421411025412500145060ustar00rootroot00000000000000# -*-org-*- #+TITLE: murphy (What could go wrong?) *NOTE*: While we'd love for people to try this out, as long as the version is less than 1.0, we reserve the right (which we may well exercise) to change the API. Consider: #+BEGIN_SRC clojure (try (print-masterpiece) ; Throws lp0-on-fire (finally (turn-off-light))) ; Throws switch-broken #+END_SRC At this point, you'll know that you need to fix your light switch, but have no idea that your printer is on fire. That's because a throw from a finally clause simply discards any pending exception. To preserve all of the failure information, we can use [[https://docs.oracle.com/javase/8/docs/api/java/lang/Throwable.html#addSuppressed-java.lang.Throwable-][exception suppression]], which is provided by newer versions of the JDK and write this instead: #+BEGIN_SRC clojure (try! (print-masterpiece) ; Throws lp0-on-fire (finally (turn-off-light))) ; Throws lp0-on-fire, with switch-broken ; available via (.getSuppressed lp0-on-fire). #+END_SRC As mentioned in the exception suppression documentation linked above, whether or not a suppressed exception is recorded or discarded depends on the constructor arguments for the original exception. In any case, if an exception does contain suppressed exceptions, they should be displayed by the normal top-level JVM exception handler, assuming they make it that far. * Facilities ** (try! ...) Behaves like the normal try, except that it supports multiple finally clauses which are executed in order, and if any given finally clause throws while an exception is already pending, the new exception will be suppressed via the Throwable addSuppressed method." ** (with-open! ...) Behaves like the normal with-open except that exceptions thrown by any of the close methods will be suppressed. And of course any exception pending at the end of the cleanup will be thrown. ** (with-final ...) Configurable cleanup, suppressing exceptions, either on :error (any throw) or :always: #+BEGIN_SRC clojure (with-final [foo (.acquire lock) :always .release bar (open something) :always .close baz {:foo foo :bar bar}] ...) #+END_SRC clojure :error can be particularly useful in cases where normal cleanup must happen in a completely different scope. #+BEGIN_SRC clojure (defn start-server [...] (with-final [foo (open-foo ...) :error .close bar (connect-bar ...) :error .disconnect ...] ...do many things... {:foo foo :bar bar ...})) (defn stop-server [info] (with-final [_ (:foo info) :always .close _ (:bar info) :always .disconnect ...] true) #+END_SRC clojure * Contributing You can run all of the existing tests (including ~lein test~) with ~lein check-all~. All patches must be "signed off" by the author before official inclusion (see ./SIGNED-OFF-BY). If you like, git can can add the appropriate pseudo-header for you via the --signoff argument to commit, amend, etc. * License This project is free software; you can redistribute it and/or modify it under the terms of (at your option) either of the following two licenses: 1) The GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1, or (at your option) any later version: https://www.gnu.org/licenses/lgpl-2.1.html 2) The Eclipse Public License; either version 1.0 or (at your option) any later version: http://www.eclipse.org/legal/epl-v10.html Copyright © 2017-2018, 2020-2021 Rob Browning murphy-0.5.2/SIGNED-OFF-BY000066400000000000000000000011361411025412500146520ustar00rootroot00000000000000 Patches to murphy should have a Signed-off-by: header. If you include this header in your patches, this signifies that you are licensing your patch to be used under the following terms: This project is free software; you can redistribute it and/or modify it under the terms of (at your option) either of the following two licenses: 1) The GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1, or (at your option) any later version 2) The Eclipse Public License; either version 1.0 or (at your option) any later version. murphy-0.5.2/eastwood.clj000066400000000000000000000000001411025412500153420ustar00rootroot00000000000000murphy-0.5.2/project.clj000066400000000000000000000012641411025412500152000ustar00rootroot00000000000000(defproject murphy "0.5.2" :description "Clojure library for better handling of bad situations." :url "https://gitlab.com/clj-murphy/murphy" :licenses [{:name "GNU Lesser General Public License, version 2.1 or newer" :url "https://www.gnu.org/licenses/lgpl-2.1.html"} {:name "Eclipse Public License 1.0 or newer" :url "http://www.eclipse.org/legal/epl-v10.html"}] :dependencies [[org.clojure/clojure "1.10.2"]] :profiles {:eastwood {:plugins [[jonase/eastwood "0.9.7"]]}} :eastwood {:config-files ["eastwood.clj"]} :aliases {"eastwood" ["with-profile" "+eastwood" "eastwood"] "check-all" ["do" "check," "eastwood," "test"]}) murphy-0.5.2/src/000077500000000000000000000000001411025412500136245ustar00rootroot00000000000000murphy-0.5.2/src/murphy.clj000066400000000000000000000114251411025412500156450ustar00rootroot00000000000000(ns murphy) (defmacro try! "Exactly like try, except that it supports multiple finally clauses which will be executed in order, and if any given finally clause throws while an exception is already pending, the new exception will be suppressed via the Throwable addSuppressed method." [& forms] (let [[others finals] (split-with #(or (not (list? %)) (not= 'finally (first %))) forms)] (when-not (every? #(= 'finally (first %)) finals) (throw (RuntimeException. "finally clauses must be last"))) (loop [[[_finally_ & fin-body] & finals] finals expansion `(try ~@others)] (if-not _finally_ expansion (recur finals `(let [fin# (fn [] ~@fin-body) result# (try ~expansion (catch Throwable ex# (try (fin#) (catch Throwable ex2# (.addSuppressed ex# ex2#))) (throw ex#)))] (fin#) result#)))))) (defn- validate-with-final-bindings [bindings] (assert (vector? bindings)) (loop [bindings bindings] (case (count bindings) 0 true 2 true ; final "name init" pair (1 3) (throw (RuntimeException. "Unexpected end of with-final bindings")) (let [[name init maybe-kind & remainder] bindings] (if-not (#{:always :error} maybe-kind) (recur (cons maybe-kind remainder)) (let [[action & remainder] remainder] (recur remainder))))))) (defmacro with-final "The bindings must be a vector of elements, each of which is either \"name init\", \"name init :always action\", or \"name init :error action\". Binds each name to the value of the corresponding init, and behaves exactly as if each subsequent name were guarded by a nested try! form that calls (action name) in its finally clause when :always is specified, or (action name) in a Throwable handler when :error is specified. Suppresses any exceptions thrown by the actions via the Throwable addSuppressed method." [bindings & body] (validate-with-final-bindings bindings) (if (empty? bindings) `(do ~@body) (if (= 2 (count bindings)) ;; "name init" (let [[bind init] bindings] `(let [~bind ~init] ~@body)) ;; either "name init" or "name init kind action" (let [[bind init maybe-kind maybe-action] bindings] (if-not (#{:always :error} maybe-kind) `(let [~bind ~init] (with-final ~(subvec bindings 2) ~@body)) (let [action maybe-action kind maybe-kind] (case kind :always `(let [finalize# (fn [x#] (~action x#)) val# ~init ~bind val#] (let [result# (try (with-final ~(subvec bindings 4) ~@body) (catch Throwable ex# (try (finalize# val#) (catch Throwable ex2# (.addSuppressed ex# ex2#))) (throw ex#)))] (finalize# val#) result#)) :error `(let [cleanup# (fn [x#] (~action x#)) val# ~init ~bind val#] (try (with-final ~(subvec bindings 4) ~@body) (catch Throwable ex# (try (cleanup# val#) (catch Throwable ex2# (.addSuppressed ex# ex2#))) (throw ex#))))))))))) (defmacro with-open! "Bindings must be a vector of [name init ...] pairs. Binds each name to the value of the corresponding init, and behaves exactly as if each subsequent name were guarded by a nested try form that calls (.close name) in its finally clause. Suppresses any exceptions thrown by the .close calls via the Throwable addSuppressed method." [bindings & body] (assert (vector? bindings)) (if (empty? bindings) `(do ~@body) (do (assert (even? (count bindings))) (let [bindings (vec (mapcat #(concat % '(:always .close)) (partition 2 bindings)))] `(with-final ~bindings ~@body))))) murphy-0.5.2/test/000077500000000000000000000000001411025412500140145ustar00rootroot00000000000000murphy-0.5.2/test/murphy_test.clj000066400000000000000000000303161411025412500170740ustar00rootroot00000000000000(ns murphy-test (:refer-clojure :exclude [ex-message]) (:require [clojure.test :refer :all] [murphy :refer [try! with-final with-open!]]) (:import (java.lang AutoCloseable))) (defn close [^AutoCloseable x] (.close x)) (def ex-message (if (resolve 'ex-message) clojure.core/ex-message (fn ex-message [ex] (.getMessage ^Throwable ex)))) (defn ex-suppressed [ex] (.getSuppressed ^Throwable ex)) (deftest suppressing-try (is (= nil (try!))) (is (= 1 (try! 1))) (is (= 3 (try! (io! 1) (io! 2) 3))) ;; io! avoids eastwood unused var (is (= nil (try! (finally)))) (is (= 1 (try! 1 (finally)))) (let [fin (atom [])] (is (= nil (try! (finally (swap! fin conj 1))))) (is (= [1] @fin))) (let [fin (atom [])] (is (= nil (try! (catch Exception ex (swap! fin conj 1)) (finally (swap! fin conj 2))))) (is (= [2] @fin))) (let [fin (atom [])] (is (= [1] (try! (throw (Exception. "one")) (catch Exception ex (swap! fin conj 1)) (finally (swap! fin conj 2))))) (is (= [1 2] @fin))) (let [fin (atom [])] (is (= nil (try! (finally (swap! fin conj 1) (swap! fin conj 2))))) (is (= [1 2] @fin))) (let [fin (atom [])] (is (= 1 (try! (inc 0) (finally (swap! fin conj 1))))) (is (= [1] @fin))) (let [fin (atom []) ex-1 (Exception. "one")] (try (try! (throw ex-1) (finally (swap! fin conj 1))) (catch Exception ex (is (= ex-1 ex)))) (is (= [1] @fin))) (let [fin (atom []) ex-1 (Exception. "one") ex-2 (Exception. "two")] (try (try! (throw ex-1) (finally (swap! fin conj 1) (throw ex-2))) (catch Exception ex (is (= ex-1 ex)) (is (= [ex-2] (seq (ex-suppressed ex)))))) (is (= [1] @fin)))) (deftest multi-finally (let [fin (atom []) ex-1 (Exception. "one") ex-2 (Exception. "two") ex-3 (Exception. "three")] (try (try! (throw ex-1) (finally (swap! fin conj 1) (throw ex-2)) (finally (swap! fin conj 2) (throw ex-3))) (catch Exception ex (is (= ex-1 ex)) (is (= [ex-2 ex-3] (seq (ex-suppressed ex)))))) (is (= [1 2] @fin)))) (defrecord CloseableThing [close-this] java.lang.AutoCloseable (close [this] (close-this this))) (deftest closeable-thing-behavior (let [closed? (atom false) closeable (->CloseableThing (fn [this] (reset! closed? true)))] (is (= false @closed?)) (with-open [^AutoCloseable x closeable] (is (= false @closed?)) :foo) (is (= true @closed?)))) (deftest with-final-always-behavior (is (= nil (with-final []))) (is (= 1 (with-final [] 1))) (testing "when nothing is thrown" (let [closed? (atom false) closeable (->CloseableThing (fn [this] (reset! closed? true)))] (is (= false @closed?)) (is (= :foo (with-final [x closeable :always close] (is (= false @closed?)) (is (= x closeable)) :foo))) (is (= true @closed?))) (let [closes (atom []) closeable-1 (->CloseableThing (fn [this] (swap! closes conj 1))) closeable-2 (->CloseableThing (fn [this] (swap! closes conj 2)))] (is (= [] @closes)) (is (= :foo (with-final [x closeable-1 :always close y closeable-2 :always close] (is (= [] @closes)) (is (= x closeable-1)) (is (= y closeable-2)) :foo))) (is (= [2 1] @closes)))) (testing "when body throws" (let [closes (atom []) closeable-1 (->CloseableThing (fn [this] (swap! closes conj 1))) closeable-2 (->CloseableThing (fn [this] (swap! closes conj 2)))] (is (= [] @closes)) (is (= ["bar" {::bar 1}] (try (with-final [x closeable-1 :always close y closeable-2 :always close] (is (= [] @closes)) (throw (ex-info "bar" {::bar 1}))) (catch clojure.lang.ExceptionInfo ex [(ex-message ex) (ex-data ex)])))) (is (= [2 1] @closes)))) (testing "when close throws" (let [closes (atom []) closeable-1 (->CloseableThing (fn [this] (swap! closes conj 1))) closeable-2 (->CloseableThing (fn [this] (swap! closes conj 2) (throw (ex-info "bar" {::bar 1})))) closeable-3 (->CloseableThing (fn [this] (swap! closes conj 3)))] (let [ex (try (with-final [x closeable-1 :always close y closeable-2 :always close z closeable-3 :always close] (is (= [] @closes)) :foo) (catch clojure.lang.ExceptionInfo ex ex))] (is (= [3 2 1] @closes)) (is (= ["bar" {::bar 1}] [(ex-message ex) (ex-data ex)])) (is (= nil (seq (ex-suppressed ex))))))) (testing "when body and close throw" (let [closes (atom []) close-ex-1 (ex-info "bar" {::bar 1}) close-ex-2 (ex-info "baz" {::baz 1}) body-ex (ex-info "bax" {::bax 1}) closeable-1 (->CloseableThing (fn [this] (swap! closes conj 1))) closeable-2 (->CloseableThing (fn [this] (swap! closes conj 2) (throw close-ex-1))) closeable-3 (->CloseableThing (fn [this] (swap! closes conj 3) (throw close-ex-2)))] (let [ex (try (with-final [x closeable-1 :always close y closeable-2 :always close z closeable-3 :always close] (is (= [] @closes)) (throw body-ex)) (catch clojure.lang.ExceptionInfo ex ex))] (is (= [3 2 1] @closes)) (is (= ["bax" {::bax 1}] [(ex-message ex) (ex-data ex)])) (is (= [close-ex-2 close-ex-1] (seq (ex-suppressed ex)))))))) (deftest with-final-destructuring (is (= [2 1] (with-final [[x y] [1 2]] [y x]))) (is (= [3 1] (with-final [[x] [1 2] [y] [3 4]] [y x]))) (is (= [2 1] (with-final [[x y :as v] [1 2] :always #(is (= [1 2] %))] [y x])))) (deftest with-final-error-behavior (is (= nil (with-final []))) (is (= 1 (with-final [] 1))) (testing "when nothing is thrown" (let [closed? (atom false) closeable (->CloseableThing (fn [this] (reset! closed? true)))] (is (= false @closed?)) (is (= :foo (with-final [x closeable :error close] (is (= false @closed?)) (is (= x closeable)) :foo))) (is (= false @closed?))) (let [closes (atom []) closeable-1 (->CloseableThing (fn [this] (swap! closes conj 1))) closeable-2 (->CloseableThing (fn [this] (swap! closes conj 2)))] (is (= [] @closes)) (is (= :foo (with-final [x closeable-1 :error close y closeable-2 :error close] (is (= [] @closes)) (is (= x closeable-1)) (is (= y closeable-2)) :foo))) (is (= [] @closes)))) (testing "when body throws" (let [closes (atom []) closeable-1 (->CloseableThing (fn [this] (swap! closes conj 1))) closeable-2 (->CloseableThing (fn [this] (swap! closes conj 2)))] (is (= [] @closes)) (is (= ["bar" {::bar 1}] (try (with-final [x closeable-1 :error close y closeable-2 :error close] (is (= [] @closes)) (throw (ex-info "bar" {::bar 1}))) (catch clojure.lang.ExceptionInfo ex [(ex-message ex) (ex-data ex)])))) (is (= [2 1] @closes)))) (testing "when only a close throws" (let [closes (atom []) closeable-1 (->CloseableThing (fn [this] (swap! closes conj 1))) closeable-2 (->CloseableThing (fn [this] (swap! closes conj 2) (throw (ex-info "bar" {::bar 1})))) closeable-3 (->CloseableThing (fn [this] (swap! closes conj 3)))] (let [result (try (with-final [x closeable-1 :error close y closeable-2 :error close z closeable-3 :error close] (is (= [] @closes)) :foo) (catch clojure.lang.ExceptionInfo ex ex))] (is (= [] @closes)) (is (= :foo result))))) (testing "when body and close throw" (let [closes (atom []) close-ex-1 (ex-info "bar" {::bar 1}) close-ex-2 (ex-info "baz" {::baz 1}) body-ex (ex-info "bax" {::bax 1}) closeable-1 (->CloseableThing (fn [this] (swap! closes conj 1))) closeable-2 (->CloseableThing (fn [this] (swap! closes conj 2) (throw close-ex-1))) closeable-3 (->CloseableThing (fn [this] (swap! closes conj 3) (throw close-ex-2)))] (let [ex (try (with-final [x closeable-1 :error close y closeable-2 :error close z closeable-3 :error close] (is (= [] @closes)) (throw body-ex)) (catch clojure.lang.ExceptionInfo ex ex))] (is (= [3 2 1] @closes)) (is (= ["bax" {::bax 1}] [(ex-message ex) (ex-data ex)])) (is (= [close-ex-2 close-ex-1] (seq (ex-suppressed ex)))))))) (deftest with-final-mixed-forms (is (= 1 (with-final [x 1] x))) (testing "normal let bindings and :error" (let [closes (atom []) closeable-1 (->CloseableThing (fn [this] (swap! closes conj 1))) closeable-2 (->CloseableThing (fn [this] (swap! closes conj 2)))] (is (= [] @closes)) (is (= ["bar" {::bar 1}] (try (with-final [c1 closeable-1 x c1 :error close c2 closeable-2 y c2 :error close] (is (= [] @closes)) (throw (ex-info "bar" {::bar 1}))) (catch clojure.lang.ExceptionInfo ex [(ex-message ex) (ex-data ex)])))) (is (= [2 1] @closes)))) (testing "normal let bindings and :always" (let [closes (atom []) closeable-1 (->CloseableThing (fn [this] (swap! closes conj 1))) closeable-2 (->CloseableThing (fn [this] (swap! closes conj 2)))] (is (= [] @closes)) (is (= ["bar" {::bar 1}] (try (with-final [c1 closeable-1 x c1 :always close c2 closeable-2 y c2 :always close] (is (= [] @closes)) (throw (ex-info "bar" {::bar 1}))) (catch clojure.lang.ExceptionInfo ex [(ex-message ex) (ex-data ex)])))) (is (= [2 1] @closes))))) (deftest suppressing-open ;; As long as this trivially is based on with-final, rely on its ;; tests for much of the work now. (is (= nil (with-open! []))) (is (= 1 (with-open! [] 1))) (testing "closeable thing" (let [closed? (atom false) closeable (->CloseableThing (fn [this] (reset! closed? true)))] (is (= false @closed?)) (with-open [^AutoCloseable x closeable] (is (= false @closed?)) :foo) (is (= true @closed?)))))