Skip to content

Commit 6347649

Browse files
committed
WIP
Signed-off-by: Mike Stitt <[email protected]>
1 parent 045aece commit 6347649

File tree

2 files changed

+147
-97
lines changed

2 files changed

+147
-97
lines changed

subprojects/robotpy-wpilib/tests/test_timedrobot.py

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import gc
1414
import pytest
1515
import threading
16+
import traceback
1617
import typing
1718
import weakref
1819

@@ -90,6 +91,7 @@ def __init__(self, reraise, robot: wpilib.RobotBase, expectFinished: bool) -> No
9091
self._robotStarted = False
9192
self._robotInitStarted = False
9293
self._robotFinished = False
94+
self._startCompetitionReturned = False
9395

9496
def _onRobotInitStarted(self) -> None:
9597
with self._cond:
@@ -101,14 +103,31 @@ def _robotThread(self, robot: TimedRobotPy) -> None:
101103
self._robotStarted = True
102104
self._cond.notify_all()
103105

104-
with self._reraise(catch=True):
106+
with self._reraise:
105107
assert robot is not None # shouldn't happen...
106108

107109
robot._TestRobot__robotInitStarted = self._onRobotInitStarted
108110

109111
try:
110112
robot.startCompetition()
111-
assert self._expectFinished == self._robotFinished
113+
print("after robot.startCompetition()",flush=True)
114+
self._startCompetitionReturned = True
115+
116+
except Exception as e:
117+
# Print the exception type and message
118+
print(f"Exception caught: {type(e).__name__}: {e}")
119+
120+
# Print the stack trace
121+
print("Stack trace:")
122+
traceback.print_exc()
123+
124+
# Alternatively, get the formatted traceback as a string:
125+
# formatted_traceback = traceback.format_exc()
126+
# print(formatted_traceback)
127+
128+
# Rethrow the exception to propagate it up the call stack
129+
raise
130+
112131
finally:
113132
del robot
114133

@@ -144,6 +163,7 @@ def runRobot(self) -> None:
144163
# in this block you should tell the sim to do sim things
145164
yield
146165
finally:
166+
print("Reached self._robotFinished", flush=True)
147167
self._robotFinished = True
148168
robot.endCompetition()
149169

@@ -173,6 +193,11 @@ def runRobot(self) -> None:
173193

174194
self._thread = None
175195

196+
#TODO the test harness captures the expected exceptions and does not raise them
197+
# so expected failures causes self._startCompetitionReturned even though they
198+
# would not outside of the test harness.
199+
#assert self._expectFinished == self._startCompetitionReturned
200+
176201
@property
177202
def robotIsAlive(self) -> bool:
178203
"""
@@ -221,6 +246,8 @@ def stepTiming(
221246
DriverStationSim.notifyNewData()
222247
stepTiming(0.001)
223248
if assert_alive and self._expectFinished:
249+
if not self.robotIsAlive:
250+
print("not self.robotIsAlive", flush=True)
224251
assert self.robotIsAlive
225252
tm += 0.001
226253

@@ -341,8 +368,32 @@ def startCompetition(self) -> None:
341368
super().startCompetition()
342369
except AssertionError:
343370
hasAssertionError = True
344-
print(f"TimedRobotPyExpectsException hasAssertionError={hasAssertionError}")
345-
assert hasAssertionError
371+
#raise
372+
373+
# TODO xyzzy The general concept is to change this so that exceptions are raised,
374+
# they propagate outside of this thread to calling thread and at the
375+
# calling thread confirm that we caught the exception.
376+
377+
except Exception as e:
378+
# Print the exception type and message
379+
print(f"Exception caught: {type(e).__name__}: {e}")
380+
381+
# Print the stack trace
382+
print("Stack trace:")
383+
traceback.print_exc()
384+
385+
# Alternatively, get the formatted traceback as a string:
386+
# formatted_traceback = traceback.format_exc()
387+
# print(formatted_traceback)
388+
389+
# Rethrow the exception to propagate it up the call stack
390+
raise
391+
392+
finally:
393+
print(f"TimedRobotPyExpectsException hasAssertionError={hasAssertionError}")
394+
assert hasAssertionError
395+
396+
346397

347398

348399
class TimedRobotPyDoNotExpectException(TimedRobotPy):
@@ -357,8 +408,25 @@ def startCompetition(self) -> None:
357408
super().startCompetition()
358409
except AssertionError:
359410
hasAssertionError = True
360-
print(f"TimedRobotPyExpectsException hasAssertionError={hasAssertionError}")
361-
assert not hasAssertionError
411+
#raise
412+
except Exception as e:
413+
# Print the exception type and message
414+
print(f"Exception caught: {type(e).__name__}: {e}")
415+
416+
# Print the stack trace
417+
print("Stack trace:")
418+
traceback.print_exc()
419+
420+
# Alternatively, get the formatted traceback as a string:
421+
# formatted_traceback = traceback.format_exc()
422+
# print(formatted_traceback)
423+
424+
# Rethrow the exception to propagate it up the call stack
425+
raise
426+
427+
finally:
428+
print(f"TimedRobotPyExpectsException hasAssertionError={hasAssertionError}")
429+
assert not hasAssertionError
362430

363431

364432
def getFPGATimeInSecondsAsStr():
@@ -614,6 +682,10 @@ def call_sequence_str(
614682
@pytest.mark.parametrize(
615683
"myRobotAddMethods, timedRobotExpectation, _expectFinished, _robotMode, _callSequenceStr",
616684
[
685+
# todo xyzzy, the general concept is to change this to a single class object
686+
# that has all of the configuration parameters, so that we only need to set the
687+
# ones that we care about for each test case.
688+
# todo add a description string of the test too.
617689
(
618690
MyRobotDefaultPass,
619691
TimedRobotPyDoNotExpectException,

subprojects/robotpy-wpilib/wpilib/timedrobotpy.py

Lines changed: 69 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import traceback
2+
from sys import argv
13
from typing import Any, Callable, Iterable, ClassVar
24
from heapq import heappush, heappop, _siftup
35
from hal import (
@@ -25,8 +27,6 @@
2527

2628
class _Callback:
2729

28-
__slots__ = "func", "_periodUs", "expirationUs"
29-
3030
def __init__(
3131
self,
3232
func: Callable[[], None],
@@ -100,25 +100,15 @@ def __repr__(self) -> str:
100100

101101
class _OrderedListSort:
102102

103-
__slots__ = "_data"
104-
105103
def __init__(self) -> None:
106104
self._data: list[Any] = []
107105

108106
def add(self, item: Any) -> None:
109107
self._data.append(item)
110108
self._data.sort()
111109

112-
def pop(self) -> Any:
113-
return self._data.pop()
114-
115-
def peek(
116-
self,
117-
) -> Any: # todo change to Any | None when we don't build with python 3.9
118-
if self._data:
119-
return self._data[0]
120-
else:
121-
return None
110+
def peek(self) -> Any:
111+
return self._data[0]
122112

123113
def reorderListAfterAChangeInTheFirstElement(self):
124114
self._data.sort()
@@ -138,24 +128,14 @@ def __repr__(self) -> str:
138128

139129
class _OrderedListMin:
140130

141-
__slots__ = "_data"
142-
143131
def __init__(self) -> None:
144132
self._data: list[Any] = []
145133

146134
def add(self, item: Any) -> None:
147135
self._data.append(item)
148136

149-
# def pop(self) -> Any:
150-
# return self._data.pop()
151-
152-
def peek(
153-
self,
154-
) -> Any: # todo change to Any | None when we don't build with python 3.9
155-
if self._data:
156-
return min(self._data)
157-
else:
158-
return None
137+
def peek(self) -> Any:
138+
return min(self._data)
159139

160140
def reorderListAfterAChangeInTheFirstElement(self):
161141
pass
@@ -175,24 +155,14 @@ def __repr__(self) -> str:
175155

176156
class _OrderedListHeapq:
177157

178-
__slots__ = "_data"
179-
180158
def __init__(self) -> None:
181159
self._data: list[Any] = []
182160

183161
def add(self, item: Any) -> None:
184162
heappush(self._data, item)
185163

186-
def pop(self) -> Any:
187-
return heappop(self._data)
188-
189-
def peek(
190-
self,
191-
) -> Any: # todo change to Any | None when we don't build with python 3.9
192-
if self._data:
193-
return self._data[0]
194-
else:
195-
return None
164+
def peek(self) -> Any:
165+
return self._data[0]
196166

197167
def reorderListAfterAChangeInTheFirstElement(self):
198168
_siftup(self._data, 0)
@@ -268,64 +238,72 @@ def startCompetition(self) -> None:
268238
observeUserProgramStarting()
269239

270240
# Loop forever, calling the appropriate mode-dependent function
271-
# (really not forever, there is a check for a break)
272-
while True:
273-
# We don't have to check there's an element in the queue first because
274-
# there's always at least one (the constructor adds one). It's re-enqueued
275-
# at the end of the loop.
276-
callback = self._callbacks.peek()
277-
278-
status = updateNotifierAlarm(self._notifier, callback.expirationUs)
279-
if status != 0:
280-
raise RuntimeError(f"updateNotifierAlarm() returned {status}")
281-
282-
self._loopStartTimeUs, status = waitForNotifierAlarm(self._notifier)
283-
284-
# The C++ code that this was based upon used the following line to establish
285-
# the loopStart time. Uncomment it and
286-
# the "self._loopStartTimeUs = startTimeUs" further below to emulate the
287-
# legacy behavior.
288-
# startTimeUs = _getFPGATime() # uncomment this for legacy behavior
289-
290-
if status != 0:
291-
raise RuntimeError(
292-
f"waitForNotifierAlarm() returned _loopStartTimeUs={self._loopStartTimeUs} status={status}"
293-
)
294-
295-
if self._loopStartTimeUs == 0:
296-
# when HAL_StopNotifier(self.notifier) is called the above waitForNotifierAlarm
297-
# will return a _loopStartTimeUs==0 and the API requires robots to stop any loops.
298-
# See the API for waitForNotifierAlarm
299-
break
300-
301-
# On a RoboRio 2, the following print statement results in values like:
302-
# print(f"expUs={callback.expirationUs} current={self._loopStartTimeUs}, legacy={startTimeUs}")
303-
# [2.27] expUs=3418017 current=3418078, legacy=3418152
304-
# [2.29] expUs=3438017 current=3438075, legacy=3438149
305-
# This indicates that there is about 60 microseconds of skid from
306-
# callback.expirationUs to self._loopStartTimeUs
307-
# and there is about 70 microseconds of skid from self._loopStartTimeUs to startTimeUs.
308-
# Consequently, this code uses "self._loopStartTimeUs, status = waitForNotifierAlarm"
309-
# to establish loopStartTime, rather than slowing down the code by adding an extra call to
310-
# "startTimeUs = _getFPGATime()".
311-
312-
# self._loopStartTimeUs = startTimeUs # Uncomment this line for legacy behavior.
313-
314-
self._runCallbackAtHeadOfListAndReschedule(callback)
315-
316-
# Process all other callbacks that are ready to run
317-
# Changing the comparison to be _getFPGATime() rather than
318-
# self._loopStartTimeUs would also be correct.
319-
while (
320-
callback := self._callbacks.peek()
321-
).expirationUs <= _getFPGATime():
322-
self._runCallbackAtHeadOfListAndReschedule(callback)
241+
# (really not forever, there is a check for a stop)
242+
while self._bodyOfMainLoop():
243+
pass
244+
print("Reached after while self._bodyOfMainLoop(): ", flush=True)
245+
323246
finally:
247+
print("Reached after finally: self._stopNotifier(): ", flush=True)
324248
# pytests hang on PC when we don't force a call to self._stopNotifier()
325249
self._stopNotifier()
326250

251+
def _bodyOfMainLoop(self) -> bool:
252+
keepGoing = True
253+
# We don't have to check there's an element in the queue first because
254+
# there's always at least one (the constructor adds one).
255+
callback = self._callbacks.peek()
256+
257+
status = updateNotifierAlarm(self._notifier, callback.expirationUs)
258+
if status != 0:
259+
raise RuntimeError(f"updateNotifierAlarm() returned {status}")
260+
261+
self._loopStartTimeUs, status = waitForNotifierAlarm(self._notifier)
262+
263+
# The C++ code that this was based upon used the following line to establish
264+
# the loopStart time. Uncomment it and
265+
# the "self._loopStartTimeUs = startTimeUs" further below to emulate the
266+
# legacy behavior.
267+
# startTimeUs = _getFPGATime() # uncomment this for legacy behavior
268+
269+
if status != 0:
270+
raise RuntimeError(
271+
f"waitForNotifierAlarm() returned _loopStartTimeUs={self._loopStartTimeUs} status={status}"
272+
)
273+
274+
if self._loopStartTimeUs == 0:
275+
# when HAL_StopNotifier(self.notifier) is called the above waitForNotifierAlarm
276+
# will return a _loopStartTimeUs==0 and the API requires robots to stop any loops.
277+
# See the API for waitForNotifierAlarm
278+
keepGoing = False
279+
return keepGoing
280+
281+
# On a RoboRio 2, the following print statement results in values like:
282+
# print(f"expUs={callback.expirationUs} current={self._loopStartTimeUs}, legacy={startTimeUs}")
283+
# [2.27] expUs=3418017 current=3418078, legacy=3418152
284+
# [2.29] expUs=3438017 current=3438075, legacy=3438149
285+
# This indicates that there is about 60 microseconds of skid from
286+
# callback.expirationUs to self._loopStartTimeUs
287+
# and there is about 70 microseconds of skid from self._loopStartTimeUs to startTimeUs.
288+
# Consequently, this code uses "self._loopStartTimeUs, status = waitForNotifierAlarm"
289+
# to establish loopStartTime, rather than slowing down the code by adding an extra call to
290+
# "startTimeUs = _getFPGATime()".
291+
292+
# self._loopStartTimeUs = startTimeUs # Uncomment this line for legacy behavior.
293+
294+
self._runCallbackAtHeadOfListAndReschedule(callback)
295+
296+
# Process all other callbacks that are ready to run
297+
# Changing the comparison to be _getFPGATime() rather than
298+
# self._loopStartTimeUs would also be correct.
299+
while (
300+
callback := self._callbacks.peek()
301+
).expirationUs <= _getFPGATime():
302+
self._runCallbackAtHeadOfListAndReschedule(callback)
303+
304+
return keepGoing
305+
327306
def _runCallbackAtHeadOfListAndReschedule(self, callback) -> None:
328-
# callback = self._callbacks.peek()
329307
# The callback.func() may have added more callbacks to self._callbacks,
330308
# but each is sorted by the _getFPGATime() at the moment it is
331309
# created, which is greater than self.expirationUs of this callback,

0 commit comments

Comments
 (0)