From 95b496d4621cd0e086d467eac20137a4f6b10877 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Thu, 5 Feb 2026 00:49:02 -0500 Subject: [PATCH] Make initialization timeout configurable - Fixes #232 - Fixes #253 --- pyfrc/mains/cli_test.py | 38 ++++++++++++++++++- pyfrc/test_support/controller.py | 10 ++++- .../pytest_isolated_tests_plugin.py | 14 ++++++- pyfrc/test_support/pytest_plugin.py | 5 ++- tests/test_pytest_plugins.py | 4 +- 5 files changed, 62 insertions(+), 9 deletions(-) diff --git a/pyfrc/mains/cli_test.py b/pyfrc/mains/cli_test.py index 040f6c9..e984f24 100644 --- a/pyfrc/mains/cli_test.py +++ b/pyfrc/mains/cli_test.py @@ -75,6 +75,12 @@ def __init__(self, parser=None): default=-1, help="Maximum isolated robot processes (default: max CPUs - 1)", ) + parser.add_argument( + "--init-timeout", + type=float, + default=None, + help="Seconds to wait for robot to start (can be set in `tool.robotpy.pyfrc.init_timeout` also)", + ) def run( self, @@ -87,6 +93,7 @@ def run( verbose: bool, pytest_args: typing.List[str], jobs: int, + init_timeout: typing.Optional[float], ): if isolated is None: pyproject_path = project_path / "pyproject.toml" @@ -106,6 +113,26 @@ def run( isolated = v + try: + v = d["tool"]["robotpy"]["pyfrc"]["init_timeout"] + except KeyError: + pass + else: + if not isinstance(v, (int, float)): + raise ValueError( + f"tool.robotpy.pyfrc.init_timeout must be a number (got {v})" + ) + elif not (v > 0): + raise ValueError( + f"tool.robotpy.pyfrc.init_timeout must be a positive number (got {v})" + ) + + if init_timeout is None: + init_timeout = float(v) + + if init_timeout is None: + init_timeout = 2.0 + if isolated is None: isolated = True @@ -120,6 +147,7 @@ def run( verbose, pytest_args, jobs, + init_timeout, ) except _TryAgain: return self._run_test( @@ -132,6 +160,7 @@ def run( verbose, pytest_args, jobs, + init_timeout, ) def _run_test( @@ -145,6 +174,7 @@ def _run_test( verbose: bool, pytest_args: typing.List[str], jobs: int, + init_timeout: float, ): # find test directory, change current directory so pytest can find the tests # -> assume that tests reside in tests or ../tests @@ -180,14 +210,18 @@ def _run_test( pytest_args, plugins=[ pytest_isolated_tests_plugin.IsolatedTestsPlugin( - robot_class, main_file, builtin, verbose, jobs + robot_class, main_file, builtin, verbose, jobs, init_timeout ) ], ) else: retv = pytest.main( pytest_args, - plugins=[pytest_plugin.PyFrcPlugin(robot_class, main_file, False)], + plugins=[ + pytest_plugin.PyFrcPlugin( + robot_class, main_file, False, init_timeout + ) + ], ) finally: os.chdir(curdir) diff --git a/pyfrc/test_support/controller.py b/pyfrc/test_support/controller.py index 85e6249..3a662c4 100644 --- a/pyfrc/test_support/controller.py +++ b/pyfrc/test_support/controller.py @@ -12,11 +12,12 @@ class TestController: Use this object to control the robot's state during tests """ - def __init__(self, reraise, robot: wpilib.RobotBase): + def __init__(self, reraise, robot: wpilib.RobotBase, init_timeout: float = 2.0): self._reraise = reraise self._thread: typing.Optional[threading.Thread] = None self._robot = robot + self._init_timeout = init_timeout self._cond = threading.Condition() self._robot_started = False @@ -72,7 +73,12 @@ def run_robot(self): # If your robotInit is taking more than 2 seconds in simulation, you're # probably doing something wrong... but if not, please report a bug! - assert self._cond.wait_for(lambda: self._robot_initialized, timeout=2) + # + # To avoid this altogether, `--init-timeout` can be passed to `robotpy test` + # or you can add `tools.robotpy.pyfrc.init_timeout` instead + assert self._cond.wait_for( + lambda: self._robot_initialized, timeout=self._init_timeout + ) try: # in this block you should tell the sim to do sim things diff --git a/pyfrc/test_support/pytest_isolated_tests_plugin.py b/pyfrc/test_support/pytest_isolated_tests_plugin.py index f1d69c0..3cceb85 100644 --- a/pyfrc/test_support/pytest_isolated_tests_plugin.py +++ b/pyfrc/test_support/pytest_isolated_tests_plugin.py @@ -128,7 +128,14 @@ def pytest_runtest_logreport(self, report: pytest.TestReport): def _run_test( - item_nodeid, config_args, robot_class, robot_file, verbose, pipe, root_path + item_nodeid, + config_args, + robot_class, + robot_file, + verbose, + pipe, + root_path, + init_timeout, ): """This function runs in a subprocess""" logging.root.addHandler(logging.NullHandler()) @@ -143,7 +150,7 @@ def _run_test( # keep the plugins around because it has a reference to the robot # and we don't want it to die and deadlock - plugin = PyFrcPlugin(robot_class, robot_file, True) + plugin = PyFrcPlugin(robot_class, robot_file, True, init_timeout) worker_plugin = WorkerPlugin(pipe) ec = pytest.main( @@ -195,11 +202,13 @@ def __init__( builtin_tests: bool, verbose: bool, parallelism: int, + init_timeout: float, ): self._robot_class = robot_class self._robot_file = robot_file self._builtin_tests = builtin_tests self._verbose = verbose + self._init_timeout = init_timeout if parallelism < 1: try: @@ -287,6 +296,7 @@ def _start_isolated_test(self, item: pytest.Function) -> IsolatedTestJob: self._verbose, cconn, self._config.rootpath, + self._init_timeout, ), ) process.start() diff --git a/pyfrc/test_support/pytest_plugin.py b/pyfrc/test_support/pytest_plugin.py index b83f439..77564b9 100644 --- a/pyfrc/test_support/pytest_plugin.py +++ b/pyfrc/test_support/pytest_plugin.py @@ -36,6 +36,7 @@ def __init__( robot_class: Type[wpilib.RobotBase], robot_file: pathlib.Path, isolated: bool, + init_timeout: float, ): self.isolated = isolated @@ -61,6 +62,8 @@ def robotInit(self): self._robot_file = robot_file self._robot_class = TestRobot + self._init_timeout = init_timeout + self._physics = physics if physics: @@ -164,7 +167,7 @@ def control(self, reraise, robot: wpilib.RobotBase) -> TestController: """ A pytest fixture that provides control over your robot """ - return TestController(reraise, robot) + return TestController(reraise, robot, self._init_timeout) @pytest.fixture() def robot_file(self) -> pathlib.Path: diff --git a/tests/test_pytest_plugins.py b/tests/test_pytest_plugins.py index e5e84d9..b90a42a 100644 --- a/tests/test_pytest_plugins.py +++ b/tests/test_pytest_plugins.py @@ -75,7 +75,7 @@ def _configure_pyfrc_plugin(pytester, robot_class="DummyRobot"): def pytest_configure(config): robot_file = pathlib.Path(__file__).resolve() - config.pluginmanager.register(PyFrcPlugin({robot_class}, robot_file, False)) + config.pluginmanager.register(PyFrcPlugin({robot_class}, robot_file, False, 2.0)) """) @@ -92,7 +92,7 @@ def pytest_configure(config): return robot_file = pathlib.Path(__file__).resolve() config.pluginmanager.register( - IsolatedTestsPlugin({robot_class}, robot_file, False, False, {parallelism}) + IsolatedTestsPlugin({robot_class}, robot_file, False, False, {parallelism}, 2.0) ) """)