Manipulating time like a TimeLord with Flux
Reading Time:
Reading Time:
Testing is a cornerstone of robust software development, but it presents unique challenges when time is a critical factor. How do you verify that a notification is sent exactly 24 hours after an event without making your test suite wait for an entire day? How do you ensure that time-sensitive logic is immune to the small, unpredictable delays of real-world execution? Mocking Python's built-in time
module can quickly become a tangled mess of patched calls and fragile tests.
This is precisely where Flux comes in. Flux is a Python library designed to give you complete and deterministic control over time in your tests. It allows you to create virtual timelines, making it trivial to fast-forward, freeze, and schedule events without the wait. In this article, we'll explore how Flux works, dive into real-world use cases, and walk through how to replace unwieldy, time-based tests with elegant, fast, and reliable ones.
Flux is all about giving you full control over time in your Python tests and simulations. In a nutshell, it:
time.sleep()
or time.time()
across different modules.This means you can test things like “what if my function runs for a whole day?” without waiting 24 real hours. You can also freeze time for deterministic tests or accelerate time to watch processes play out in a fraction of their usual duration.
Here are just a few places where Flux can make your life easier:
Unit Testing Long Waits
Accelerating Simulations
Freezing Time for Deterministic Tests
freeze()
), your time-based tests can make exact comparisons without worrying about the overhead of Python or other system delays.Scheduling Automated Callbacks
Seamless Global Integration
time
module calls in your entire project (or specific modules) with a single global timeline, making your application’s notion of time fully under your control.If Flux is on PyPI or you have it locally, install it with:
pip install flux
(Or adjust accordingly if you have a different installation process.)
Timeline
Flux offers a class called Timeline
. This is the hero of the story—an object that represents a virtual clock. Let’s see it in action:
from flux import Timeline
# Create a timeline instance
timeline = Timeline()
# Get the current virtual time
print(f"Current virtual time: {timeline.time()}")
At creation, the timeline starts at a default epoch (similar to the real time’s epoch). You can query it just like time.time()
in standard Python.
Timeline.sleep()
behaves similarly to time.sleep()
, but it advances virtual time:
print(f"Before sleep: {timeline.time()}")
# Sleep 10 virtual seconds
timeline.sleep(10)
print(f"After sleep: {timeline.time()}")
By default, s>time factor = 1, so sleeping 10 virtual seconds takes 10 real seconds—unless you change the rules.
Think of this like the classic sci-fi or fantasy trope where time moves differently in another dimension—five years in Narnia might be only two minutes in the real world. The time factor in Flux gives you that exact power over your code's timeline. It determines how many virtual seconds pass per real second. For example:
timeline.set_time_factor(5)
print(f"Time factor: {timeline.get_time_factor()}")
# Now, sleeping 2 virtual seconds will only take 0.4 real seconds.
timeline.sleep(2)
This is a game-changer if you want to speed up or slow down time-dependent logic.
Sometimes, you don’t want the timeline to progress automatically at all. This is where freezing comes in. When time is frozen (by setting the time factor to 0), timeline.sleep()
is the only way to advance the virtual clock, and it does so instantly with no real-world delay.
timeline.freeze() # Sets time factor to 0
print(f"Frozen time: {timeline.time()}")
# Sleeping will instantly advance the virtual clock with no real waiting
timeline.sleep(30)
print(f"Time after 'sleeping' on frozen timeline: {timeline.time()}")
Perfect for avoiding flaky tests caused by unpredictable real-world time offsets.
Timeline.schedule_callback(when, callback_function)
allows you to set a future point in the virtual timeline to run a function. If you need to wait until all scheduled callbacks have triggered, the library provides a convenient sleep_wait_all_scheduled()
method.
def say_hello():
print(f"Hello at virtual time: {timeline.time()}")
timeline.schedule_callback(timeline.time() + 50, say_hello)
timeline.sleep(60) # The callback fires after we've passed the scheduled time
Let’s walk through how Flux shines in typical situations you might face.
Ever needed to test that a function notifies someone after a day’s wait? With Flux, no problem:
import time
def long_running_func(_sleep=time.sleep, _time=time.time):
start = _time()
while True:
if _time() - start > 60 * 60 * 24: # 24 hours
print("Notification triggered!")
break
_sleep(30)
# Test using a virtual timeline
from flux import Timeline
def test_long_wait():
timeline = Timeline()
timeline.set_time_factor(0) # freeze time to skip real waits
# After 24 hours + 1 second in virtual time, we expect a notification
timeline.schedule_callback((60 * 60 * 24) + 1, lambda: None)
# Run the function with the timeline's time and sleep
long_running_func(_sleep=timeline.sleep, _time=timeline.time)
print("Test passed without waiting 24 real hours!")
Notice how we never truly wait 24 hours in real-time. The virtual clock leaps to the future instantly.
Simulations often need to “hurry up” in test or demo mode. Enter time factor:
def simulate(duration):
print("Simulation started.")
time.sleep(duration)
print("Simulation ended.")
from flux import Timeline, current_timeline
def test_simulation_speed():
# Use the global timeline for convenience
current_timeline.set(Timeline())
current_timeline.set_time_factor(1000)
# If we call simulate(3600), it should take around 3.6s in real time
simulate(3600)
print("Sim finished quickly!")
Want to “ping” a sensor every 10 virtual seconds, three times in total?
def sensor_ping():
print("Sensor ping at virtual time")
timeline = Timeline()
timeline.set_time_factor(1) # Normal speed (just for illustration)
# Schedule pings
for i in range(3):
timeline.schedule_callback(timeline.time() + 10*(i+1), sensor_ping)
# Sleep long enough for all pings
timeline.sleep(40)
print("All sensor pings done!")
When time is unfrozen, your code typically runs a few milliseconds slower or faster than you expect. Flux can remove that uncertainty:
timeline = Timeline()
timeline.freeze()
start = timeline.time()
timeline.sleep(10)
end = timeline.time()
# Perfectly deterministic test
assert (end - start) == 10, "Time delta should be exactly 10!"
print("Freeze test successful!")
In a large project, you might want multiple modules to share the same timeline. The current_timeline
proxy is perfect for this. It allows multiple modules to share the same virtual clock without having to pass the Timeline
object around. Here’s the pattern:
# moduleA.py
try:
from flux import current_timeline as time
except ImportError:
import time # fallback in case flux is not installed
def do_something_after_a_while():
time.sleep(100)
print("Done!")
# test_moduleA.py
from flux import Timeline, current_timeline
import moduleA
tl = Timeline()
tl.freeze() # Instantly advance time
current_timeline.set(tl)
moduleA.do_something_after_a_while()
# "Done!" will print immediately
The true power of Flux shines when testing complex systems where the outcome isn’t predictable by a simple formula. Imagine you've built a sophisticated stock prediction algorithm. You need to test how it performs over five years of simulated market data, a process that would be impossible to run in real-time. With Flux, you can validate its long-term behavior in minutes.
import time
import random
def run_prediction_algo(market_data, duration_days):
"""
A complex function that simulates running a prediction algorithm.
The logic here would be your proprietary model.
"""
print("Starting 5-year market prediction simulation...")
for day in range(duration_days):
# Simulate complex daily processing
# e.g., fetching data, running models, making trades
_ = [random.random() ** 2 for _ in range(1000)]
time.sleep(86400) # Advance one virtual day
print("Simulation complete.")
return "final_portfolio_value"
# --- Test File ---
from flux import Timeline, current_timeline
def test_five_year_simulation():
# Set up a global timeline that is frozen to run instantly
tl = Timeline()
tl.freeze()
current_timeline.set(tl)
five_years_in_days = 365 * 5
# Run the five-year simulation. Since the timeline is frozen,
# the 1825 calls to sleep(86400) happen instantly.
result = run_prediction_algo(
market_data="mock_data_source",
duration_days=five_years_in_days
)
# The entire 5-year test completes in a fraction of a real second
print(f"Test finished. Result from 5-year simulation: {result}")
assert result == "final_portfolio_value"
test_five_year_simulation()
How do you test a function that's supposed to run on a complex schedule, like "every second Friday of the month"? Writing a test that waits for real Fridays to pass is not an option. This is a perfect use case for Flux, pairing it with Python's `datetime` module to check the logic of the scheduler.
import datetime
import time
# --- The Function to Test ---
def cleanup_job_runner(get_time):
"""
A runner that executes a cleanup job, but only on the 2nd Friday of any month.
"""
last_run_day = -1
jobs_fired = 0
# Run for a simulated year
for _ in range(365):
now = datetime.datetime.fromtimestamp(get_time())
# Prevent running multiple times on the same day
if now.day == last_run_day:
time.sleep(86400) # Move to the next day
continue
# Logic: Is it Friday? And is it in the second week of the month?
is_friday = (now.weekday() == 4)
is_second_week = (now.day > 7 and now.day <= 14)
if is_friday and is_second_week:
print(f"Job fired on {now.strftime('%Y-%m-%d')}, the 2nd Friday.")
jobs_fired += 1
last_run_day = now.day
time.sleep(86400) # Move to the next virtual day
return jobs_fired
# --- The Test File ---
from flux import Timeline, current_timeline
def test_cleanup_job_fires_correctly_over_one_year():
# Start on a known date: Jan 1, 2024 (a Monday)
tl = Timeline(start_time=1704067200)
tl.freeze()
current_timeline.set(tl)
# Run the scheduler function over a virtual year
fire_count = cleanup_job_runner(get_time=current_timeline.time)
print(f"Test complete. The job fired {fire_count} times in a virtual year.")
# There are 12 months, so the job should fire 12 times in a year.
assert fire_count == 12
test_cleanup_job_fires_correctly_over_one_year()
As a data engineer, one of the most persistent challenges is managing time across distributed systems and diverse data sources. While Flux brilliantly solves the problem of testing time-dependent logic within an application, the real world often introduces a far more chaotic variable: timezones.
Different databases, APIs, and services often have their own idea of what 'now' is. Cloud-based providers are notorious for this, frequently defaulting to regional timestamps (like PST or EST) depending on where a server is located. This can lead to maddening bugs where data seems to arrive out of order or disappears entirely, simply because timestamps aren't being compared on a level playing field.
This is why it is an iron-clad best practice to standardize all time data to Coordinated Universal Time (UTC). By converting every timestamp to UTC as early as possible in your data pipeline, you create a single source of truth. This practice eliminates ambiguity and ensures that when you compare timestamps from a database in Virginia and a log file from Singapore, you're comparing apples to apples. While Flux helps you control time inside your tests, a strict UTC-first policy will help you tame it in your production systems.
Flux transforms the way you handle time-based tests and simulations. By mocking time progression, you sidestep the messy patching of Python’s time
module in scattered places. You can freeze time, fast-forward through days in a blink, or schedule future events with simple, expressive code.
Timeline
object does everything you need.With Flux, you’ll find testing long or complex time-based scenarios becomes almost trivial—no more waiting overnight for tests to pass, and no more dealing with frustrating “close enough” validations in your assertions.
So the next time you have a feature that waits hours or days to do something, consider Flux. Your future self (and your test suite) will thank you.