textual
textual copied to clipboard
Mock time
We should make sure that there is only one source of time in Textual.
Ideally App should have get_time function which returns the current time.
We should have a mechanism to replace the system time with an alternative time, which we can use to ensure tests are deterministic.
Suggest that App accepts a get_time callable which will replace monotonic
We already use time_machine in the tests, is that suitable for whatever use case you're thinking of here?
Maybe. @DrBenton Do you think time_machine would be adequate to make the tests quicker / deterministic?
ah, I knew I had seen a time-mocking library in Textual or Rich's dev dependencies, but I was not sure which project it was and whether if was still used or not! ^_^
If we have time_machine already on board we can likely use it indeed :slightly_smiling_face:
Ok. I have a feeling that a passing a get_time callable may also be worthwhile, and its a small change anyway. Will leave this ticket up for graphs, but sounds like time_machine is the way to go for integration tests.
@DrBenton Do you think it would be worthwhile making that change to your recent integration test?
@willmcgugan yes, I'll try to integrate such a "time control" machinery to our integration test classes! :slightly_smiling_face:
@willmcgugan @darrenburns From my attempts to mock time in a Textual app so far, it seems to me that the current machinery makes it quite difficult...
TLDR; A centralised "Textual time" orchestrator could allow us not only to mock time properly in our integration tests, but also potentially improve performance (by avoiding too many calls to the system clock)
The problem
In the test of a Django app for example, almost everything is pretty stateless and the database is the single source of truth, which makes time mocking rather easy to use.
However, in our case our Timers (which are the workhorse of Textual's lifecycle) are stateful units, without connection between them, and all running their own endless loop and triggering stuff in this loop when certain conditions are met. During these loops there is a combination of calls to asyncio.sleep(delay) and checks of the monotonic clock.
So even if I mock the source of monotonic time, it's really hard to have consistent results as the Timers use various time-based patterns to do their stuff :grimacing:
Potential alternative implementation
One option I could see to solve this would be to have a single time-based loop in a Textual app, running a loop cycle 60 times per second for example, and this single loop would iterate though all the active Timer instances instances 60 times per second, check what the own ticking period of each Timer is, and execute their tick if it's their turn to go.
In such an architecture each Timer would rely on this orchestrator to be triggered, and not do any "clock wall time"-related check themselves. By having a single orchestrator of the time, which would act as as single source of truth for time that passes, we would be able to mock animations more easily for example - by simply running ticks in a accelerated way in the orchestrator.
As a side effect such an implementation could make our Timers perform better overall, as the monotonic clock would be used only once per event loop cycle? The Octopus Energy's Python conventions guide for example warns against using "system clocks" - and even though I don't know the details of how the underlying monotonic clock is implemented, I reckon it does such calls under the hood?
Avoid calls to the system clock in the domain layer of the application. That is, calls to
localtime.now(),localtime.today()etc. Think of such calls like network or database calls.
I don't think there is any performance issue in getting the time, it's a fairly lean operation:
>>> monotonic()-monotonic()
-1.1669471859931946e-06
I think I see where you are going with an orchestrator. I hadn't considered asyncio.sleep as a sneaky source of time that we can't control.
But I suspect 60fps is going to be a performance drain. If I have understood you correctly, it would have to be running all the time. Even if it wasn't doing anything, thats still a cpu drain.
How about we move all explicit calls to get the time in to app, which we can mock. We also move the responsibility for asyncio.sleep in to App. In normal operation we defer to asyncio.sleep, but for testing we base it on our mocked time.
I'm thinking a method like App.advance_time(delta) which moves the clock forward and wakes up any sleeping cooroutines.
Let's have a chat about it tomorrow. It's a tricky topic, and it's worth having the 3 of us on this.
https://github.com/Textualize/textual/wiki/Sorry-we-closed-your-issue
Did we solve your problem?
Glad we could help!