The runner.py module is the main test orchestrator in the Roboticks testing framework. It manages the complete test lifecycle: starting the device manager, deploying compositions, executing pytest, and reporting results.
Both cloud runners and self-hosted runners use the same test runner binary. The only difference is where it runs (EC2 vs your infrastructure). The execution logic is identical.
def setup(self) -> bool: """Set up test environment.""" # Verify device manager binary exists if not self.device_manager_bin.exists(): logger.error("Device manager binary not found") return False # Verify deployment path if not self.deployment_path.exists(): logger.warning("Deployment path does not exist") return True
Waits for device registration with the backend. During test execution, the device manager connects to Roboticks backend services just like any regular device - it authenticates, receives certificates, and establishes MQTT connections.
Copy
def verify_registration(self, timeout: int = 30) -> bool: """Wait for device registration.""" if self.device_manager.wait_for_registration(timeout): self.device_dsn = self.device_manager.get_dsn() logger.info(f"Registered with DSN: {self.device_dsn}") return True return False
Test Devices vs Production DevicesTest devices register with a special TEST_DEVICE=true flag. This means:
They follow the same authentication flow as production devices
They appear in your dashboard for monitoring during tests
They are automatically marked as test devices
They do not consume your purchased Device units
This allows you to run unlimited tests without affecting your billing.
def cleanup(self) -> None: """Clean up test environment.""" if self.device_manager: # Stop active session session_id = self.device_manager.get_active_session_id() if session_id: self.device_manager.stop_active_session() # Stop device manager if self.device_manager.is_running(): self.device_manager.stop(timeout_seconds=5)
# Via environment variableos.environ["ROBOTICKS_COMPOSITION"] = "HelloWorldComposition"os.environ["ROBOTICKS_COMPOSITION_MODE"] = "dev"# This will only start HelloWorldComposition in dev mode
The main() function is the entry point called by the self-hosted runner:
Copy
def main() -> None: """Main entry point for the test runner.""" # Set up logging roboticks_root = Path(os.environ.get("ROBOTICKS_ROOT", "/opt/roboticks")) log_file = roboticks_root / "test-output" / "test-run.log" setup_logging(log_file) # Create and run runner = RoboticksTestRunner() exit_code = runner.run() sys.exit(exit_code)if __name__ == "__main__": main()
The runner uses os._exit() in the finally block to ensure clean termination, especially when Python bindings are involved. This prevents hanging on background threads.
The runner handles errors at each stage and provides detailed logging:
Copy
def run(self) -> int: """Run the complete test suite.""" try: if not self.setup(): return 1 if not self.start_device_manager(): return 1 if not self.verify_registration(): return 1 if not self.verify_session_created(): return 1 if not self.run_deployment(): return 1 if not self.run_tests(): return 1 return 0 except Exception as e: logger.error(f"Test run failed: {e}", exc_info=True) return 1 finally: self.cleanup(print_dm_logs=True)