Skip to content

Commit 6453fee

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 6453fee

File tree

1 file changed

+71
-43
lines changed

1 file changed

+71
-43
lines changed

tests/extmod/time_res.py

Lines changed: 71 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,62 +7,90 @@
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+
try:
22+
t1 = time_func()
23+
time.sleep_ms(sleep_ms)
24+
t2 = time_func()
25+
except AttributeError:
26+
# Function exists but calls unavailable time functions (e.g., gmtime_time)
27+
return None
28+
29+
# For tuple return values (gmtime, localtime), compare as tuples
30+
if isinstance(t1, tuple) and isinstance(t2, tuple):
31+
# Should have changed (checking resolution)
32+
return t2 != t1
33+
# For numeric return values (time, ticks_*)
34+
else:
35+
# Use appropriate diff function for ticks
36+
if func_name.startswith("ticks_"):
37+
diff = time.ticks_diff(t2, t1)
38+
if func_name == "ticks_ms":
39+
# Expect 80%-200% of sleep time (tolerance for overhead and loaded CI)
40+
expected = sleep_ms
41+
return (expected * 0.8) <= diff <= (expected * 2.0)
42+
elif func_name == "ticks_us":
43+
# Expect 80%-200% of sleep time in microseconds
44+
expected = sleep_ms * 1000
45+
return (expected * 0.8) <= diff <= (expected * 2.0)
46+
elif func_name == "ticks_ns":
47+
# Expect 80%-200% of sleep time in nanoseconds
48+
expected = sleep_ms * 1000000
49+
return (expected * 0.8) <= diff <= (expected * 2.0)
50+
elif func_name == "ticks_cpu":
51+
# ticks_cpu may return 0 on some ports, just check it advanced
52+
return diff > 0 or t2 == 0
53+
else:
54+
# For time() and other float/int returns, check both bounds
55+
# 2x tolerance for overhead and loaded CI systems
56+
min_expected = min_advance_s
57+
max_expected = (sleep_ms / 1000.0) * 2.0
58+
actual_diff = t2 - t1
59+
return min_expected <= actual_diff <= max_expected
60+
61+
1062
def gmtime_time():
63+
# May raise AttributeError if gmtime not available
1164
return time.gmtime(time.time())
1265

1366

1467
def localtime_time():
68+
# May raise AttributeError if localtime not available
1569
return time.localtime(time.time())
1670

1771

1872
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),
73+
# Test configuration: (function name, minimum advance in seconds, sleep time in ms)
74+
TEST_CONFIG = (
75+
("time", 1, 1200),
76+
("gmtime", 0, 1200), # gmtime returns tuple, just check it changes
77+
("localtime", 0, 1200),
78+
("gmtime_time", 0, 1200),
79+
("localtime_time", 0, 1200),
80+
("ticks_ms", 0, 150), # Test millisecond resolution
81+
("ticks_us", 0, 150), # Test microsecond resolution
82+
("ticks_ns", 0, 150), # Test nanosecond resolution
83+
("ticks_cpu", 0, 150),
3184
)
3285

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:
86+
for func_name, min_advance, sleep_ms in TEST_CONFIG:
5487
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)
88+
result = test_monotonic_advance(func_name, min_advance, sleep_ms)
89+
if result is None:
90+
# Function not available, skip silently
6091
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-
)
92+
elif not result:
93+
print("%s() did not advance as expected" % func_name)
6694

6795

6896
test()

0 commit comments

Comments
 (0)