Skip to content

Commit 14b63f8

Browse files
committed
tests/extmod: Make test time_res.py more deterministic.
Replace sample-counting approach with direct monotonicity and resolution tests. The previous method mixed ticks_ms() and RTC clock sources which could drift, causing non-deterministic results across platforms. New approach directly tests that time functions advance at their documented rates. Signed-off-by: Andrew Leech <[email protected]>
1 parent 4efc5e1 commit 14b63f8

File tree

1 file changed

+65
-43
lines changed

1 file changed

+65
-43
lines changed

tests/extmod/time_res.py

Lines changed: 65 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,54 @@
77
raise SystemExit
88

99

10+
def test_monotonic_advance(func_name, min_advance_s, sleep_ms):
11+
"""Test that a time function advances within expected bounds."""
12+
try:
13+
if func_name.endswith("_time"):
14+
# Helper functions defined below
15+
time_func = globals()[func_name]
16+
else:
17+
time_func = getattr(time, func_name)
18+
except AttributeError:
19+
return None # Function not available
20+
21+
t1 = time_func()
22+
time.sleep_ms(sleep_ms)
23+
t2 = time_func()
24+
25+
# For tuple return values (gmtime, localtime), compare as tuples
26+
if isinstance(t1, tuple) and isinstance(t2, tuple):
27+
# Should have changed (checking resolution)
28+
return t2 != t1
29+
# For numeric return values (time, ticks_*)
30+
else:
31+
# Use appropriate diff function for ticks
32+
if func_name.startswith("ticks_"):
33+
diff = time.ticks_diff(t2, t1)
34+
if func_name == "ticks_ms":
35+
# Expect 80%-200% of sleep time (tolerance for overhead and loaded CI)
36+
expected = sleep_ms
37+
return (expected * 0.8) <= diff <= (expected * 2.0)
38+
elif func_name == "ticks_us":
39+
# Expect 80%-200% of sleep time in microseconds
40+
expected = sleep_ms * 1000
41+
return (expected * 0.8) <= diff <= (expected * 2.0)
42+
elif func_name == "ticks_ns":
43+
# Expect 80%-200% of sleep time in nanoseconds
44+
expected = sleep_ms * 1000000
45+
return (expected * 0.8) <= diff <= (expected * 2.0)
46+
elif func_name == "ticks_cpu":
47+
# ticks_cpu may return 0 on some ports, just check it advanced
48+
return diff > 0 or t2 == 0
49+
else:
50+
# For time() and other float/int returns, check both bounds
51+
# 2x tolerance for overhead and loaded CI systems
52+
min_expected = min_advance_s
53+
max_expected = (sleep_ms / 1000.0) * 2.0
54+
actual_diff = t2 - t1
55+
return min_expected <= actual_diff <= max_expected
56+
57+
1058
def gmtime_time():
1159
return time.gmtime(time.time())
1260

@@ -16,53 +64,27 @@ def localtime_time():
1664

1765

1866
def test():
19-
TEST_TIME = 2500
20-
EXPECTED_MAP = (
21-
# (function name, min. number of results in 2.5 sec)
22-
("time", 3),
23-
("gmtime", 3),
24-
("localtime", 3),
25-
("gmtime_time", 3),
26-
("localtime_time", 3),
27-
("ticks_ms", 15),
28-
("ticks_us", 15),
29-
("ticks_ns", 15),
30-
("ticks_cpu", 15),
67+
# Test configuration: (function name, minimum advance in seconds, sleep time in ms)
68+
TEST_CONFIG = (
69+
("time", 1, 1200),
70+
("gmtime", 0, 1200), # gmtime returns tuple, just check it changes
71+
("localtime", 0, 1200),
72+
("gmtime_time", 0, 1200),
73+
("localtime_time", 0, 1200),
74+
("ticks_ms", 0, 150), # Test millisecond resolution
75+
("ticks_us", 0, 150), # Test microsecond resolution
76+
("ticks_ns", 0, 150), # Test nanosecond resolution
77+
("ticks_cpu", 0, 150),
3178
)
3279

33-
# call time functions
34-
results_map = {}
35-
end_time = time.ticks_ms() + TEST_TIME
36-
while time.ticks_diff(end_time, time.ticks_ms()) > 0:
37-
time.sleep_ms(100)
38-
for func_name, _ in EXPECTED_MAP:
39-
try:
40-
if func_name.endswith("_time"):
41-
time_func = globals()[func_name]
42-
else:
43-
time_func = getattr(time, func_name)
44-
now = time_func() # may raise AttributeError
45-
except AttributeError:
46-
continue
47-
try:
48-
results_map[func_name].add(now)
49-
except KeyError:
50-
results_map[func_name] = {now}
51-
52-
# check results
53-
for func_name, min_len in EXPECTED_MAP:
80+
for func_name, min_advance, sleep_ms in TEST_CONFIG:
5481
print("Testing %s" % func_name)
55-
results = results_map.get(func_name)
56-
if results is None:
57-
pass
58-
elif func_name == "ticks_cpu" and results == {0}:
59-
# ticks_cpu() returns 0 on some ports (e.g. unix)
82+
result = test_monotonic_advance(func_name, min_advance, sleep_ms)
83+
if result is None:
84+
# Function not available, skip silently
6085
pass
61-
elif len(results) < min_len:
62-
print(
63-
"%s() returns %s result%s in %s ms, expecting >= %s"
64-
% (func_name, len(results), "s"[: len(results) != 1], TEST_TIME, min_len)
65-
)
86+
elif not result:
87+
print("%s() did not advance as expected" % func_name)
6688

6789

6890
test()

0 commit comments

Comments
 (0)