diff --git a/.cljfmt.edn b/.cljfmt.edn
new file mode 100644
index 0000000..9a6a915
--- /dev/null
+++ b/.cljfmt.edn
@@ -0,0 +1,6 @@
+{:legacy/merge-indents? true
+ :indents {cond [[:inner 0]]
+ futurama.core/async [[:inner 0]]
+ futurama.core/thread [[:inner 0]]
+ failsage.core/execute [[:inner 0]]
+ failsage.core/execute-async [[:inner 0]]}}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 661c235..8273247 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,23 @@
This is a history of changes to k13labs/failsage
+# 0.2.0 - 2025-10-22
+
+### Features
+
+* **Map-based Policy Building**: Policies can now be defined using plain Clojure maps with a `:type` key to support policies as data.
+ * All policy types support map format: `:retry`, `:circuit-breaker`, `:fallback`, `:timeout`, `:rate-limiter`, `:bulkhead`
+ * Maps can be used directly with `execute`, `execute-async`, and `executor` functions
+ * Policy maps are automatically built via the `IPolicyBuilder` protocol.
+ * Mix map-based and function-based policies seamlessly in policy composition
+ * Example: `(fs/execute {:type :retry :max-retries 3} (call-service))`
+ * Stateful policy maps (circuit breaker, rate limiter, bulkhead) throw exceptions when used directly to prevent state loss
+* Added `policies` function to explicitly create policy lists from regular policies, maps, or mixed combinations
+
+### Changes
+
+* Removed some redundant functions from the `failsage.impl` namespace, and moved the `IPolicyBuilder protocol to the `failsage.core` namespace.
+* Extended `IPolicyBuilder` protocol to support `IPersistentMap` interface for automatic policy building
+
# 0.1.0 - 2025-10-21
Initial release providing idiomatic Clojure wrappers for [Failsafe.dev](https://failsafe.dev/).
diff --git a/README.md b/README.md
index b2e78e8..dd1425e 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ Rather than reinventing the resilience patterns, Failsage wisely wraps Failsafe
- **Idiomatic Clojure API**: Keyword arguments, immutable data structures, and functional style
- **Async Support**: Seamless async execution integration via [futurama](https://github.com/k13labs/futurama), including channels/futures/promises/deferreds.
- **Unified Interface**: Consistent API for both synchronous and asynchronous code paths
+- **Data-Driven Policies**: Define policies as plain Clojure maps for configuration-driven setups
Failsage supports all core Failsafe policies:
- **Retry**: Automatic retry with configurable backoff strategies
@@ -137,6 +138,98 @@ For detailed information on each policy's behavior and configuration options, se
(process-task task))
```
+## Policy Maps
+
+Policies can also be defined using plain Clojure maps with a `:type` key. This is useful for configuration-driven setups or when you want to serialize/deserialize policy definitions:
+
+```clj
+;; Define policies as maps
+(def retry-map {:type :retry
+ :max-retries 3
+ :delay-ms 100})
+
+(def circuit-breaker-map {:type :circuit-breaker
+ :delay-ms 60000
+ :failure-threshold 5})
+
+(def fallback-map {:type :fallback
+ :result {:status :degraded}})
+
+;; IMPORTANT: Stateful policies (circuit breakers, rate limiters, bulkheads) maintain
+;; state across executions. Using their maps directly with execute/execute-async will
+;; throw an exception to prevent accidentally losing state.
+
+;; ❌ THROWS EXCEPTION - stateful policy map used directly
+(fs/execute circuit-breaker-map (api-call-1))
+;; => ExceptionInfo: Cannot execute with stateful policy map directly
+
+;; ✅ CORRECT - create executor once, reuse it
+(def executor (fs/executor circuit-breaker-map))
+(fs/execute executor (api-call-1))
+(fs/execute executor (api-call-2))
+
+;; ✅ ALSO CORRECT - use policies function
+(def policy-list (fs/policies circuit-breaker-map))
+(fs/execute policy-list (api-call-1))
+(fs/execute policy-list (api-call-2))
+
+;; Stateless policies (retry, timeout, fallback) can be used directly as maps
+(fs/execute retry-map
+ (call-unreliable-service))
+```
+
+All policy types support the map format:
+
+```clj
+;; Retry
+{:type :retry :max-retries 3 :backoff-delay-ms 100 :backoff-max-delay-ms 5000}
+
+;; Circuit Breaker
+{:type :circuit-breaker :delay-ms 60000 :failure-threshold 5}
+
+;; Fallback
+{:type :fallback :result "default-value"}
+{:type :fallback :result-fn (fn [event] "computed-value")}
+
+;; Timeout
+{:type :timeout :timeout-ms 5000 :interrupt true}
+
+;; Rate Limiter
+{:type :rate-limiter :max-executions 100 :period-ms 1000 :burst true}
+
+;; Bulkhead
+{:type :bulkhead :max-concurrency 10 :max-wait-time-ms 1000}
+```
+
+Policy maps work seamlessly with all execution modes:
+
+```clj
+;; With executor (executor is created once and can be reused)
+(def executor (fs/executor {:type :retry :max-retries 3}))
+(fs/execute executor (operation-1))
+(fs/execute executor (operation-2))
+
+;; Direct with execute (only works for stateless policies like fallback, retry, timeout)
+(fs/execute {:type :fallback :result :default}
+ (risky-operation))
+
+;; With execute-async (only works for stateless policies)
+(fs/execute-async {:type :retry :max-retries 3}
+ (f/async (async-operation)))
+
+;; Stateful policy maps throw exceptions when used directly
+;; (fs/execute {:type :circuit-breaker :failure-threshold 5} (call))
+;; => ExceptionInfo: Cannot execute with stateful policy map directly
+
+;; Mix maps with regular policies
+(def retry-map {:type :retry :max-retries 3})
+(def fallback-policy (fs/fallback {:result :default}))
+;; Build executor once with both policies
+(def mixed-executor (fs/executor [fallback-policy retry-map]))
+(fs/execute mixed-executor (operation-1))
+(fs/execute mixed-executor (operation-2))
+```
+
## Policy Composition
Policies can be composed together. They are applied in order from innermost to outermost:
@@ -180,6 +273,47 @@ Policies can be composed together. They are applied in order from innermost to o
(def executor (fs/executor [fallback cb bulkhead]))
```
+**Configuration-driven with Policy Maps**: Load policies from config
+```clj
+;; Load from config file/env
+(def policy-config
+ [{:type :fallback :result {:status :degraded}}
+ {:type :circuit-breaker :failure-threshold 10 :delay-ms 30000}
+ {:type :retry :max-retries 3 :delay-ms 100}])
+
+;; Build executor once from map config - policies are built automatically
+(def executor (fs/executor policy-config))
+
+;; Reuse the executor for multiple calls - circuit breaker state is maintained
+(fs/execute executor (api-call-1))
+(fs/execute executor (api-call-2))
+(fs/execute executor (api-call-3))
+```
+
+**Creating Reusable Policy Lists**: Use `policies` to build policy lists independently
+```clj
+;; Create a reusable policy list - policies are built from maps
+;; Note: the policies function builds all policies immediately
+(def standard-policies
+ (fs/policies
+ (fs/fallback {:result {:status :degraded}})
+ {:type :circuit-breaker :failure-threshold 5 :delay-ms 30000}
+ (fs/retry {:max-retries 3})))
+
+;; Create executors with the same policy list
+;; Each executor gets the SAME policy instances (e.g., same circuit breaker)
+(def executor-1 (fs/executor standard-policies))
+(def executor-2 (fs/executor :io standard-policies))
+
+;; Both executors share the same circuit breaker state
+(fs/execute executor-1 (api-call))
+(fs/execute executor-2 (api-call))
+
+;; Or use directly with execute (creates a new executor each time)
+;; Note: this still reuses the same policy instances from standard-policies
+(fs/execute standard-policies (api-call))
+```
+
## Asynchronous Execution
Failsage integrates with [futurama](https://github.com/k13labs/futurama) for async execution:
diff --git a/pom.xml b/pom.xml
index 494a99d..aea0507 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,9 +5,9 @@
com.github.k13labs
failsage
failsage
- 0.1.0
+ 0.2.0
- 0.1.0
+ 0.2.0
https://github.com/k13labs/failsage
scm:git:git://github.com/k13labs/failsage.git
scm:git:ssh://git@github.com/k13labs/failsage.git
diff --git a/resources/clj-kondo.exports/failsage/failsage/hooks/failsage.clj_kondo b/resources/clj-kondo.exports/failsage/failsage/hooks/failsage.clj_kondo
index 4554220..f6a4981 100644
--- a/resources/clj-kondo.exports/failsage/failsage/hooks/failsage.clj_kondo
+++ b/resources/clj-kondo.exports/failsage/failsage/hooks/failsage.clj_kondo
@@ -5,11 +5,11 @@
"analyze execute macro to support optional context binding"
[{:keys [:node]}]
(let [execute-args (rest (:children node))
- [executor-or-policy context-binding body] (when (= 3 (count execute-args))
- execute-args)]
+ [executor-or-policy context-binding & body] (when (>= (count execute-args) 3)
+ execute-args)]
(if context-binding
{:node (api/list-node
- (list
+ (list*
(api/token-node 'let)
(api/vector-node
(vector context-binding executor-or-policy))
diff --git a/src/failsage/core.clj b/src/failsage/core.clj
index 6771426..4d85eca 100644
--- a/src/failsage/core.clj
+++ b/src/failsage/core.clj
@@ -4,30 +4,122 @@
[failsage.impl :as impl]
[futurama.core :refer [!> (for [policy (flatten policy-args)
+ :when (some? policy)]
+ (cond
+ (instance? Policy policy)
+ policy
+
+ (satisfies? IPolicyBuilder policy)
+ (build-policy policy)
+
+ :else (throw (ex-info "Invalid policy type" {:policy policy}))))
+ vec))
+
+(defn executor
+ "Creates a FailsafeExecutor instance with the provided policies.
+
+ Docs: https://failsafe.dev/javadoc/core/dev/failsafe/FailsafeExecutor.html
+
+ Options:
+ - `:pool` (optional): An executor service to use for async executions.
+ If not provided, it will use the currently bound `futurama.core/*thread-pool*` or `:io` pool.
+ - `:policies` (optional): The policy or policies to use with the FailsafeExecutor.
+ If not provided, the executor will treat any exceptions as a failure without any additional error handling."
+ (^FailsafeExecutor []
+ (executor nil nil))
+ (^FailsafeExecutor [executor-or-policies]
+ (executor nil executor-or-policies))
+ (^FailsafeExecutor [pool executor-or-policies]
+ (let [pool (impl/get-pool pool)]
+ (if (instance? FailsafeExecutor executor-or-policies)
+ (.with ^FailsafeExecutor executor-or-policies pool)
+ (let [policies (policies executor-or-policies)]
+ (if (empty? policies)
+ (.with (Failsafe/none) pool)
+ (.with (Failsafe/with policies) pool)))))))
+
+(defmacro execute
+ "Executes the given function with the provided Failsafe executor or policy.
+
+ Docs: https://failsafe.dev/javadoc/core/dev/failsafe/FailsafeExecutor.html
+
+ Parameters:
+ - `executor-or-policy`: The FailsafeExecutor or Policies to use for execution.
+ - `context-binding`: the binding to use for the ExecutionContext context.
+ - `body`: the body of code to execute.
+
+ Returns the result or throws an exception."
+ ([body]
+ `(execute [] context# ~body))
+ ([executor-or-policy body]
+ `(execute ~executor-or-policy context# ~body))
+ ([executor-or-policy context-binding & body]
+ `(impl/execute-get (executor (impl/validate-not-stateful-map! ~executor-or-policy))
+ (bound-fn [~(vary-meta context-binding assoc :tag ExecutionContext)]
+ ~@body))))
-(defn build-policy
- "Builds and returns the Failsafe Policy instance from a builder."
- [builder]
- (impl/build-policy builder))
+(defmacro execute-async
+ "Executes the given async function with the provided Failsafe executor or policy.
+
+ Docs: https://failsafe.dev/javadoc/core/dev/failsafe/FailsafeExecutor.html
+
+ Parameters:
+ - `executor-or-policy`: The FailsafeExecutor or Policies to use for async execution.
+ - `context-binding`: the binding to use for the AsyncExecution context.
+ - `body`: the body of code to execute.
+
+ Returns a future representing the result or exception."
+ ([body]
+ `(execute-async [] context# ~body))
+ ([executor-or-policy body]
+ `(execute-async ~executor-or-policy context# ~body))
+ ([executor-or-policy context-binding & body]
+ `(impl/execute-get-async (executor (impl/validate-not-stateful-map! ~executor-or-policy))
+ (bound-fn [~(vary-meta context-binding assoc :tag AsyncExecution)]
+ (async
+ (try
+ (impl/record-async-success ~context-binding (!executor nil nil))
- (^FailsafeExecutor [policies]
- (impl/->executor nil policies))
- (^FailsafeExecutor [pool policies]
- (impl/->executor pool policies)))
-
-(defmacro execute
- "Executes the given function with the provided Failsafe executor or policy.
-
- Docs: https://failsafe.dev/javadoc/core/dev/failsafe/FailsafeExecutor.html
-
- Parameters:
- - `executor-or-policy`: The FailsafeExecutor or Policies to use for execution.
- - `context-binding`: the binding to use for the ExecutionContext context.
- - `body`: the body of code to execute.
-
- Returns the result or throws an exception."
- ([body]
- `(execute [] context# ~body))
- ([executor-or-policy body]
- `(execute ~executor-or-policy context# ~body))
- ([executor-or-policy context-binding body]
- `(impl/execute-get ~executor-or-policy
- (bound-fn [~(vary-meta context-binding assoc :tag ExecutionContext)]
- ~body))))
-
-(defmacro execute-async
- "Executes the given async function with the provided Failsafe executor or policy.
-
- Docs: https://failsafe.dev/javadoc/core/dev/failsafe/FailsafeExecutor.html
-
- Parameters:
- - `executor-or-policy`: The FailsafeExecutor or Policies to use for async execution.
- - `context-binding`: the binding to use for the AsyncExecution context.
- - `body`: the body of code to execute.
-
- Returns a future representing the result or exception."
- ([body]
- `(execute-async [] context# ~body))
- ([executor-or-policy body]
- `(execute-async ~executor-or-policy context# ~body))
- ([executor-or-policy context-binding body]
- `(impl/execute-get-async ~executor-or-policy
- (bound-fn [~context-binding]
- (async
- (try
- (impl/record-async-success ~context-binding (!> (for [policy (flatten policy-args)
- :when (some? policy)]
- (cond
- (instance? Policy policy)
- policy
-
- (satisfies? IPolicyBuilder policy)
- (build-policy policy)
-
- :else (throw (ex-info "Invalid policy type" {:policy policy}))))
- vec))
-
(defn record-async-success
"Records the result of an execution in the given ExecutionContext."
[^AsyncExecution context ^Object result]
@@ -138,31 +83,32 @@
[^AsyncExecution context ^Throwable error]
(.recordException context error))
-(defn ->executor
- "Creates a FailsafeExecutor with the given thread pool and policies.
- If no policies are provided, uses Failsafe.none()
- If no pool is provided, uses a default thread pool."
- (^FailsafeExecutor []
- (->executor nil nil))
- (^FailsafeExecutor [executor-or-policies]
- (->executor nil executor-or-policies))
- (^FailsafeExecutor [pool executor-or-policies]
- (let [pool (get-pool pool)]
- (if (instance? FailsafeExecutor executor-or-policies)
- (.with ^FailsafeExecutor executor-or-policies pool)
- (let [policies (get-policy-list executor-or-policies)]
- (if (empty? policies)
- (.with (Failsafe/none) pool)
- (.with (Failsafe/with policies) pool)))))))
-
(defn execute-get
"Executes the given CheckedSupplier using the Failsafe executor."
- [executor-or-policies execute-fn]
- (.get (->executor executor-or-policies)
- (->contextual-supplier execute-fn)))
+ [^FailsafeExecutor executor execute-fn]
+ (.get executor (->contextual-supplier execute-fn)))
(defn execute-get-async
"Executes the given CheckedSupplier using the Failsafe executor."
- [executor-or-policies execute-fn]
- (.getAsyncExecution (->executor executor-or-policies)
- (->async-runnable execute-fn)))
+ [^FailsafeExecutor executor execute-fn]
+ (.getAsyncExecution executor (->async-runnable execute-fn)))
+
+(defn- check-stateful-map!
+ "Checks if a single policy is a stateful map and throws if so."
+ [policy]
+ (when (and (map? policy)
+ (#{:circuit-breaker :rate-limiter :bulkhead} (:type policy)))
+ (throw (ex-info "Cannot execute with stateful policy map directly"
+ {:policy-map policy
+ :policy-type (:type policy)
+ :stateful-types #{:circuit-breaker :rate-limiter :bulkhead}}))))
+
+(defn validate-not-stateful-map!
+ "Validates that executor-or-policy is not a stateful policy map.
+ Returns the input unchanged if valid, throws if invalid."
+ [executor-or-policy]
+ (if (sequential? executor-or-policy)
+ (doseq [policy (flatten executor-or-policy)]
+ (check-stateful-map! policy))
+ (check-stateful-map! executor-or-policy))
+ executor-or-policy)
diff --git a/test/failsage/core_test.clj b/test/failsage/core_test.clj
index 03fcaf6..f9a7502 100644
--- a/test/failsage/core_test.clj
+++ b/test/failsage/core_test.clj
@@ -17,11 +17,59 @@
RetryPolicy
Timeout
TimeoutExceededException]
- [java.lang ArithmeticException IllegalArgumentException]))
+ [java.lang ArithmeticException IllegalArgumentException]
+ [java.time Duration]))
;; Dynamic var for testing
(def ^:dynamic *test-var* nil)
+;; IPolicyBuilder Protocol Tests
+
+(deftest test-build-policy-bulkhead-builder
+ (testing "build-policy builds a Bulkhead from BulkheadBuilder"
+ (let [builder (-> (Bulkhead/builder 5)
+ (.withMaxWaitTime (Duration/ofMillis 100)))
+ policy (fs/build-policy builder)]
+ (is (instance? Bulkhead policy))
+ (is (instance? Policy policy)))))
+
+(deftest test-build-policy-circuit-breaker-builder
+ (testing "build-policy builds a CircuitBreaker from CircuitBreakerBuilder"
+ (let [builder (-> (CircuitBreaker/builder)
+ (.withDelay (Duration/ofMillis 100)))
+ policy (fs/build-policy builder)]
+ (is (instance? CircuitBreaker policy))
+ (is (instance? Policy policy)))))
+
+(deftest test-build-policy-fallback-builder
+ (testing "build-policy builds a Fallback from FallbackBuilder"
+ (let [builder (Fallback/builder "default-value")
+ policy (fs/build-policy builder)]
+ (is (instance? Fallback policy))
+ (is (instance? Policy policy)))))
+
+(deftest test-build-policy-rate-limiter-builder
+ (testing "build-policy builds a RateLimiter from RateLimiterBuilder"
+ (let [builder (RateLimiter/smoothBuilder 10 (Duration/ofSeconds 1))
+ policy (fs/build-policy builder)]
+ (is (instance? RateLimiter policy))
+ (is (instance? Policy policy)))))
+
+(deftest test-build-policy-retry-policy-builder
+ (testing "build-policy builds a RetryPolicy from RetryPolicyBuilder"
+ (let [builder (-> (RetryPolicy/builder)
+ (.withMaxRetries 3))
+ policy (fs/build-policy builder)]
+ (is (instance? RetryPolicy policy))
+ (is (instance? Policy policy)))))
+
+(deftest test-build-policy-timeout-builder
+ (testing "build-policy builds a Timeout from TimeoutBuilder"
+ (let [builder (Timeout/builder (Duration/ofSeconds 5))
+ policy (fs/build-policy builder)]
+ (is (instance? Timeout policy))
+ (is (instance? Policy policy)))))
+
;; Bulkhead Tests
(deftest test-bulkhead-creation
@@ -298,11 +346,11 @@
counter (atom 0)]
(try
(fs/execute executor
- (do
- (swap! counter inc)
- (when (<= @counter 2)
- (throw (ex-info "retry" {})))
- "success"))
+ (do
+ (swap! counter inc)
+ (when (<= @counter 2)
+ (throw (ex-info "retry" {})))
+ "success"))
(catch Exception _))
;; Should have tried 3 times (initial + 2 retries)
(is (= 3 @counter)))))
@@ -337,9 +385,9 @@
counter (atom 0)]
(try
(fs/execute executor
- (do
- (swap! counter inc)
- (throw (IllegalArgumentException. "abort"))))
+ (do
+ (swap! counter inc)
+ (throw (IllegalArgumentException. "abort"))))
(catch IllegalArgumentException _))
;; Should only try once, no retries
(is (= 1 @counter)))))
@@ -350,9 +398,9 @@
executor (fs/executor policy)
counter (atom 0)
result (fs/execute executor
- (do
- (swap! counter inc)
- :abort))]
+ (do
+ (swap! counter inc)
+ :abort))]
(is (= :abort result))
;; Should only try once, no retries
(is (= 1 @counter)))))
@@ -372,11 +420,11 @@
executor (fs/executor policy)
counter (atom 0)
result (fs/execute executor
- (do
- (swap! counter inc)
- (if (< @counter 2)
- :retry
- :success)))]
+ (do
+ (swap! counter inc)
+ (if (< @counter 2)
+ :retry
+ :success)))]
(is (= :success result))
(is (= 2 @counter)))))
@@ -392,11 +440,11 @@
executor (fs/executor policy)
counter (atom 0)]
(fs/execute executor
- (do
- (swap! counter inc)
- (when (< @counter 2)
- (throw (ex-info "retry" {})))
- "success"))
+ (do
+ (swap! counter inc)
+ (when (< @counter 2)
+ (throw (ex-info "retry" {})))
+ "success"))
(is (= 1 @failed-attempt-count))
(is (= 1 @retry-count))
(is @success-called))))
@@ -435,6 +483,67 @@
(fs/execute executor "success")
(is @success-called))))
+;; Policies Tests
+
+(deftest test-policies-creation
+ (testing "policies function creates a policy list"
+ (testing "with single policy"
+ (let [policy (fs/retry {:max-retries 3})
+ policy-list (fs/policies policy)]
+ (is (instance? java.util.List policy-list))
+ (is (= 1 (count policy-list)))
+ (is (instance? RetryPolicy (first policy-list)))))
+
+ (testing "with multiple policies"
+ (let [retry-policy (fs/retry {:max-retries 3})
+ timeout-policy (fs/timeout {:timeout-ms 1000})
+ policy-list (fs/policies retry-policy timeout-policy)]
+ (is (instance? java.util.List policy-list))
+ (is (= 2 (count policy-list)))
+ (is (instance? RetryPolicy (first policy-list)))
+ (is (instance? Timeout (second policy-list)))))
+
+ (testing "with map-based policies"
+ (let [retry-map {:type :retry :max-retries 3}
+ fallback-map {:type :fallback :result "default"}
+ policy-list (fs/policies retry-map fallback-map)]
+ (is (instance? java.util.List policy-list))
+ (is (= 2 (count policy-list)))
+ (is (instance? RetryPolicy (first policy-list)))
+ (is (instance? Fallback (second policy-list)))))
+
+ (testing "with mixed policy types"
+ (let [retry-policy (fs/retry {:max-retries 3})
+ fallback-map {:type :fallback :result "default"}
+ policy-list (fs/policies retry-policy fallback-map)]
+ (is (instance? java.util.List policy-list))
+ (is (= 2 (count policy-list)))
+ (is (instance? RetryPolicy (first policy-list)))
+ (is (instance? Fallback (second policy-list)))))
+
+ (testing "with nested collections"
+ (let [retry-policy (fs/retry {:max-retries 3})
+ timeout-policy (fs/timeout {:timeout-ms 1000})
+ policy-list (fs/policies [retry-policy timeout-policy])]
+ (is (instance? java.util.List policy-list))
+ (is (= 2 (count policy-list)))))
+
+ (testing "with nil values filtered out"
+ (let [retry-policy (fs/retry {:max-retries 3})
+ policy-list (fs/policies retry-policy nil)]
+ (is (instance? java.util.List policy-list))
+ (is (= 1 (count policy-list)))
+ (is (instance? RetryPolicy (first policy-list)))))
+
+ (testing "empty list with no policies"
+ (let [policy-list (fs/policies)]
+ (is (instance? java.util.List policy-list))
+ (is (empty? policy-list))))
+
+ (testing "throws exception for invalid policy type"
+ (is (thrown? Exception
+ (fs/policies "invalid-policy"))))))
+
;; Executor Tests
(deftest test-executor-creation-no-policies
@@ -479,9 +588,9 @@
(let [executor (fs/executor)
counter (atom 0)
result (fs/execute executor
- (do
- (swap! counter inc)
- @counter))]
+ (do
+ (swap! counter inc)
+ @counter))]
(is (= 1 result))
(is (= 1 @counter)))))
@@ -497,11 +606,11 @@
executor (fs/executor policy)
counter (atom 0)]
(fs/execute executor
- (do
- (swap! counter inc)
- (when (< @counter 2)
- (throw (ex-info "retry" {})))
- "success"))
+ (do
+ (swap! counter inc)
+ (when (< @counter 2)
+ (throw (ex-info "retry" {})))
+ "success"))
(is (= 2 @counter)))))
(deftest test-execute-with-fallback-policy
@@ -520,9 +629,9 @@
executor (fs/executor [fallback-policy retry-policy])
counter (atom 0)
result (fs/execute executor
- (do
- (swap! counter inc)
- (throw (ex-info "fail" {}))))]
+ (do
+ (swap! counter inc)
+ (throw (ex-info "fail" {}))))]
(is (= "fallback" result))
;; Should have tried 3 times (initial + 2 retries)
(is (= 3 @counter)))))
@@ -562,9 +671,9 @@
(let [executor (fs/executor)
received-context (atom nil)
result (fs/execute executor ctx
- (do
- (reset! received-context ctx)
- 42))]
+ (do
+ (reset! received-context ctx)
+ 42))]
(is (= 42 result))
(is (some? @received-context))
(is (instance? dev.failsafe.ExecutionContext @received-context)))))
@@ -576,12 +685,12 @@
counter (atom 0)
attempt-counts (atom [])]
(fs/execute executor ctx
- (do
- (swap! attempt-counts conj (.getAttemptCount ^ExecutionContext ctx))
- (swap! counter inc)
- (when (< @counter 2)
- (throw (ex-info "retry" {})))
- "success"))
+ (do
+ (swap! attempt-counts conj (.getAttemptCount ^ExecutionContext ctx))
+ (swap! counter inc)
+ (when (< @counter 2)
+ (throw (ex-info "retry" {})))
+ "success"))
(is (= [0 1] @attempt-counts)))))
(deftest test-execute-context-has-start-time
@@ -589,9 +698,9 @@
(let [executor (fs/executor)
start-time (atom nil)]
(fs/execute executor ctx
- (do
- (reset! start-time (.getStartTime ^ExecutionContext ctx))
- 42))
+ (do
+ (reset! start-time (.getStartTime ^ExecutionContext ctx))
+ 42))
(is (some? @start-time)))))
;; Execute with Policy (not Executor) Tests
@@ -601,11 +710,11 @@
(let [policy (fs/retry {:max-retries 2})
counter (atom 0)
result (fs/execute policy
- (do
- (swap! counter inc)
- (when (< @counter 2)
- (throw (ex-info "retry" {})))
- "success"))]
+ (do
+ (swap! counter inc)
+ (when (< @counter 2)
+ (throw (ex-info "retry" {})))
+ "success"))]
(is (= "success" result))
(is (= 2 @counter)))))
@@ -615,9 +724,9 @@
fallback-policy (fs/fallback {:result "fallback"})
counter (atom 0)
result (fs/execute [fallback-policy retry-policy]
- (do
- (swap! counter inc)
- (throw (ex-info "fail" {}))))]
+ (do
+ (swap! counter inc)
+ (throw (ex-info "fail" {}))))]
(is (= "fallback" result))
(is (= 3 @counter)))))
@@ -626,11 +735,11 @@
(let [policy (fs/retry {:max-retries 1})
attempt-counts (atom [])]
(fs/execute policy ctx
- (do
- (swap! attempt-counts conj (.getAttemptCount ^ExecutionContext ctx))
- (when (= 1 (count @attempt-counts))
- (throw (ex-info "retry" {})))
- "success"))
+ (do
+ (swap! attempt-counts conj (.getAttemptCount ^ExecutionContext ctx))
+ (when (= 1 (count @attempt-counts))
+ (throw (ex-info "retry" {})))
+ "success"))
(is (= [0 1] @attempt-counts)))))
;; Execute Async Tests
@@ -640,7 +749,7 @@
(f/with-pool :io
(let [executor (fs/executor :io nil)
result (fs/execute-async executor
- (f/async 42))]
+ (f/async 42))]
(is (= 42 @result))))))
(deftest test-execute-async-with-retry
@@ -650,11 +759,11 @@
executor (fs/executor :io policy)
counter (atom 0)
result (fs/execute-async executor
- (f/async
- (swap! counter inc)
- (when (< @counter 2)
- (throw (ex-info "retry" {})))
- "success"))]
+ (f/async
+ (swap! counter inc)
+ (when (< @counter 2)
+ (throw (ex-info "retry" {})))
+ "success"))]
(is (= "success" @result))
(is (>= @counter 2))))))
@@ -664,7 +773,7 @@
(let [executor (fs/executor :io nil)]
(binding [*test-var* "async-bound-value"]
(let [result (fs/execute-async executor
- (f/async *test-var*))]
+ (f/async *test-var*))]
(is (= "async-bound-value" @result))))))))
;; Execute Async with 1-arity (no executor) Tests
@@ -680,8 +789,8 @@
(f/with-pool :io
(let [counter (atom 0)
result (fs/execute-async (f/async
- (swap! counter inc)
- @counter))]
+ (swap! counter inc)
+ @counter))]
(is (= 1 @result))
(is (= 1 @counter))))))
@@ -689,7 +798,7 @@
(testing "execute-async with no executor propagates exceptions"
(f/with-pool :io
(let [future-result (fs/execute-async (f/async
- (throw (ex-info "async error" {}))))]
+ (throw (ex-info "async error" {}))))]
;; Exception is thrown when dereferencing the future
(is (thrown-with-msg? Exception #"async error"
@future-result))))))
@@ -702,9 +811,9 @@
(let [executor (fs/executor :io nil)
received-context (atom nil)
result (fs/execute-async executor ctx
- (f/async
- (reset! received-context ctx)
- 42))]
+ (f/async
+ (reset! received-context ctx)
+ 42))]
(is (= 42 @result))
(is (some? @received-context))
(is (instance? dev.failsafe.AsyncExecution @received-context))))))
@@ -717,12 +826,12 @@
counter (atom 0)
attempt-counts (atom [])
result (fs/execute-async executor ctx
- (f/async
- (swap! attempt-counts conj (.getAttemptCount ^ExecutionContext ctx))
- (swap! counter inc)
- (when (< @counter 2)
- (throw (ex-info "retry" {})))
- "success"))]
+ (f/async
+ (swap! attempt-counts conj (.getAttemptCount ^ExecutionContext ctx))
+ (swap! counter inc)
+ (when (< @counter 2)
+ (throw (ex-info "retry" {})))
+ "success"))]
(is (= "success" @result))
(is (= [0 1] @attempt-counts))))))
@@ -731,9 +840,9 @@
(f/with-pool :io
(let [executor (fs/executor :io nil)
result (fs/execute-async executor ctx
- (f/async
+ (f/async
;; The macro handles recording, but we can verify context methods exist
- (.getAttemptCount ^ExecutionContext ctx)))]
+ (.getAttemptCount ^ExecutionContext ctx)))]
(is (= 0 @result))))))
;; Execute Async with Policy (not Executor) Tests
@@ -744,11 +853,11 @@
(let [policy (fs/retry {:max-retries 2})
counter (atom 0)
result (fs/execute-async policy
- (f/async
- (swap! counter inc)
- (when (< @counter 2)
- (throw (ex-info "retry" {})))
- "success"))]
+ (f/async
+ (swap! counter inc)
+ (when (< @counter 2)
+ (throw (ex-info "retry" {})))
+ "success"))]
(is (= "success" @result))
(is (= 2 @counter))))))
@@ -759,9 +868,9 @@
fallback-policy (fs/fallback {:result "fallback"})
counter (atom 0)
result (fs/execute-async [fallback-policy retry-policy]
- (f/async
- (swap! counter inc)
- (throw (ex-info "fail" {}))))]
+ (f/async
+ (swap! counter inc)
+ (throw (ex-info "fail" {}))))]
(is (= "fallback" @result))
(is (= 3 @counter))))))
@@ -771,11 +880,11 @@
(let [policy (fs/retry {:max-retries 1})
attempt-counts (atom [])
result (fs/execute-async policy ctx
- (f/async
- (swap! attempt-counts conj (.getAttemptCount ^ExecutionContext ctx))
- (when (= 1 (count @attempt-counts))
- (throw (ex-info "retry" {})))
- "success"))]
+ (f/async
+ (swap! attempt-counts conj (.getAttemptCount ^ExecutionContext ctx))
+ (when (= 1 (count @attempt-counts))
+ (throw (ex-info "retry" {})))
+ "success"))]
(is (= "success" @result))
(is (= [0 1] @attempt-counts))))))
@@ -805,9 +914,9 @@
executor (fs/executor [timeout-policy retry-policy])]
(is (thrown? TimeoutExceededException
(fs/execute executor
- (do
- (Thread/sleep 50)
- (throw (ex-info "retry" {})))))))))
+ (do
+ (Thread/sleep 50)
+ (throw (ex-info "retry" {})))))))))
(deftest test-integration-bulkhead-with-retry
(testing "bulkhead with retry policy"
@@ -816,10 +925,315 @@
executor (fs/executor [bulkhead-policy retry-policy])
counter (atom 0)
result (fs/execute executor
- (do
- (swap! counter inc)
- (when (< @counter 2)
- (throw (ex-info "retry" {})))
- "success"))]
+ (do
+ (swap! counter inc)
+ (when (< @counter 2)
+ (throw (ex-info "retry" {})))
+ "success"))]
(is (= "success" result))
(is (= 2 @counter)))))
+
+;; Map-based Policy Building Tests
+
+(deftest test-map-based-policy-building
+ (testing "Map-based policy building with :type key"
+
+ (testing "bulkhead policy creation"
+ (testing "basic bulkhead from map"
+ (let [policy (fs/build-policy {:type :bulkhead :max-concurrency 5})]
+ (is (instance? Bulkhead policy))
+ (is (instance? Policy policy))))
+
+ (testing "bulkhead with additional options"
+ (let [success-called (atom false)
+ policy (fs/build-policy {:type :bulkhead
+ :max-concurrency 3
+ :max-wait-time-ms 100
+ :on-success-fn (fn [_] (reset! success-called true))})
+ executor (fs/executor policy)]
+ (is (instance? Bulkhead policy))
+ (fs/execute executor "success")
+ (is @success-called))))
+
+ (testing "circuit-breaker policy creation"
+ (testing "basic circuit-breaker from map"
+ (let [policy (fs/build-policy {:type :circuit-breaker
+ :delay-ms 1000
+ :failure-threshold 3})]
+ (is (instance? CircuitBreaker policy))
+ (is (instance? Policy policy))))
+
+ (testing "circuit-breaker with state callbacks"
+ (let [opened (atom false)
+ policy (fs/build-policy {:type :circuit-breaker
+ :delay-ms 100
+ :failure-threshold 2
+ :on-open-fn (fn [_] (reset! opened true))})
+ executor (fs/executor policy)]
+ (is (instance? CircuitBreaker policy))
+ (try (fs/execute executor (throw (ex-info "fail1" {}))) (catch Exception _))
+ (try (fs/execute executor (throw (ex-info "fail2" {}))) (catch Exception _))
+ (is @opened))))
+
+ (testing "fallback policy creation"
+ (testing "basic fallback with static result"
+ (let [policy (fs/build-policy {:type :fallback :result "default"})]
+ (is (instance? Fallback policy))
+ (is (instance? Policy policy))))
+
+ (testing "fallback with result function"
+ (let [policy (fs/build-policy {:type :fallback
+ :result-fn (fn [_] "computed")})
+ executor (fs/executor policy)
+ result (fs/execute executor (throw (ex-info "fail" {})))]
+ (is (instance? Fallback policy))
+ (is (= "computed" result)))))
+
+ (testing "rate-limiter policy creation"
+ (testing "basic smooth rate-limiter from map"
+ (let [policy (fs/build-policy {:type :rate-limiter
+ :max-executions 10
+ :period-ms 1000})]
+ (is (instance? RateLimiter policy))
+ (is (instance? Policy policy))))
+
+ (testing "bursty rate-limiter enforces limits"
+ (let [policy (fs/build-policy {:type :rate-limiter
+ :max-executions 2
+ :period-ms 10000
+ :burst true
+ :max-wait-time-ms 0})
+ executor (fs/executor policy)]
+ (is (instance? RateLimiter policy))
+ (is (= 1 (fs/execute executor 1)))
+ (is (= 2 (fs/execute executor 2)))
+ (is (thrown? RateLimitExceededException
+ (fs/execute executor 3))))))
+
+ (testing "retry policy creation"
+ (testing "basic retry from map"
+ (let [policy (fs/build-policy {:type :retry :max-retries 3})]
+ (is (instance? RetryPolicy policy))
+ (is (instance? Policy policy))))
+
+ (testing "retry with backoff options"
+ (let [policy (fs/build-policy {:type :retry
+ :max-retries 3
+ :backoff-delay-ms 10
+ :backoff-max-delay-ms 100})
+ executor (fs/executor policy)
+ counter (atom 0)]
+ (is (instance? RetryPolicy policy))
+ (fs/execute executor
+ (do
+ (swap! counter inc)
+ (when (< @counter 2)
+ (throw (ex-info "retry" {})))
+ "success"))
+ (is (= 2 @counter)))))
+
+ (testing "timeout policy creation"
+ (testing "basic timeout from map"
+ (let [policy (fs/build-policy {:type :timeout :timeout-ms 1000})]
+ (is (instance? Timeout policy))
+ (is (instance? Policy policy))))
+
+ (testing "timeout enforces time limit"
+ (let [policy (fs/build-policy {:type :timeout
+ :timeout-ms 100
+ :interrupt true})
+ executor (fs/executor policy)]
+ (is (instance? Timeout policy))
+ (is (thrown? TimeoutExceededException
+ (fs/execute executor (Thread/sleep 200)))))))
+
+ (testing "error handling"
+ (testing "unknown policy type throws exception"
+ (is (thrown? IllegalArgumentException
+ (fs/build-policy {:type :unknown-policy})))))
+
+ (testing "usage with executor"
+ (testing "single map-based policy with executor"
+ (let [policy-map {:type :retry :max-retries 2}
+ executor (fs/executor policy-map)
+ counter (atom 0)
+ result (fs/execute executor
+ (do
+ (swap! counter inc)
+ (when (< @counter 2)
+ (throw (ex-info "retry" {})))
+ "success"))]
+ (is (= "success" result))
+ (is (= 2 @counter))))
+
+ (testing "map-based policy directly with execute"
+ (let [policy-map {:type :fallback :result "fallback-value"}
+ result (fs/execute policy-map (throw (ex-info "fail" {})))]
+ (is (= "fallback-value" result))))
+
+ (testing "multiple map-based policies"
+ (let [retry-map {:type :retry :max-retries 2}
+ fallback-map {:type :fallback :result "fallback"}
+ executor (fs/executor [fallback-map retry-map])
+ counter (atom 0)
+ result (fs/execute executor
+ (do
+ (swap! counter inc)
+ (throw (ex-info "fail" {}))))]
+ (is (= "fallback" result))
+ (is (= 3 @counter))))
+
+ (testing "mixed map and regular policies"
+ (let [retry-map {:type :retry :max-retries 2}
+ fallback-policy (fs/fallback {:result "fallback"})
+ executor (fs/executor [fallback-policy retry-map])
+ counter (atom 0)
+ result (fs/execute executor
+ (do
+ (swap! counter inc)
+ (throw (ex-info "fail" {}))))]
+ (is (= "fallback" result))
+ (is (= 3 @counter)))))
+
+ (testing "integration scenarios"
+ (testing "circuit breaker with fallback"
+ (let [cb-map {:type :circuit-breaker :delay-ms 1000 :failure-threshold 2}
+ fb-map {:type :fallback :result "fallback"}
+ executor (fs/executor [fb-map cb-map])]
+ (is (= "fallback" (fs/execute executor (throw (ex-info "fail1" {})))))
+ (is (= "fallback" (fs/execute executor (throw (ex-info "fail2" {})))))
+ (is (= "fallback" (fs/execute executor "should not execute")))))
+
+ (testing "retry with timeout"
+ (let [retry-map {:type :retry :max-retries 5 :delay-ms 100}
+ timeout-map {:type :timeout :timeout-ms 250}
+ executor (fs/executor [timeout-map retry-map])]
+ (is (thrown? TimeoutExceededException
+ (fs/execute executor
+ (do
+ (Thread/sleep 50)
+ (throw (ex-info "retry" {})))))))))
+
+ (testing "async execution"
+ (testing "map-based policy with execute-async"
+ (f/with-pool :io
+ (let [retry-map {:type :retry :max-retries 2}
+ counter (atom 0)
+ result (fs/execute-async retry-map
+ (f/async
+ (swap! counter inc)
+ (when (< @counter 2)
+ (throw (ex-info "retry" {})))
+ "success"))]
+ (is (= "success" @result))
+ (is (>= @counter 2))))))
+
+ (testing "callback support"
+ (testing "all callback options work with map-based policies"
+ (let [success-called (atom false)
+ policy-map {:type :retry
+ :max-retries 2
+ :on-success-fn (fn [_] (reset! success-called true))}
+ counter (atom 0)]
+ (fs/execute policy-map
+ (do
+ (swap! counter inc)
+ (when (< @counter 2)
+ (throw (ex-info "retry" {})))
+ "success"))
+ (is (= 2 @counter))
+ (is @success-called))))
+
+ (testing "build option behavior"
+ (testing "map-based policy always builds (build:false is overridden)"
+ (let [policy-map {:type :retry :max-retries 3 :build false}
+ policy (fs/build-policy policy-map)]
+ (is (instance? RetryPolicy policy))
+ (is (instance? Policy policy)))))))
+
+;; Stateful Policy Validation Tests
+
+(deftest test-stateful-policy-map-validation
+ (testing "execute throws when using stateful policy maps directly"
+ (testing "circuit-breaker map throws"
+ (is (thrown-with-msg?
+ clojure.lang.ExceptionInfo
+ #"Cannot execute with stateful policy map directly"
+ (fs/execute {:type :circuit-breaker :failure-threshold 5}
+ "result"))))
+
+ (testing "rate-limiter map throws"
+ (is (thrown-with-msg?
+ clojure.lang.ExceptionInfo
+ #"Cannot execute with stateful policy map directly"
+ (fs/execute {:type :rate-limiter :max-executions 10 :period-ms 1000}
+ "result"))))
+
+ (testing "bulkhead map throws"
+ (is (thrown-with-msg?
+ clojure.lang.ExceptionInfo
+ #"Cannot execute with stateful policy map directly"
+ (fs/execute {:type :bulkhead :max-concurrency 5}
+ "result")))))
+
+ (testing "execute-async throws when using stateful policy maps directly"
+ (testing "circuit-breaker map throws"
+ (is (thrown-with-msg?
+ clojure.lang.ExceptionInfo
+ #"Cannot execute with stateful policy map directly"
+ (fs/execute-async {:type :circuit-breaker :failure-threshold 5}
+ (f/async "result")))))
+
+ (testing "rate-limiter map throws"
+ (is (thrown-with-msg?
+ clojure.lang.ExceptionInfo
+ #"Cannot execute with stateful policy map directly"
+ (fs/execute-async {:type :rate-limiter :max-executions 10 :period-ms 1000}
+ (f/async "result")))))
+
+ (testing "bulkhead map throws"
+ (is (thrown-with-msg?
+ clojure.lang.ExceptionInfo
+ #"Cannot execute with stateful policy map directly"
+ (fs/execute-async {:type :bulkhead :max-concurrency 5}
+ (f/async "result"))))))
+
+ (testing "execute throws when stateful map is in list of policies"
+ (testing "list with circuit-breaker map throws"
+ (is (thrown-with-msg?
+ clojure.lang.ExceptionInfo
+ #"Cannot execute with stateful policy map directly"
+ (fs/execute [{:type :retry :max-retries 3}
+ {:type :circuit-breaker :failure-threshold 5}]
+ "result"))))
+
+ (testing "nested list with bulkhead map throws"
+ (is (thrown-with-msg?
+ clojure.lang.ExceptionInfo
+ #"Cannot execute with stateful policy map directly"
+ (fs/execute [[{:type :fallback :result "default"}]
+ [{:type :bulkhead :max-concurrency 5}]]
+ "result")))))
+
+ (testing "stateless policy maps work correctly"
+ (testing "retry map works"
+ (is (= "result" (fs/execute {:type :retry :max-retries 3} "result"))))
+
+ (testing "timeout map works"
+ (is (= "result" (fs/execute {:type :timeout :timeout-ms 1000} "result"))))
+
+ (testing "fallback map works"
+ (is (= "result" (fs/execute {:type :fallback :result "default"} "result")))))
+
+ (testing "built policies and executors work correctly"
+ (testing "executor with stateful policy map works"
+ (let [executor (fs/executor {:type :circuit-breaker :failure-threshold 5})]
+ (is (= "result" (fs/execute executor "result")))))
+
+ (testing "policies function with stateful policy map works"
+ (let [policy-list (fs/policies {:type :circuit-breaker :failure-threshold 5})]
+ (is (= "result" (fs/execute policy-list "result")))))
+
+ (testing "explicit policy works"
+ (let [policy (fs/circuit-breaker {:failure-threshold 5})]
+ (is (= "result" (fs/execute policy "result")))))))
diff --git a/test/failsage/impl_test.clj b/test/failsage/impl_test.clj
index ec0b889..e27d7e9 100644
--- a/test/failsage/impl_test.clj
+++ b/test/failsage/impl_test.clj
@@ -1,69 +1,16 @@
(ns failsage.impl-test
(:require
[clojure.test :refer [deftest is testing]]
+ [failsage.core]
[failsage.impl :as impl]
[futurama.core :as f])
(:import
[dev.failsafe
- Bulkhead
- CircuitBreaker
ExecutionContext
Failsafe
- Fallback
- Policy
- RateLimiter
- RetryPolicy
- Timeout]
- [java.time Duration]
+ RetryPolicy]
[java.util.concurrent ExecutorService]))
-;; IPolicyBuilder Protocol Tests
-
-(deftest test-build-policy-bulkhead-builder
- (testing "build-policy builds a Bulkhead from BulkheadBuilder"
- (let [builder (-> (Bulkhead/builder 5)
- (.withMaxWaitTime (Duration/ofMillis 100)))
- policy (impl/build-policy builder)]
- (is (instance? Bulkhead policy))
- (is (instance? Policy policy)))))
-
-(deftest test-build-policy-circuit-breaker-builder
- (testing "build-policy builds a CircuitBreaker from CircuitBreakerBuilder"
- (let [builder (-> (CircuitBreaker/builder)
- (.withDelay (Duration/ofMillis 100)))
- policy (impl/build-policy builder)]
- (is (instance? CircuitBreaker policy))
- (is (instance? Policy policy)))))
-
-(deftest test-build-policy-fallback-builder
- (testing "build-policy builds a Fallback from FallbackBuilder"
- (let [builder (Fallback/builder "default-value")
- policy (impl/build-policy builder)]
- (is (instance? Fallback policy))
- (is (instance? Policy policy)))))
-
-(deftest test-build-policy-rate-limiter-builder
- (testing "build-policy builds a RateLimiter from RateLimiterBuilder"
- (let [builder (RateLimiter/smoothBuilder 10 (Duration/ofSeconds 1))
- policy (impl/build-policy builder)]
- (is (instance? RateLimiter policy))
- (is (instance? Policy policy)))))
-
-(deftest test-build-policy-retry-policy-builder
- (testing "build-policy builds a RetryPolicy from RetryPolicyBuilder"
- (let [builder (-> (RetryPolicy/builder)
- (.withMaxRetries 3))
- policy (impl/build-policy builder)]
- (is (instance? RetryPolicy policy))
- (is (instance? Policy policy)))))
-
-(deftest test-build-policy-timeout-builder
- (testing "build-policy builds a Timeout from TimeoutBuilder"
- (let [builder (Timeout/builder (Duration/ofSeconds 5))
- policy (impl/build-policy builder)]
- (is (instance? Timeout policy))
- (is (instance? Policy policy)))))
-
;; EventListener Converter Tests
(deftest test-event-listener-creation
@@ -203,62 +150,6 @@
(let [pool (impl/get-pool nil)]
(is (instance? ExecutorService pool))))))
-;; Policy List Tests
-
-(deftest test-get-policy-list-with-single-policy
- (testing "get-policy-list handles a single policy"
- (let [policy (.build (RetryPolicy/builder))
- result (impl/get-policy-list policy)]
- (is (= 1 (count result)))
- (is (instance? Policy (first result))))))
-
-(deftest test-get-policy-list-with-multiple-policies
- (testing "get-policy-list handles multiple policies"
- (let [policy1 (.build (RetryPolicy/builder))
- policy2 (.build (Timeout/builder (Duration/ofSeconds 5)))
- result (impl/get-policy-list policy1 policy2)]
- (is (= 2 (count result)))
- (is (every? #(instance? Policy %) result)))))
-
-(deftest test-get-policy-list-with-builder
- (testing "get-policy-list builds policies from builders"
- (let [builder (RetryPolicy/builder)
- result (impl/get-policy-list builder)]
- (is (= 1 (count result)))
- (is (instance? RetryPolicy (first result))))))
-
-(deftest test-get-policy-list-with-mixed-types
- (testing "get-policy-list handles mixed policies and builders"
- (let [policy (.build (RetryPolicy/builder))
- builder (Timeout/builder (Duration/ofSeconds 5))
- result (impl/get-policy-list policy builder)]
- (is (= 2 (count result)))
- (is (instance? RetryPolicy (first result)))
- (is (instance? Timeout (second result))))))
-
-(deftest test-get-policy-list-filters-nil
- (testing "get-policy-list filters out nil values"
- (let [policy (.build (RetryPolicy/builder))
- result (impl/get-policy-list policy nil nil)]
- (is (= 1 (count result))))))
-
-(deftest test-get-policy-list-flattens-nested
- (testing "get-policy-list flattens nested collections"
- (let [policy1 (.build (RetryPolicy/builder))
- policy2 (.build (Timeout/builder (Duration/ofSeconds 5)))
- result (impl/get-policy-list [policy1 policy2])]
- (is (= 2 (count result))))))
-
-(deftest test-get-policy-list-empty
- (testing "get-policy-list handles empty input"
- (let [result (impl/get-policy-list)]
- (is (empty? result)))))
-
-(deftest test-get-policy-list-invalid-type
- (testing "get-policy-list throws on invalid policy type"
- (is (thrown-with-msg? Exception #"Invalid policy type"
- (impl/get-policy-list "not-a-policy")))))
-
;; Execute Tests
(deftest test-execute-get-basic
@@ -300,48 +191,6 @@
(is (= "success" result))
(is (= 2 @counter)))))
-;; ->executor Tests
-
-(deftest test-executor-creation-no-args
- (testing "->executor creates an executor with no arguments"
- (let [executor (impl/->executor)]
- (is (some? executor))
- (is (instance? dev.failsafe.FailsafeExecutor executor)))))
-
-(deftest test-executor-creation-with-policies
- (testing "->executor creates an executor with policies"
- (let [retry-policy (.build (RetryPolicy/builder))
- executor (impl/->executor [retry-policy])]
- (is (some? executor))
- (is (instance? dev.failsafe.FailsafeExecutor executor)))))
-
-(deftest test-executor-creation-with-pool-and-policies
- (testing "->executor creates an executor with pool and policies"
- (let [retry-policy (.build (RetryPolicy/builder))
- executor (impl/->executor :io [retry-policy])]
- (is (some? executor))
- (is (instance? dev.failsafe.FailsafeExecutor executor)))))
-
-(deftest test-executor-creation-from-executor
- (testing "->executor accepts a FailsafeExecutor and returns a new one with pool"
- (let [base-executor (Failsafe/none)
- executor (impl/->executor :io base-executor)]
- (is (some? executor))
- (is (instance? dev.failsafe.FailsafeExecutor executor)))))
-
-(deftest test-executor-creation-with-empty-policies
- (testing "->executor with empty policies uses Failsafe.none"
- (let [executor (impl/->executor [])]
- (is (some? executor))
- (is (instance? dev.failsafe.FailsafeExecutor executor)))))
-
-(deftest test-executor-creation-with-single-policy
- (testing "->executor with single policy (not in collection)"
- (let [retry-policy (.build (RetryPolicy/builder))
- executor (impl/->executor retry-policy)]
- (is (some? executor))
- (is (instance? dev.failsafe.FailsafeExecutor executor)))))
-
;; record-async-success and record-async-failure Tests
(deftest test-record-async-success
@@ -420,7 +269,7 @@
result (impl/execute-get-async executor
(fn [ctx]
(f/async
- (impl/record-async-success ctx 42))))]
+ (impl/record-async-success ctx 42))))]
(is (= 42 @result)))))
(deftest test-execute-get-async-with-exception
@@ -429,10 +278,10 @@
result (impl/execute-get-async executor
(fn [ctx]
(f/async
- (try
- (throw (ex-info "async error" {}))
- (catch Throwable t
- (impl/record-async-failure ctx t))))))]
+ (try
+ (throw (ex-info "async error" {}))
+ (catch Throwable t
+ (impl/record-async-failure ctx t))))))]
(is (thrown-with-msg? Exception #"async error"
@result)))))
@@ -444,7 +293,7 @@
(fn [ctx]
(reset! received-context ctx)
(f/async
- (impl/record-async-success ctx 42))))]
+ (impl/record-async-success ctx 42))))]
(is (= 42 @result))
(is (some? @received-context))
(is (instance? dev.failsafe.AsyncExecution @received-context)))))