babashka/test-resources/lib_tests/again/core_test.clj

335 lines
11 KiB
Clojure
Raw Permalink Normal View History

(ns again.core-test
(:require [again.core :as a :refer [with-retries]]
[clojure.test :refer [is deftest testing]]
[clojure.test.check.clojure-test :refer [defspec]]
[clojure.test.check.generators :as gen]
[clojure.test.check.properties :as prop]))
(defspec spec-max-retries
200
(prop/for-all
[n gen/s-pos-int]
(let [s (a/max-retries n (repeat 0))]
(= (count s) n))))
(defspec spec-clamp-delay
200
(prop/for-all
[n gen/s-pos-int
max-delay gen/s-pos-int]
(let [s (a/max-retries
n
;; The increment is picked so that we'll cross max-delay on delay 3
(a/clamp-delay max-delay (a/additive-strategy 0 (/ max-delay 2))))]
(every? #(<= % max-delay) s))))
(defspec spec-max-delay
200
(prop/for-all
[n gen/s-pos-int
max-delay gen/s-pos-int]
(let [s (a/max-retries
n
(a/max-delay max-delay (a/additive-strategy 0 (/ max-delay 10))))]
(and (= (count s) (min n 10))
(every? #(<= % max-delay) s)))))
(defspec spec-max-duration
200
(prop/for-all
[d gen/s-pos-int]
(let [s (take (* 2 d) (a/max-duration d (a/constant-strategy 1)))]
(and (= (count s) d)
(= (reduce + s) d)))))
(deftest test-max-duration
(testing "with not enough delays to satisfy specified duration"
(is (= (a/max-duration 10000 [0]) [0]))))
(defspec spec-constant-strategy
200
(prop/for-all
[n gen/s-pos-int
delay gen/pos-int]
(let [s (a/max-retries n (a/constant-strategy delay))]
(and (= (count s) n)
(= (set s) #{delay})))))
(defspec spec-immediate-strategy
200
(prop/for-all
[n gen/s-pos-int]
(let [s (a/max-retries n (a/immediate-strategy))]
(and (= (count s) n)
(= (set s) #{0})))))
(defspec spec-additive-strategy
200
(prop/for-all
[n gen/s-pos-int
initial-delay gen/pos-int
increment gen/pos-int]
(let [s (a/max-retries n (a/additive-strategy initial-delay increment))
p (fn [[a b]] (= (+ a increment) b))]
(and (= (count s) n)
(= (first s) initial-delay)
(every? p (partition 2 1 s))))))
(defspec spec-multiplicative-strategy
200
(prop/for-all
[n gen/s-pos-int
initial-delay gen/s-pos-int
delay-multiplier (gen/elements [1.0 1.1 1.3 1.6 2.0 3.0 5.0 9.0 14.0 20.0])]
(let [s (a/max-retries
n
(a/multiplicative-strategy initial-delay delay-multiplier))
p (fn [[a b]] (= (* a delay-multiplier) b))]
(and (= (count s) n)
(= (first s) initial-delay)
(every? p (partition 2 1 s))))))
(defspec spec-randomize-delay
200
(prop/for-all
[rand-factor (gen/elements [0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9])
delay gen/s-pos-int]
(let [randomize-delay #'again.core/randomize-delay
min-delay (bigint (* delay (- 1 rand-factor)))
max-delay (bigint (inc (* delay (+ 1 rand-factor))))
rand-delay (randomize-delay rand-factor delay)]
(and (<= 0 rand-delay)
(<= min-delay rand-delay max-delay)))))
(defspec spec-randomize-strategy
200
(prop/for-all
[n gen/s-pos-int
rand-factor (gen/elements [0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9])]
(let [initial-delay 1000
s (a/max-retries
n
(a/randomize-strategy
rand-factor
(a/constant-strategy initial-delay)))
min-delay (bigint (* initial-delay (- 1 rand-factor)))
max-delay (bigint (inc (* initial-delay (+ 1 rand-factor))))]
(every? #(<= min-delay % max-delay) s))))
(deftest test-stop-strategy
(is (empty? (a/stop-strategy)) "stop strategy has no delays"))
(defn new-failing-fn
"Returns a map consisting of the following fields:
- `f` - a function that will succeed on the `n`th call
- `attempts` - an atom counting the number of executions of `f`
- `exception` - the exception that `f` throws until it succeeds"
[& [n]]
(let [n (or n Integer/MAX_VALUE)
attempts (atom 0)
exception (Exception. "retry")
f #(let [i (swap! attempts inc)]
(if (< i n)
(throw exception)
i))]
{:attempts attempts :exception exception :f f}))
(defn new-callback-fn
"Returns a map consisting of the following fields:
- `callback` - a callback function to pass to `with-retries` that will fail
the operation early after the `n`th call
- `args` - an atom recording the arguments passed to `callback`"
[& [n]]
(let [n (or n Integer/MAX_VALUE)
attempts (atom 0)
args (atom [])
callback #(let [i (swap! attempts inc)]
(swap! args conj %)
(when (< n i)
::a/fail))]
{:args args :callback callback}))
(defspec spec-with-retries
200
(prop/for-all
[strategy (gen/vector gen/s-pos-int)]
(let [{:keys [attempts f]} (new-failing-fn)
delays (atom [])]
(with-redefs [a/sleep #(swap! delays conj %)]
(try
(with-retries strategy (f))
(catch Exception _)))
(and (= @attempts (inc (count strategy)))
(= @delays strategy)))))
(deftest test-with-retries
(with-redefs [a/sleep (constantly nil)]
(testing "with-retries"
(testing "with non-nil return value"
(is (= (with-retries [] :ok) :ok) "returns form value"))
(testing "with nil return value"
(is (nil? (with-retries [] nil)) "returns form value"))
(testing "with user-context"
(let [context {:a :b}
{:keys [args callback]} (new-callback-fn)
options {::a/callback callback
::a/strategy []
::a/user-context context}
_ (with-retries options :ok)]
(is (= (count @args) 1) "calls callback hook once")
(is (= (::a/user-context (first @args)) context)
"calls callback hook with user context")))
(testing "with success on first try"
(let [{:keys [attempts f]} (new-failing-fn 1)
{:keys [args callback]} (new-callback-fn)]
(with-retries
{::a/callback callback
::a/strategy []}
(f))
(is (= @attempts 1) "executes operation once")
(is (= (count @args) 1) "calls callback hook once")
(is (= (first @args)
{::a/attempts 1
::a/slept 0
::a/status :success})
"calls callback hook with success")))
(testing "with success on second try"
(let [{:keys [attempts exception f]} (new-failing-fn 2)
{:keys [args callback]} (new-callback-fn)]
(with-retries
{::a/callback callback
::a/strategy [12]}
(f))
(is (= @attempts 2) "executes operation twice")
(is (= (count @args) 2) "calls callback hook twice")
(is (= (first @args)
{::a/attempts 1
::a/exception exception
::a/slept 0
::a/status :retry})
"calls callback hook with failure")
(is (= (second @args)
{::a/attempts 2
::a/slept 12
::a/status :success})
"calls callback hook with success")))
(testing "with permanent failure"
(let [{:keys [exception f]} (new-failing-fn)
{:keys [args callback]} (new-callback-fn)]
(is (thrown?
Exception
(with-retries
{::a/callback callback
::a/strategy [123]}
(f)))
"throws exception")
(is (= (count @args) 2) "calls callback hook twice")
(is (= (first @args)
{::a/attempts 1
::a/exception exception
::a/slept 0
::a/status :retry})
"calls callback hook with failure")
(is (= (second @args)
{::a/attempts 2
::a/exception exception
::a/slept 123
::a/status :failure})
"calls callback hook with permanent failure")))
(testing "with early failure"
(let [{:keys [exception f]} (new-failing-fn)
{:keys [args callback]} (new-callback-fn 1)]
(is (thrown?
Exception
(with-retries
{::a/callback callback
::a/strategy [1 2 3]}
(f)))
"throws exception")
(is (= (count @args) 2) "calls callback hook three twice")
(is (= (first @args)
{::a/attempts 1
::a/exception exception
::a/slept 0
::a/status :retry})
"first callback call")
(is (= (second @args)
{::a/attempts 2
::a/exception exception
::a/slept 1
::a/status :retry})
"last callback call"))))))
(defmulti log-attempt ::a/status)
(defmethod log-attempt :retry [s]
(if (< (count @(::a/user-context s)) 1)
(swap! (::a/user-context s) conj :retry)
(do
(swap! (::a/user-context s) conj :fail)
::a/fail)))
(defmethod log-attempt :success [s]
(swap! (::a/user-context s) conj :success))
(defmethod log-attempt :failure [s]
(swap! (::a/user-context s) conj :failure))
(defmethod log-attempt :default [s] (assert false))
(deftest test-multimethod-callback
(with-redefs [a/sleep (constantly nil)]
(testing "multi-method-callback"
(testing "with success"
(let [{:keys [f]} (new-failing-fn 2)
user-context (atom [])]
(with-retries
{::a/callback log-attempt
::a/strategy [1 2]
::a/user-context user-context}
(f))
(is (= (count @user-context) 2) "multimethod is called twice")
(is (= (first @user-context) :retry) "first call is a retry")
(is (= (second @user-context) :success) "second call is a success")))
(testing "with failure"
(let [{:keys [exception f]} (new-failing-fn)
user-context (atom [])]
(try
(with-retries
{::a/callback log-attempt
::a/strategy [1]
::a/user-context user-context}
(f))
(catch Exception e
(is (= e exception) "Unexpected exception")))
(is (= (count @user-context) 2) "multimethod is called twice")
(is (= (first @user-context) :retry) "first call is a retry")
(is (= (second @user-context) :failure) "second call is a failure")))
(testing "with early failure"
(let [{:keys [exception f]} (new-failing-fn)
user-context (atom [])]
(try
(with-retries
{::a/callback log-attempt
::a/strategy [1 2]
::a/user-context user-context}
(f))
(catch Exception e
(is (= e exception) "Unexpected exception")))
(is (= (count @user-context) 2) "multimethod is called three times")
(is (= (first @user-context) :retry) "first call is a retry")
(is (= (second @user-context) :fail) "second call is a fail"))))))