55import wpilib
66from wpilib .simulation ._simulation import _resetWpilibSimulationData
77
8+ from pathlib import Path
9+
10+ import gc
11+
12+ import weakref
13+
14+ import hal
15+ import hal .simulation
16+ import wpilib .shuffleboard
17+ from wpilib .simulation import DriverStationSim , pauseTiming , restartTiming
18+ import wpilib .simulation
19+ from pyfrc .test_support .controller import TestController
20+ from pyfrc .physics .core import PhysicsInterface
21+
22+ try :
23+ import commands2
24+ except ImportError :
25+ commands2 = None
26+
27+
828
929@pytest .fixture
1030def cfg_logging (caplog ):
@@ -29,3 +49,127 @@ def nt(cfg_logging, wpilib_state):
2949 finally :
3050 instance .stopLocal ()
3151 instance ._reset ()
52+
53+ @pytest .fixture (scope = "class" , autouse = True )
54+ def physics_and_decorated_robot_class (myrobot_class , robots_sim_enable_physics )-> tuple :
55+ # attach physics
56+
57+ robotClass = myrobot_class
58+ physicsInterface = None
59+ if robots_sim_enable_physics :
60+ physicsInterface , robotClass = PhysicsInterface ._create_and_attach (
61+ myrobot_class ,
62+ Path (__file__ ).parent ,
63+ )
64+
65+ if physicsInterface :
66+ physicsInterface .log_init_errors = False
67+
68+ # Tests need to know when robotInit is called, so override the robot
69+ # to do that
70+ class TestRobot (robotClass ):
71+ def robotInit (self ):
72+ try :
73+ super ().robotInit ()
74+ finally :
75+ self .__robotInitialized ()
76+
77+
78+ TestRobot .__name__ = robotClass .__name__
79+ TestRobot .__module__ = robotClass .__module__
80+ TestRobot .__qualname__ = robotClass .__qualname__
81+
82+ return (physicsInterface , TestRobot )
83+
84+ @pytest .fixture (scope = "function" , autouse = True )
85+ def robot_with_sim_setup_teardown (physics_and_decorated_robot_class ):
86+ """
87+ Your robot instance
88+
89+ .. note:: RobotPy/WPILib testing infrastructure is really sensitive
90+ to ensuring that things get cleaned up properly. Make sure
91+ that you don't store references to your robot or other
92+ WPILib objects in a global or static context.
93+ """
94+
95+ #
96+ # This function needs to do the same things that RobotBase.main does
97+ # plus some extra things needed for testing
98+ #
99+ # Previously this was separate from robot fixture, but we need to
100+ # ensure that the robot cleanup happens deterministically relative to
101+ # when handle cleanup/etc happens, otherwise unnecessary HAL errors will
102+ # bubble up to the user
103+ #
104+
105+ nt_inst = ntcore .NetworkTableInstance .getDefault ()
106+ nt_inst .startLocal ()
107+
108+ pauseTiming ()
109+ restartTiming ()
110+
111+ wpilib .DriverStation .silenceJoystickConnectionWarning (True )
112+ DriverStationSim .setAutonomous (False )
113+ DriverStationSim .setEnabled (False )
114+ DriverStationSim .notifyNewData ()
115+
116+ robot = physics_and_decorated_robot_class [1 ]()
117+
118+ # Tests only get a proxy to ensure cleanup is more reliable
119+ yield weakref .proxy (robot )
120+
121+ # If running in separate processes, no need to do cleanup
122+ #if ISOLATED:
123+ # # .. and funny enough, in isolated mode we *don't* want the
124+ # # robot to be cleaned up, as that can deadlock
125+ # self._saved_robot = robot
126+ # return
127+
128+ # reset engine to ensure it gets cleaned up too
129+ # -> might be holding wpilib objects, or the robot
130+ if physics_and_decorated_robot_class [0 ]:
131+ physics_and_decorated_robot_class [0 ].engine = None
132+
133+ # HACK: avoid motor safety deadlock
134+ wpilib .simulation ._simulation ._resetMotorSafety ()
135+
136+ del robot
137+
138+ if commands2 is not None :
139+ commands2 .CommandScheduler .resetInstance ()
140+
141+ # Double-check all objects are destroyed so that HAL handles are released
142+ gc .collect ()
143+
144+ # shutdown networktables before other kinds of global cleanup
145+ # -> some reset functions will re-register listeners, so it's important
146+ # to do this before so that the listeners are active on the current
147+ # NetworkTables instance
148+ nt_inst .stopLocal ()
149+ nt_inst ._reset ()
150+
151+ # Cleanup WPILib globals
152+ # -> preferences, SmartDashboard, Shuffleboard, LiveWindow, MotorSafety
153+ wpilib .simulation ._simulation ._resetWpilibSimulationData ()
154+ wpilib ._wpilib ._clearSmartDashboardData ()
155+ wpilib .shuffleboard ._shuffleboard ._clearShuffleboardData ()
156+
157+ # Cancel all periodic callbacks
158+ hal .simulation .cancelAllSimPeriodicCallbacks ()
159+
160+ # Reset the HAL handles
161+ hal .simulation .resetGlobalHandles ()
162+
163+ # Reset the HAL data
164+ hal .simulation .resetAllSimData ()
165+
166+ # Don't call HAL shutdown! This is only used to cleanup HAL extensions,
167+ # and functions will only be called the first time (unless re-registered)
168+ # hal.shutdown()
169+
170+ @pytest .fixture (scope = "function" )
171+ def control (reraise , robot_with_sim_setup_teardown : wpilib .RobotBase ) -> TestController :
172+ """
173+ A pytest fixture that provides control over your robot_with_sim_setup_teardown
174+ """
175+ return TestController (reraise , robot_with_sim_setup_teardown )
0 commit comments