Open onetom opened 3 years ago
In case it's not clear from the main description, (swap! test-clk t/>> (t/new-duration 5 :minutes))
would be the way to change the time within automated tests. I would rather write it as (swap! test-clk t/>> 5 :minutes)
, but that's an orthogonal issue.
Another thing I haven't made clear probably is that my proposal would be the replacement of tick.core/AtomicClock
. While it's a funny name, fusing the Clojure atom concept with the java.time.Clock; atomic clock also has a well-defined meaning in the problem-domain of time and I find it misleading. My first impression was that it has something to do with NTP and obtaining some hyper-precise time. That's why I came up with the time-capsule mental-model. I specifically thought of this visualization of a possible warp-drive: https://en.wikipedia.org/wiki/Alcubierre_drive
I also forgot to mention the t/with-clock
facility, which is described here: https://www.juxt.land/tick/docs/index.html#_substitution
While t/with-clock
solves the problem for a single thread, using dynamic vars is trickier in multi-threaded situations. For example, in an integration test, where we spin up a temporary http-kit
server for our API on a random port and talk to it via a http-kit
client, we are going to deal with multiple threads. Of course, it's questionable, whether we should use fake clocks in such a test, but I found it useful for exploratory, system modelling work.
Nice problem statement!
I agree that with-clock
has issues, but they are part of the java.time/tick approach to getting the now time - you can choose to pass a clock around, or just use the ambient clock. having a *clock*
binding, tick makes the ambient-clock route even more tempting. Still, I think for a lot of use cases that works ok.
Is the intent for the capsule to be referred to explicitly by any code that needs now
?
FYI for another reference point... and something will make it into tick eventually, is https://tc39.es/proposal-temporal/docs/index.html#Temporal-now - which serves the same function as java.time Clock. In https://github.com/henryw374/tempo I'm looking at making an api for 'clocks' that works with Temporal.now and java.time.Clock
fyi mutable clock in clojure https://gist.github.com/henryw374/2291e787087eeea513f9a8e5a5bd6f69
is there any significance of using mutable_clock.MutableClock
, instead of just MutableClock
in the proxy
call?
good question. I don't think so.... I tried just now without and it seems to work ok. If I had a reason it has been forgotten as this was a few months ago. Also looking at this again, it would be good to be able to atomically set zone and instant. maybe it should just be a zoned-date-time.
Circling back to the capsule idea... I am leaning towards preferring to create instances of java.time.Clock (or in js-land js/Temporal.Now) as these things work with the native APIs (just constructor fns afaik).
I think 1-6 can still be addressed with that constraint. but pls let me know otherwise.
and if so... I guess having a mutable clock in the lib would be handy. and anything more esoteric can be done in userspace.
btw in Tempo there are no zero-args 'now' fns - you have to provide a clock. I've come to prefer working that way with tick - ie not using with-clock at all.
Very exciting!
In our app we have a :clk
component in our system map and everything relies on it explicitly.
We inject it into our Datomic components too, so even the db schema can be transacted in the past virtually, so we can transact theoretical past scenarios in our tests (because Datomic doesn't allow future transactions).
The only thing we couldn't control the clock of is java.net.CookieManager
, which just calls System/currentTimeMillis
directly in its cleanup routine, which throws away expired cookies, we couldn't test session expiry code in full integration over a web server.
I haven't explored the Tempo
lib yet, but we have ended up not really utilizing the cross-platform nature of tick
, so I might consider falling back to Tempo, to reduce complexity.
+1 for using java.time.Clock
, if possible, for better interop.
I'll try to review this thread in the coming ~2weeks, to give some feedback.
another clock implementation https://gist.github.com/henryw374/38d61b20c62cd5450331f36ee9029a61
Problem
In automated tests, it's desirable to control time. It is achieved by something typically called a fake clock. The Use a fake system clock article enumerated the expected behaviours of such a clock:
These lists completely matched my expectations and felt common sense to me. To my surprise, only few of these use-cases were supported out of the box, by
java.time.Clock
or thisjuxt/tick
library. I don't see how can I achieve use-case 4, 5 and 6.java.time.Clock/fixed
solves use-case 1, 2 and 3.tick.core/AtomicClock
kinda solves use-case 4, but at the cost of introducing custom variants ofclojure.core/atom,{reset,swap}{,-vals}!,compare-and-set!
. The mentioned article demonstrates a solution to use-case 5, by a custom implementation ofj.t.Clock
.I've found a Java implementation of a
MutableClock
It solves use-case 4, but not 5. It's also introducing extra, custom API (set
,setInstant
) for changing the clock; typical parochialism. Its implementation is complected with concerns like serialization, and the way the clock might be changed. Neither of these are very desirable in Clojure context. ThisMutableClock
is also a bit obscure, because I haven't found it mentioned in higher-level documentations, only in the auto-generated Java docs. I don't feel confident using it and pulling in this extra library, just for this simple concept, despite it's authored by the same @jodastephen, who is the shepherd ofjava.time
and JSR-310.Solution
To cater for use-case 3 and 4, while maintaining a convenient and idiomatic use for use-case 7, I propose constructing a time-capsule concept. We can imagine this capsule having a clock-stand, which holds a concrete clock. This clock might be standing still at a time we specify, or ticking at the same, or a different rate as the system clock. During tests, we can replace the clock on this imaginary stand, with a different one, which might be derived from the one already on the stand.
From an application's point of view, this capsule should just look like any other
j.t.Clock
. Sincej.t.Clock
is just an abstract class, not a Java interface, we cannot extend it conveniently from Clojure, without providing a full-blown implementation for it and getting sucked into OO-land.Instead, we can treat clocks as something
deref
able, just liket/AtomicClock
does, to conveniently take a reading of their current time. To adjust the clock though, we need access to both the clock and the stand itself, toswap!
it, so references to the capsule won't be affected.Here is a possible implementation, for Clojure-only, for the sake of clarity:
I've also omitted
reset!
,reset-vals!
andswap-vals!
for the sake of simplifying discussion, by focusing on the core idea.When I tried to print a
time/capsule
, I got an error about multi-method ambiguity, which I resolved with:I don't have a lot of experience with hierarchies in Clojure, so I'm not sure how to avoid this ambiguity, or what's better to prefer. I should probably just provide a
(defmethod print-method Capsule)
(and itspprint
variant), but I'm not sure yet, what should the implementation look like.Here is a demonstration of using a
time/capsule
:Pros:
j.t.Clock
implementingIDeref
, as opposed to an(atom (Clock/system.))
, which would require(t/instant @clk-holder)
to be read, or maybe@@clock-holder
somehow.Cons:
swap!
to its transformation function, is the clock, which is different from the value we get withderef
, which is the time on that clock.IAtom
,IAtom2
and the ClojureScriptIReset
andISwap
protocols is quite a lot of boilerplate