def setUp(self, machine_path): """Set up fuzzer.""" self.loop = TimeTravelLoop() self.loop.set_exception_handler(self._exception_handler) self.clock = TestClock(self.loop) self.machine = TestMachineController( os.path.abspath(os.path.join( mpf.core.__path__[0], os.pardir)), machine_path, self.get_options(), self.machine_config_patches, self.machine_config_defaults, self.clock, dict(), True) try: self.loop.run_until_complete(self.machine.initialise()) except RuntimeError as e: try: self.machine.stop() # pylint: disable-msg=broad-except except Exception: pass if self._exception and "exception" in self._exception: raise self._exception['exception'] elif self._exception: raise Exception(self._exception, e) raise e self.machine.events.process_event_queue() self.advance_time_and_run(1) self.switch_list = sorted(self.machine.switches.keys()) for switch in self.machine.switches: self.machine.switch_controller.process_switch_obj(switch, 0, True)
def setUp(self): """Setup test.""" self._get_event_loop = asyncio.get_event_loop asyncio.get_event_loop = None self._get_event_loop2 = asyncio.events.get_event_loop events.get_event_loop = None # we want to reuse config_specs to speed tests up mpf.core.config_validator.ConfigValidator.unload_config_spec = ( MagicMock()) self._events = {} self._last_event_kwargs = {} self._exception = None # print(threading.active_count()) self.test_start_time = time.time() if self.unittest_verbosity() > 1: logging.basicConfig(level=logging.DEBUG, format='%(asctime)s : %(levelname)s : %(' 'name)s : %(message)s') else: # no logging by default logging.basicConfig(level=99) self.save_and_prepare_sys_path() # init machine machine_path = self.get_absolute_machine_path() self.loop = TimeTravelLoop() self.loop.set_exception_handler(self._exception_handler) self.clock = TestClock(self.loop) self._mock_loop() try: self.machine = TestMachineController( os.path.abspath(os.path.join(mpf.core.__path__[0], os.pardir)), machine_path, self.get_options(), self.machine_config_patches, self.machine_config_defaults, self.clock, self._get_mock_data(), self.get_enable_plugins(), self._early_machine_init) self._initialise_machine() # pylint: disable-msg=broad-except except Exception as e: try: self.machine.stop() except AttributeError: pass if self._exception and 'exception' in self._exception: raise self._exception['exception'] elif self._exception: raise Exception(self._exception, e) raise e
def setup(self, machine_path): """Set up fuzzer.""" self.loop = TimeTravelLoop() self.loop.set_exception_handler(self._exception_handler) self.clock = TestClock(self.loop) config_loader = UnitTestConfigLoader(machine_path, ["config.yaml"], self.machine_config_defaults, self.machine_config_patches, {}) config = config_loader.load_mpf_config() try: # remove virtual_platform_start_active_switches as it messes with out switch logic config.get_machine_config().pop( "virtual_platform_start_active_switches") except KeyError: pass self.machine = TestMachineController(self.get_options(), config, self.machine_config_patches, self.machine_config_defaults, self.clock, dict(), True) try: self.loop.run_until_complete(self.machine.initialise()) except RuntimeError as e: try: self.machine.stop() # pylint: disable-msg=broad-except except Exception: pass if self._exception and "exception" in self._exception: raise self._exception['exception'] elif self._exception: raise Exception(self._exception, e) raise e self.machine.events.process_event_queue() self.advance_time_and_run(1) self.switch_list = sorted(self.machine.switches.keys()) for switch in self.machine.switches: self.machine.switch_controller.process_switch_obj(switch, 0, True)
def setUp(self): # we want to reuse config_specs to speed tests up ConfigValidator.unload_config_spec = MagicMock() self._events = {} # print(threading.active_count()) self.test_start_time = time.time() if self.unittest_verbosity() > 1: logging.basicConfig(level=logging.DEBUG, format='%(asctime)s : %(levelname)s : %(' 'name)s : %(message)s') else: # no logging by default logging.basicConfig(level=99) self.save_and_prepare_sys_path() # init machine machine_path = os.path.abspath( os.path.join(mpfmc.__path__[0], os.pardir, 'mpfmc', self.getMachinePath())) try: self.loop = TimeTravelLoop() self.clock = TestClock(self.loop) # Note the 'True' for enabling plugins, change from base self.machine = TestMachineController( os.path.abspath(os.path.join(mpf.core.__path__[0], os.pardir)), machine_path, self.getOptions(), self.machine_config_patches, self.clock, {}, True) while not self.machine.test_init_complete: self.advance_time_and_run(0.01) self.machine.ball_controller.num_balls_known = 99 self.advance_time_and_run(300) except Exception as e: # todo temp until I can figure out how to stop the asset loader # thread automatically. try: self.machine.stop() except AttributeError: pass raise e # remove config patches self.machine_config_patches = dict() # use bcp mock self.machine_config_patches['bcp'] = \ {"connections": {"local_display": {"type": "mpf.tests.MpfTestCase.MockBcpClient"}}}
def setUp(self, machine_path): """Set up fuzzer.""" self.loop = TimeTravelLoop() self.loop.set_exception_handler(self._exception_handler) self.clock = TestClock(self.loop) self.machine = TestMachineController( os.path.abspath(os.path.join(mpf.core.__path__[0], os.pardir)), machine_path, self.getOptions(), self.machine_config_patches, self.machine_config_defaults, self.clock, dict(), True) self.loop.run_until_complete(self.machine.initialise()) self.machine.events.process_event_queue() self.advance_time_and_run(1) self.switch_list = sorted(self.machine.switches.keys()) for switch in self.machine.switches: self.machine.switch_controller.process_switch_obj(switch, 0, True)
class AflRunner(object): """AFL fuzzer.""" __slots__ = [ "loop", "clock", "machine", "debug", "machine_config_patches", "machine_config_defaults", "switch_list", "use_virtual", "_invalid_input", "_exception" ] def __init__(self, use_virtual, debug): """Initialize fuzzer.""" self.loop = None self.clock = None self.machine = None # type: TestMachineController self.debug = debug self.machine_config_patches = dict() self.machine_config_patches['mpf'] = dict() self.machine_config_patches['mpf']['default_platform_hz'] = 1 self.machine_config_patches['bcp'] = [] self.machine_config_defaults = {} self.switch_list = [] self.use_virtual = use_virtual self._invalid_input = False self._exception = None def _exception_handler(self, loop, context): try: loop.stop() except RuntimeError: pass self._exception = context def get_platform(self): """Return platform.""" if self.use_virtual: return "virtual" return "smart_virtual" @staticmethod def get_config_file(): """Return config file.""" return "config.yaml" def get_options(self): """Return option arrays.""" mpfconfig = os.path.abspath( os.path.join(mpf.core.__path__[0], os.pardir, 'mpfconfig.yaml')) return { 'force_platform': self.get_platform(), 'mpfconfigfile': mpfconfig, 'configfile': Util.string_to_event_list(self.get_config_file()), 'debug': False, 'bcp': False, 'no_load_cache': False, 'create_config_cache': True, 'text_ui': False, 'production': not self.debug, } def advance_time_and_run(self, delta=1.0): """Advance time and run.""" try: self.loop.run_until_complete( asyncio.sleep(delay=delta, loop=self.loop)) return except RuntimeError as e: if self._exception and "exception" in self._exception: raise self._exception['exception'] elif self._exception: raise Exception(self._exception, e) raise e def setup(self, machine_path): """Set up fuzzer.""" self.loop = TimeTravelLoop() self.loop.set_exception_handler(self._exception_handler) self.clock = TestClock(self.loop) config_loader = UnitTestConfigLoader(machine_path, ["config.yaml"], self.machine_config_defaults, self.machine_config_patches, {}) config = config_loader.load_mpf_config() try: # remove virtual_platform_start_active_switches as it messes with out switch logic config.get_machine_config().pop( "virtual_platform_start_active_switches") except KeyError: pass self.machine = TestMachineController(self.get_options(), config, self.machine_config_patches, self.machine_config_defaults, self.clock, dict(), True) try: self.loop.run_until_complete(self.machine.initialise()) except RuntimeError as e: try: self.machine.stop() # pylint: disable-msg=broad-except except Exception: pass if self._exception and "exception" in self._exception: raise self._exception['exception'] elif self._exception: raise Exception(self._exception, e) raise e self.machine.events.process_event_queue() self.advance_time_and_run(1) self.switch_list = sorted(self.machine.switches.keys()) for switch in self.machine.switches: self.machine.switch_controller.process_switch_obj(switch, 0, True) def add_balls(self): """Add balls.""" balls_to_add = 3 for device in sorted(self.machine.ball_devices.values()): if "trough" in device.tags: for switch in device.ball_count_handler.counter.config.get( 'ball_switches', []): self.machine.switch_controller.process_switch_obj( switch, 1, True) balls_to_add -= 1 if balls_to_add == 0: break if balls_to_add == 0: break if device.ball_count_handler.counter.config.get( 'entrance_switch'): self.machine.switch_controller.process_switch_obj( device.ball_count_handler.counter. config['entrance_switch'], 1, True) balls_to_add -= 1 if balls_to_add == 0: break # let balls settle self.advance_time_and_run(10) def start_game(self): """Start game.""" found_start_switch = False for switch in self.machine.switches: if "start" in switch.tags: found_start_switch = True self.machine.switch_controller.process_switch_obj( switch, 1, True) self.machine.switch_controller.process_switch_obj( switch, 0, True) if not found_start_switch: raise AssertionError( "Did not find any switch tagged with tag \"start\".") def _abort(self, **kwargs): """Abort fuzzer run.""" del kwargs self._invalid_input = True if not self.debug: os._exit(0) # NOQA def run(self, actions, find_logic_bugs): """Run fuzzer.""" if find_logic_bugs: self.machine.events.add_handler("balldevice_ball_missing", self._abort) self.machine.events.add_handler("found_new_ball", self._abort) self.machine.events.add_handler("mode_game_stopped", self._abort) for action in actions: if self._invalid_input: # bail out if we hit an invalid input. afl will notice this return if action & 0b10000000: if action == 0b11111111: if self.machine.game and \ self.machine.game.balls_in_play < self.machine.ball_controller.num_balls_known: self.machine.playfield.add_ball() self.machine.game.balls_in_play += 1 else: self._abort() else: ms = int(action & 0b01111111) ms *= ms self.advance_time_and_run(ms / 1000.0) else: switch = int(action & 0b01111111) if switch >= len(self.switch_list): self._abort() return switch_obj = self.machine.switches[self.switch_list[switch]] state = switch_obj.hw_state ^ 1 # print(switch_list[switch], state, switch_obj.hw_state) self.machine.switch_controller.process_switch_by_num( switch_obj.hw_switch.number, state, self.machine.default_platform) if find_logic_bugs: self.advance_time_and_run(60) if self._invalid_input: # might happen late return balls = 0 for playfield in self.machine.playfields: balls += playfield.balls if balls != self.machine.game.balls_in_play: print("Balls in play:", self.machine.game.balls_in_play) print("Playfields:") for playfield in self.machine.playfields: print(playfield.name, playfield.balls) print("Devices:") for device in self.machine.ball_devices: print(device.name, device.balls, device.available_balls) raise AssertionError( "Balls in play do not match balls on playfields.") def dump(self, wait, add_balls, start_game, actions): """Dump fuzzer input.""" if add_balls: balls_to_add = 3 for device in sorted(self.machine.ball_devices.values()): if "trough" in device.tags: for switch in device.ball_count_handler.counter.config.get( 'ball_switches', []): print("Enable switch {}".format(switch.name)) switch.hw_state = 1 balls_to_add -= 1 if balls_to_add == 0: break if balls_to_add == 0: break if device.ball_count_handler.counter.config.get( 'entrance_switch'): print("Enable switch {}".format( device.config['entrance_switch'].name)) device.config['entrance_switch'].switch.hw_state = 1 balls_to_add -= 1 if balls_to_add == 0: break print("Advance time 10s") if start_game: for switch in self.machine.switches: if "start" in switch.tags: print("Enable/disable switch {}".format(switch.name)) if wait > 0: print("Advance time {}s".format(wait)) for action in actions: if action & 0b10000000: if action == 0b11111111: print("Add ball into play") else: ms = int(action & 0b01111111) ms *= ms print("Advance time by {} ms".format(ms)) else: switch = int(action & 0b01111111) if switch >= len(self.switch_list): print("Invalid switch. Abort!") return switch_obj = self.machine.switches[self.switch_list[switch]] switch_obj.hw_state = state = switch_obj.hw_state ^ 1 # print(switch_list[switch], state, switch_obj.hw_state) print("Toggle switch {}. New state: {}".format( switch_obj.name, state))
def setUp(self): """Setup test.""" self._get_event_loop = asyncio.get_event_loop asyncio.get_event_loop = None self._get_event_loop2 = asyncio.events.get_event_loop events.get_event_loop = None # we want to reuse config_specs to speed tests up mpf.core.config_validator.ConfigValidator.unload_config_spec = ( MagicMock()) self._events = {} self._last_event_kwargs = {} self._exception = None # print(threading.active_count()) self.test_start_time = time.time() self.save_and_prepare_sys_path() # init machine machine_path = self.get_absolute_machine_path() self.loop = TimeTravelLoop() self.loop.set_exception_handler(self._exception_handler) self.clock = TestClock(self.loop) self._mock_loop() if self.unittest_verbosity() > 1: class LogRecordClock(logging.LogRecord): def __init__(self_inner, *args, **kwargs): self_inner.created_clock = self.clock.get_time() super().__init__(*args, **kwargs) logging.setLogRecordFactory(LogRecordClock) console_log = logging.StreamHandler() console_log.setLevel(logging.DEBUG) console_log.setFormatter(MpfUnitTestFormatter(fmt='%(asctime)s : %(levelname)s : %(' 'name)s : %(message)s')) logging.basicConfig(level=logging.DEBUG, handlers=[console_log]) else: # no logging by default logging.basicConfig(level=99) # load config config_loader = UnitTestConfigLoader(machine_path, [self._get_config_file()], self.machine_config_defaults, self.machine_config_patches, self.machine_spec_patches) config = config_loader.load_mpf_config() try: self.machine = TestMachineController( self.get_options(), config, self.machine_config_patches, self.machine_config_defaults, self.clock, self._get_mock_data(), self.get_enable_plugins()) self._early_machine_init(self.machine) self._initialise_machine() # pylint: disable-msg=broad-except except Exception as e: try: self.machine.stop() except AttributeError: pass if self._exception and 'exception' in self._exception: raise self._exception['exception'] elif self._exception: raise Exception(self._exception, e) raise e
class MpfTestCase(unittest.TestCase): """Primary TestCase class used for all MPF unit tests.""" def __init__(self, methodName='runTest'): self._get_event_loop = None self._get_event_loop2 = None LogMixin.unit_test = True super().__init__(methodName) self.machine = None # type: TestMachineController self.machine_config_patches = dict() self.machine_config_patches['mpf'] = dict() self.machine_config_patches['mpf']['default_platform_hz'] = 100 self.machine_config_patches['mpf']['plugins'] = list() self.machine_config_patches['bcp'] = [] self.machine_config_defaults = dict() self.machine_config_defaults['playfields'] = dict() self.machine_config_defaults['playfields']['playfield'] = dict() self.machine_config_defaults['playfields']['playfield']['tags'] = "default" self.machine_config_defaults['playfields']['playfield']['default_source_device'] = None self.machine_spec_patches = {} self._last_event_kwargs = {} self._events = {} self.expected_duration = 0.5 def start_mode(self, mode): """Start mode.""" self.assertIn(mode, self.machine.modes) self.assertModeNotRunning(mode) self.machine.modes[mode].start() self.machine_run() self.assertModeRunning(mode) def stop_mode(self, mode): """Stop mode.""" self.assertModeRunning(mode) self.machine.modes[mode].stop() self.machine_run() self.assertModeNotRunning(mode) def get_config_file(self): """Return a string name of the machine config file to use for the tests in this class. You should override this method in your own test class to point to the config file you need for your tests. Returns: A string name of the machine config file to use, complete with the ``.yaml`` file extension. For example: .. code:: def get_config_file(self): return 'my_config.yaml' """ return 'null.yaml' def get_machine_path(self): """Return a string name of the path to the machine folder to use for the tests in this class. You should override this method in your own test class to point to the machine folder root you need for your tests. Returns: A string name of the machine path to use For example: .. code:: def get_machine_path(self): return 'tests/machine_files/my_test/' Note that this path is relative to the MPF package root """ return 'tests/machine_files/null/' def get_absolute_machine_path(self): """Return absolute machine path.""" # check if there is a decorator config_directory = getattr(getattr(self, self._testMethodName), "config_directory", None) if not config_directory: config_directory = self.get_machine_path() # creates an absolute path based on machine_path return os.path.abspath(os.path.join( mpf.core.__path__[0], os.pardir, config_directory)) @staticmethod def get_abs_path(path): """Get absolute path relative to current directory.""" return os.path.join(os.path.abspath(os.curdir), path) def post_event(self, event_name, run_time=0): """Post an MPF event and optionally advance the time. Args: event_name: String name of the event to post run_time: How much time (in seconds) the test should advance after this event has been posted. For example, to post an event called "shot1_hit": .. code:: self.post_event('shot1_hit') To post an event called "tilt" and then advance the time 1.5 seconds: .. code:: self.post_event('tilt', 1.5) """ self.machine.events.post(event_name) self.advance_time_and_run(run_time) def post_event_with_params(self, event_name, **params): """Post an MPF event with kwarg parameters. Args: event_name: String name of the event to post **params: One or more kwarg key/value pairs to post with the event. For example, to post an event called "jackpot" with the parameters ``count=1`` and ``first_time=True``, you would use: .. code:: self.post_event('jackpot', count=1, first_time=True) """ self.machine.events.post(event_name, **params) self.machine_run() def post_relay_event_with_params(self, event_name, **params): """Post a relay event synchronously and return the result.""" future = self.machine.events.post_relay_async(event_name, **params) result = self.machine.clock.loop.run_until_complete(future) return result def assertPlaceholderEvaluates(self, expected, condition): """Assert that a placeholder evaluates to a certain value.""" result = self.machine.placeholder_manager.build_raw_template(condition).evaluate([], fail_on_missing_params=True) self.assertEqual(expected, result, "{} = {} != {}".format(condition, result, expected)) def assertNumBallsKnown(self, balls): """Assert that a certain number of balls are known in the machine.""" self.assertEqual(balls, self.machine.ball_controller.num_balls_known) def set_num_balls_known(self, balls): """Set the ball controller's ``num_balls_known`` attribute. This is needed for tests where you don't have any ball devices and other situations where you need the ball controller to think the machine has a certain amount of balls to run a test. Example: .. code:: self.set_num_balls_known(3) """ # in case the test does not have any ball devices self.machine.ball_controller.num_balls_known = balls def get_platform(self): """Force this test class to use a certain platform. Returns: String name of the platform this test class will use. If you don't include this method in your test class, the platform will be set to `virtual`. If you want to use the smart virtual platform, you would add the following to your test class: .. code:: def get_platform(self): return 'smart_virtual` """ return 'virtual' def get_use_bcp(self): """Control whether tests in this class should use BCP. Returns: True or False The default is False. To use BCP in your test class, add the following: .. code:: def get_use_bcp(self): return True """ return False def get_enable_plugins(self): """Control whether tests in this class should load MPF plugins. Returns: True or False The default is False. To load plugins in your test class, add the following: .. code:: def get_enable_plugins(self): return True """ return False def _get_config_file(self): """Return test decorator value or the return of get_config_file.""" config_file = getattr(getattr(self, self._testMethodName), "config_file", None) if config_file: return config_file else: return self.get_config_file() def get_options(self): """Return options for the machine controller.""" mpfconfig = os.path.abspath(os.path.join( mpf.core.__path__[0], os.pardir, 'mpfconfig.yaml')) return { 'force_platform': self.get_platform(), 'production': False, 'mpfconfigfile': mpfconfig, 'configfile': Util.string_to_event_list(self._get_config_file()), 'debug': True, 'bcp': self.get_use_bcp(), 'no_load_cache': False, 'create_config_cache': True, 'text_ui': False, } def advance_time_and_run(self, delta=1.0): """Advance the test clock and run anything that should run during that time. Args: delta: How much time to advance the test clock by (in seconds) This method will cause anything scheduled during the time to run, including things like delays, timers, etc. Advancing the clock will happen in multiple small steps if things are scheduled to happen during this advance. For example, you can advance the clock 10 seconds like this: .. code:: self.advance_time_and_run(10) If there is a delay callback that is scheduled to happen in 2 seconds, then this method will advance the clock 2 seconds, process that delay, and then advance the remaining 8 seconds. """ self.machine.log.info("Advancing time by %s", delta) try: self.loop.run_until_complete(asyncio.sleep(delay=delta, loop=self.loop)) return except RuntimeError as e: try: self.machine.stop() except Exception: pass if self._exception and "exception" in self._exception: exception = self._exception['exception'] self._exception = None raise exception elif self._exception: exception = self._exception['exception'] self._exception = None raise Exception(exception, e) raise e def machine_run(self): """Process any delays, timers, or anything else scheduled. Note this is the same as: .. code:: self.advance_time_and_run(0) """ self.advance_time_and_run(0) @staticmethod def unittest_verbosity(): """Return the verbosity setting of the currently running unittest program, or 0 if none is running. Returns: An integer value of the current verbosity setting. """ frame = inspect.currentframe() while frame: obj = frame.f_locals.get('self') if isinstance(obj, unittest.TestProgram) or isinstance(obj, unittest.TextTestRunner): return obj.verbosity frame = frame.f_back return 0 def save_and_prepare_sys_path(self): """Save sys path and prepare it for the test.""" # save path self._sys_path = copy.deepcopy(sys.path) mpf_path = os.path.abspath(os.path.join(mpf.core.__path__[0], os.pardir)) if mpf_path in sys.path: sys.path.remove(mpf_path) # make tests path independent. remove current dir absolue if os.curdir in sys.path: sys.path.remove(os.curdir) # make tests path independent. remove current dir relative if "" in sys.path: sys.path.remove("") def restore_sys_path(self): """Restore sys path after test.""" sys.path = self._sys_path def _get_mock_data(self): """Return data for MockDataMangager in test.""" return dict() def _mock_loop(self): pass def _early_machine_init(self, machine): pass def _exception_handler(self, loop, context): try: loop.stop() except RuntimeError: pass self._exception = context def setUp(self): """Setup test.""" self._get_event_loop = asyncio.get_event_loop asyncio.get_event_loop = None self._get_event_loop2 = asyncio.events.get_event_loop events.get_event_loop = None # we want to reuse config_specs to speed tests up mpf.core.config_validator.ConfigValidator.unload_config_spec = ( MagicMock()) self._events = {} self._last_event_kwargs = {} self._exception = None # print(threading.active_count()) self.test_start_time = time.time() self.save_and_prepare_sys_path() # init machine machine_path = self.get_absolute_machine_path() self.loop = TimeTravelLoop() self.loop.set_exception_handler(self._exception_handler) self.clock = TestClock(self.loop) self._mock_loop() if self.unittest_verbosity() > 1: class LogRecordClock(logging.LogRecord): def __init__(self_inner, *args, **kwargs): self_inner.created_clock = self.clock.get_time() super().__init__(*args, **kwargs) logging.setLogRecordFactory(LogRecordClock) console_log = logging.StreamHandler() console_log.setLevel(logging.DEBUG) console_log.setFormatter(MpfUnitTestFormatter(fmt='%(asctime)s : %(levelname)s : %(' 'name)s : %(message)s')) logging.basicConfig(level=logging.DEBUG, handlers=[console_log]) else: # no logging by default logging.basicConfig(level=99) # load config config_loader = UnitTestConfigLoader(machine_path, [self._get_config_file()], self.machine_config_defaults, self.machine_config_patches, self.machine_spec_patches) config = config_loader.load_mpf_config() try: self.machine = TestMachineController( self.get_options(), config, self.machine_config_patches, self.machine_config_defaults, self.clock, self._get_mock_data(), self.get_enable_plugins()) self._early_machine_init(self.machine) self._initialise_machine() # pylint: disable-msg=broad-except except Exception as e: try: self.machine.stop() except AttributeError: pass if self._exception and 'exception' in self._exception: raise self._exception['exception'] elif self._exception: raise Exception(self._exception, e) raise e def _initialise_machine(self): init = asyncio.ensure_future(self.machine.initialise(), loop=self.loop) self._wait_for_start(init, 20) self.machine.events.process_event_queue() self.advance_time_and_run(.001) def _wait_for_start(self, init, timeout): start = time.time() while not init.done() and not self._exception: self.loop._run_once() if time.time() > start + timeout: raise AssertionError("Start took more than {}s".format(timeout)) # trigger exception if there was one init.result() def _mock_event_handler(self, event_name, **kwargs): self._last_event_kwargs[event_name] = kwargs self._events[event_name] += 1 def mock_event(self, event_name): """Configure an event to be mocked. Args: event_name: String name of the event to mock. Mocking an event is an easy way to check if an event was called without configuring some kind of callback action in your tests. Note that an event must be mocked here *before* it's posted in order for :meth:`assertEventNotCalled` and :meth:`assertEventCalled` to work. Mocking an event will not "break" it. In other words, any other registered handlers for this event will also be called even if the event has been mocked. For example: .. code:: self.mock_event('my_event') self.assertEventNotCalled('my_event') # This will be True self.post_event('my_event') self.assertEventCalled('my_event') # This will also be True """ self._events[event_name] = 0 self._last_event_kwargs.pop(event_name, None) self.machine.events.remove_handler_by_event(event=event_name, handler=self._mock_event_handler) self.machine.events.add_handler(event=event_name, handler=self._mock_event_handler, event_name=event_name) def assertBallsOnPlayfield(self, balls, playfield="playfield"): """Assert that a certain number of ball is on a playfield.""" self.assertEqual(balls, self.machine.playfields[playfield].balls, "Playfield {} contains {} balls but should be {}".format( playfield, self.machine.playfields[playfield].balls, balls)) def assertAvailableBallsOnPlayfield(self, balls, playfield="playfield"): """Assert that a certain number of ball is available on a playfield.""" self.assertEqual(balls, self.machine.playfields[playfield].available_balls) def assertMachineVarEqual(self, value, machine_var): """Assert that a machine variable exists and has a certain value.""" self.assertTrue(self.machine.variables.is_machine_var(machine_var), "Machine Var {} does not exist.".format(machine_var)) self.assertEqual(value, self.machine.variables.get_machine_var(machine_var)) def assertPlayerVarEqual(self, value, player_var): """Assert that a player variable exists and has a certain value.""" self.assertIsNotNone(self.machine.game, "There is no game.") self.assertEqual(value, self.machine.game.player[player_var], "Value of player var {} is {} but should be {}". format(player_var, self.machine.game.player[player_var], value)) def assertSwitchState(self, name, state): """Assert that a switch exists and has a certain state.""" self.assertIn(name, self.machine.switches, "Switch {} does not exist.".format(name)) self.assertEqual(state, self.machine.switch_controller.is_active(self.machine.switches[name]), "Switch {} is in state {} != {}".format( name, self.machine.switch_controller.is_active(self.machine.switches[name]), state)) def assertLightChannel(self, light_name, brightness, channel="white"): """Assert that a light channel has a certain brightness.""" self.assertIn(light_name, self.machine.lights, "Light {} does not exist".format(light_name)) self.assertAlmostEqual(brightness / 255.0, self.machine.lights[light_name].hw_drivers[channel][0]. current_brightness) def assertNotLightChannel(self, light_name, brightness, channel="white"): """Assert that a light channel does not have a certain brightness.""" self.assertIn(light_name, self.machine.lights, "Light {} does not exist".format(light_name)) self.assertNotEqual(brightness, self.machine.lights[light_name].hw_drivers[channel][0]. current_brightness) def assertLightColor(self, light_name, color): """Assert that a light exists and shows one color.""" self.assertIn(light_name, self.machine.lights, "Light {} does not exist".format(light_name)) if isinstance(color, str) and color.lower() == 'on': color = self.machine.lights[light_name].config['default_on_color'] self.assertEqual(RGBColor(color), self.machine.lights[light_name].get_color(), "Light {}: {} != {}. Stack: {}".format( light_name, RGBColor(color).name, self.machine.lights[light_name].get_color().name, self.machine.lights[light_name].stack)) def assertNotLightColor(self, light_name, color): """Assert that a light exists and does not show a certain color.""" self.assertIn(light_name, self.machine.lights, "Light {} does not exist".format(light_name)) if isinstance(color, str) and color.lower() == 'on': color = self.machine.lights[light_name].config['default_on_color'] self.assertNotEqual(RGBColor(color), self.machine.lights[light_name].get_color(), "{} == {}".format(RGBColor(color).name, self.machine.lights[light_name].get_color().name)) def assertLightColors(self, light_name, color_list, secs=1, check_delta=.1): """Assert that a light exists and shows all colors in a list over a period.""" self.assertIn(light_name, self.machine.lights, "Light {} does not exist".format(light_name)) colors = list() # have to do it this weird way because `if 'on' in color_list:` doesn't # work since it tries to convert it to a color for color in color_list[:]: if isinstance(color, str) and color.lower() == 'on': color_list.remove('on') color_list.append(self.machine.lights[light_name].config['default_on_color']) break for x in range(int(secs / check_delta)): color = self.machine.lights[light_name].get_color() colors.append(color) self.advance_time_and_run(check_delta) for color in color_list: self.assertIn(RGBColor(color), colors) def assertLightOn(self, light_name): """Assert that a light exists and is on.""" self.assertIn(light_name, self.machine.lights, "Light {} does not exist".format(light_name)) self.assertEqual(255, self.machine.lights[ light_name].hw_driver.current_brightness) def assertLightOff(self, light_name): """Assert that a light exists and is off.""" self.assertIn(light_name, self.machine.lights, "Light {} does not exist".format(light_name)) self.assertEqual(0, self.machine.lights[light_name].hw_driver.current_brightness) def assertLightFlashing(self, light_name, color=None, secs=1, check_delta=.1): """Assert that a light exists and is flashing.""" self.assertIn(light_name, self.machine.lights, "Light {} does not exist".format(light_name)) brightness_values = list() if not color: color = [255, 255, 255] for _ in range(int(secs / check_delta)): brightness_values.append( self.machine.lights[light_name].get_color()) self.advance_time_and_run(check_delta) self.assertIn([0, 0, 0], brightness_values) self.assertIn(color, brightness_values) def assertModeRunning(self, mode_name): """Assert that a mode exists and is running.""" if mode_name not in self.machine.modes: raise AssertionError("Mode {} not known.".format(mode_name)) self.assertIn(self.machine.modes[mode_name], self.machine.mode_controller.active_modes, "Mode {} not running.".format(mode_name)) def assertModeNotRunning(self, mode_name): """Assert that a mode exists and is not running.""" if mode_name not in self.machine.modes: raise AssertionError("Mode {} not known.".format(mode_name)) self.assertNotIn(self.machine.modes[mode_name], self.machine.mode_controller.active_modes, "Mode {} running but should not.".format(mode_name)) def assertEventNotCalled(self, event_name): """Assert that event was not called. Args: event_name: String name of the event to check. Note that the event must be mocked via ``self.mock_event()`` first in order to use this method. """ if event_name not in self._events: raise AssertionError("Event {} not mocked.".format(event_name)) if self._events[event_name] != 0: raise AssertionError("Event {} was called {} times but was not expected to " "be called.".format(event_name, self._events[event_name])) def assertEventCalled(self, event_name, times=None): """Assert that event was called. Args: event_name: String name of the event to check. times: An optional value to confirm the number of times the event was called. Default of *None* means this method will pass as long as the event has been called at least once. If you want to reset the ``times`` count, you can mock the event again. Note that the event must be mocked via ``self.mock_event()`` first in order to use this method. For example: .. code:: self.mock_event('my_event') self.assertEventNotCalled('my_event') # This will pass self.post_event('my_event') self.assertEventCalled('my_event') # This will pass self.assertEventCalled('my_event', 1) # This will pass self.post_event('my_event') self.assertEventCalled('my_event') # This will pass self.assertEventCalled('my_event', 2) # This will pass """ if event_name not in self._events: raise AssertionError("Event {} not mocked.".format(event_name)) if self._events[event_name] == 0 and times != 0: if times is None: raise AssertionError("Event {} was not called.".format(event_name)) raise AssertionError("Event {} was not called but we expected {} calls.".format(event_name, times)) if times is not None and self._events[event_name] != times: raise AssertionError("Event {} was called {} times instead of {} times expected.".format( event_name, self._events[event_name], times)) def assertEventCalledWith(self, event_name, **kwargs): """Assert an event was called with certain kwargs. Args: event_name: String name of the event to check. **kwargs: Name/value parameters to check. For example: .. code:: self.mock_event('jackpot') self.post_event('jackpot', count=1, first_time=True) self.assertEventCalled('jackpot') # This will pass self.assertEventCalledWith('jackpot', count=1, first_time=True) # This will also pass self.assertEventCalledWith('jackpot', count=1, first_time=False) # This will fail """ self.assertEventCalled(event_name) self.assertEqual(kwargs, self._last_event_kwargs[event_name], "Args for {} differ.".format(event_name)) def assertColorAlmostEqual(self, color1, color2, delta=6): """Assert that two color are almost equal. Args: color1: The first color, as an RGBColor instance or 3-item iterable. color2: The second color, as an RGBColor instance or 3-item iterable. delta: How close the colors have to be. The deltas between red, green, and blue are added together and must be less or equal to this value for this assertion to succeed. """ if isinstance(color1, RGBColor) and isinstance(color2, RGBColor): difference = abs(color1.red - color2.red) +\ abs(color1.blue - color2.blue) +\ abs(color1.green - color2.green) else: difference = abs(color1[0] - color2[0]) +\ abs(color1[1] - color2[1]) +\ abs(color1[2] - color2[2]) self.assertLessEqual(difference, delta, "Colors do not match: " + str(color1) + " " + str(color2)) def reset_mock_events(self): """Reset all mocked events. This will reset the count of number of times called every mocked event is. """ for event in self._events.keys(): self._events[event] = 0 def hit_switch_and_run(self, name, delta): """Activates a switch and advances the time. Args: name: The name of the switch to activate. delta: The time (in seconds) to advance the clock. Note that this method does not deactivate the switch once the time has been advanced, meaning the switch stays active. To make the switch inactive, use the :meth:`release_switch_and_run`. """ self.machine.switch_controller.process_switch(name, state=1, logical=True) self.advance_time_and_run(delta) def release_switch_and_run(self, name, delta): """Deactivates a switch and advances the time. Args: name: The name of the switch to activate. delta: The time (in seconds) to advance the clock. """ self.machine.switch_controller.process_switch(name, state=0, logical=True) self.advance_time_and_run(delta) def hit_and_release_switch(self, name): """Momentarily activates and then deactivates a switch. Args: name: The name of the switch to hit. This method immediately activates and deactivates a switch with no time in between. """ self.machine.switch_controller.process_switch(name, state=1, logical=True) self.machine.switch_controller.process_switch(name, state=0, logical=True) self.machine_run() def hit_and_release_switches_simultaneously(self, names): """Momentarily activates and then deactivates multiple switches. Switches are hit sequentially and then released sequentially. Events are only processed at the end of the sequence which is useful to reproduce race conditions when processing nearly simultaneous hits. Args: names: The names of the switches to hit and release. """ for name in names: self.machine.switch_controller.process_switch(name, state=1, logical=True) for name in names: self.machine.switch_controller.process_switch(name, state=0, logical=True) self.machine_run() def tearDown(self): """Tear down test.""" if self._exception: try: self.machine.shutdown() except: pass if self._exception and 'exception' in self._exception: raise self._exception['exception'] elif self._exception: raise Exception(self._exception) duration = time.time() - self.test_start_time if duration > self.expected_duration: print("Test {}.{} took {} > {}s".format(self.__class__, self._testMethodName, round(duration, 2), self.expected_duration)) self.machine.log.debug("Test ended") if sys.exc_info != (None, None, None): # disable teardown logging after error logging.basicConfig(level=99) else: # fire all delays self.advance_time_and_run(300) self.machine._do_stop() self.machine = None self.restore_sys_path() asyncio.get_event_loop = self._get_event_loop self._get_event_loop = None events.get_event_loop = self._get_event_loop2 self._get_event_loop2 = None @staticmethod def add_to_config_validator(machine, key, new_dict): """Add config dict to validator.""" machine.config_validator.get_config_spec()[key] = new_dict
class MpfTestCase(unittest.TestCase): def __init__(self, methodName='runTest'): self._get_event_loop = None self._get_event_loop2 = None super().__init__(methodName) self.machine = None # type: TestMachineController self.machine_config_patches = dict() self.machine_config_patches['mpf'] = dict() self.machine_config_patches['mpf']['default_platform_hz'] = 100 self.machine_config_patches['mpf']['plugins'] = list() self.machine_config_patches['bcp'] = [] self._last_event_kwargs = {} self._events = {} self.expected_duration = 0.5 self.min_frame_time = 1 / 30 # test with default Hz def getConfigFile(self): """Override this method in your own test class to point to the config file you need for your tests. """ return 'null.yaml' def getMachinePath(self): """Override this method in your own test class to point to the machine folder you need for your tests. Path is related to the MPF package root """ return 'tests/machine_files/null/' def getAbsoluteMachinePath(self): # creates an absolute path based on machine_path return os.path.abspath( os.path.join(mpf.core.__path__[0], os.pardir, self.getMachinePath())) def get_abs_path(self, path): return os.path.join(os.path.abspath(os.curdir), path) def post_event(self, event_name, run_time=0): self.machine.events.post(event_name) self.advance_time_and_run(run_time) def post_event_with_params(self, event_name, **params): self.machine.events.post(event_name, **params) self.machine_run() def set_num_balls_known(self, balls): # in case the test does not have any ball devices self.machine.ball_controller.num_balls_known = balls def get_platform(self): return 'virtual' def get_use_bcp(self): return False def get_enable_plugins(self): return False def getOptions(self): mpfconfig = os.path.abspath( os.path.join(mpf.core.__path__[0], os.pardir, 'mpfconfig.yaml')) return { 'force_platform': self.get_platform(), 'mpfconfigfile': mpfconfig, 'configfile': Util.string_to_list(self.getConfigFile()), 'debug': True, 'bcp': self.get_use_bcp(), 'no_load_cache': False, 'create_config_cache': True, } def advance_time_and_run(self, delta=1.0): self.machine.log.info("Advancing time by %s", delta) try: self.loop.run_until_complete( asyncio.sleep(delay=delta, loop=self.loop)) return except RuntimeError as e: if self._exception and "exception" in self._exception: raise self._exception['exception'] elif self._exception: raise Exception(self._exception, e) raise e def machine_run(self): self.advance_time_and_run(0) def unittest_verbosity(self): """Return the verbosity setting of the currently running unittest program, or 0 if none is running. """ frame = inspect.currentframe() while frame: obj = frame.f_locals.get('self') if isinstance(obj, unittest.TestProgram) or isinstance( obj, unittest.TextTestRunner): return obj.verbosity frame = frame.f_back return 0 def save_and_prepare_sys_path(self): # save path self._sys_path = copy.deepcopy(sys.path) mpf_path = os.path.abspath( os.path.join(mpf.core.__path__[0], os.pardir)) if mpf_path in sys.path: sys.path.remove(mpf_path) # make tests path independent. remove current dir absolue if os.curdir in sys.path: sys.path.remove(os.curdir) # make tests path independent. remove current dir relative if "" in sys.path: sys.path.remove("") def restore_sys_path(self): # restore sys path sys.path = self._sys_path def _get_mock_data(self): """Return data for MockDataMangager in test.""" return dict() def _mock_loop(self): pass def _exception_handler(self, loop, context): try: loop.stop() except RuntimeError: pass self._exception = context def setUp(self): self._get_event_loop = asyncio.get_event_loop asyncio.get_event_loop = None self._get_event_loop2 = asyncio.events.get_event_loop events.get_event_loop = None # we want to reuse config_specs to speed tests up mpf.core.config_validator.ConfigValidator.unload_config_spec = ( MagicMock()) self._events = {} self._last_event_kwargs = {} self._exception = None # print(threading.active_count()) self.test_start_time = time.time() if self.unittest_verbosity() > 1: logging.basicConfig(level=logging.DEBUG, format='%(asctime)s : %(levelname)s : %(' 'name)s : %(message)s') else: # no logging by default logging.basicConfig(level=99) self.save_and_prepare_sys_path() # init machine machine_path = self.getAbsoluteMachinePath() self.loop = TimeTravelLoop() self.loop.set_exception_handler(self._exception_handler) self.clock = TestClock(self.loop) self._mock_loop() try: self.machine = TestMachineController( os.path.abspath(os.path.join(mpf.core.__path__[0], os.pardir)), machine_path, self.getOptions(), self.machine_config_patches, self.clock, self._get_mock_data(), self.get_enable_plugins()) start = time.time() while not self.machine.test_init_complete and time.time( ) < start + 20: self.advance_time_and_run(0.01) self.machine.events.process_event_queue() self.advance_time_and_run(1) except Exception as e: # todo temp until I can figure out how to stop the asset loader # thread automatically. try: self.machine.stop() except AttributeError: pass if self._exception and 'exception' in self._exception: raise self._exception['exception'] elif self._exception: raise Exception(self._exception, e) raise e self.assertTrue(self.machine.test_init_complete, "Machine crashed during start") def _mock_event_handler(self, event_name, **kwargs): self._last_event_kwargs[event_name] = kwargs self._events[event_name] += 1 def mock_event(self, event_name): self._events[event_name] = 0 self.machine.events.remove_handler_by_event( event=event_name, handler=self._mock_event_handler) self.machine.events.add_handler(event=event_name, handler=self._mock_event_handler, event_name=event_name) def assertBallsOnPlayfield(self, balls, playfield="playfield"): self.assertEqual(balls, self.machine.playfields[playfield].balls) def assertAvailableBallsOnPlayfield(self, balls, playfield="playfield"): self.assertEqual(balls, self.machine.playfields[playfield].available_balls) def assertMachineVarEqual(self, value, machine_var): self.assertTrue(self.machine.is_machine_var(machine_var), "Machine Var {} does not exist.".format(machine_var)) self.assertEqual(value, self.machine.get_machine_var(machine_var)) def assertPlayerVarEqual(self, value, player_var): self.assertIsNotNone(self.machine.game, "There is no game.") self.assertEqual( value, self.machine.game.player[player_var], "Value of player var {} is {} but should be {}".format( player_var, self.machine.game.player[player_var], value)) def assertSwitchState(self, name, state): self.assertIn(name, self.machine.switch_controller.switches, "Switch {} does not exist.".format(name)) self.assertEqual(state, self.machine.switch_controller.is_active(name)) def assertLedColor(self, led_name, color): if isinstance(color, str) and color.lower() == 'on': color = self.machine.leds[led_name].config['default_color'] self.assertEqual(list(RGBColor(color).rgb), self.machine.leds[led_name].hw_driver.current_color) def assertLedColors(self, led_name, color_list, secs=1, check_delta=.1): colors = list() # have to do it this weird way because `if 'on' in color_list:` doesn't # work since it tries to convert it to a color for color in color_list[:]: if isinstance(color, str) and color.lower() == 'on': color_list.remove('on') color_list.append( self.machine.leds[led_name].config['default_color']) break for x in range(int(secs / check_delta)): colors.append( RGBColor(self.machine.leds[led_name].hw_driver.current_color)) self.advance_time_and_run(check_delta) for color in color_list: self.assertIn(RGBColor(color), colors) def assertLightOn(self, light_name): self.assertEqual( 255, self.machine.lights[light_name].hw_driver.current_brightness) def assertLightOff(self, light_name): self.assertEqual( 0, self.machine.lights[light_name].hw_driver.current_brightness) def assertLightFlashing(self, light_name, secs=1, check_delta=.1): brightness_values = list() for _ in range(int(secs / check_delta)): brightness_values.append( self.machine.lights[light_name].hw_driver.current_brightness) self.advance_time_and_run(check_delta) self.assertIn(0, brightness_values) self.assertIn(255, brightness_values) def assertModeRunning(self, mode_name): if mode_name not in self.machine.modes: raise AssertionError("Mode {} not known.".format(mode_name)) self.assertIn(self.machine.modes[mode_name], self.machine.mode_controller.active_modes, "Mode {} not running.".format(mode_name)) def assertModeNotRunning(self, mode_name): if mode_name not in self.machine.modes: raise AssertionError("Mode {} not known.".format(mode_name)) self.assertNotIn(self.machine.modes[mode_name], self.machine.mode_controller.active_modes, "Mode {} running but should not.".format(mode_name)) def assertEventNotCalled(self, event_name): """Assert that event was not called.""" if event_name not in self._events: raise AssertionError("Event {} not mocked.".format(event_name)) if self._events[event_name] != 0: raise AssertionError("Event {} was called {} times.".format( event_name, self._events[event_name])) def assertEventCalled(self, event_name, times=None): """Assert that event was called.""" if event_name not in self._events: raise AssertionError("Event {} not mocked.".format(event_name)) if self._events[event_name] == 0 and times != 0: raise AssertionError("Event {} was not called.".format(event_name)) if times is not None and self._events[event_name] != times: raise AssertionError( "Event {} was called {} instead of {}.".format( event_name, self._events[event_name], times)) def assertEventCalledWith(self, event_name, **kwargs): """Assert that event was called with kwargs.""" self.assertEventCalled(event_name) self.assertEqual(kwargs, self._last_event_kwargs[event_name], "Args for {} differ.".format(event_name)) def assertShotShow(self, shot_name, show_name): """Assert that the highest priority running show for a shot is a certain show name.""" if shot_name not in self.machine.shots: raise AssertionError( "Shot {} is not a valid shot".format(shot_name)) if show_name: self.assertIsNotNone(self.machine.shots[shot_name].profiles) self.assertIsNotNone( self.machine.shots[shot_name].profiles[0]['running_show']) self.assertEqual( show_name, self.machine.shots[shot_name].profiles[0]['running_show'].name) else: self.assertIsNone( self.machine.shots[shot_name].profiles[0]['running_show']) def assertShotProfile(self, shot_name, profile_name): """Assert that the highest priority profile for a shot is a certain profile name.""" if shot_name not in self.machine.shots: raise AssertionError( "Shot {} is not a valid shot".format(shot_name)) if profile_name: self.assertIsNotNone(self.machine.shots[shot_name].profiles) self.assertIsNotNone( self.machine.shots[shot_name].profiles[0]['profile']) self.assertEqual( profile_name, self.machine.shots[shot_name].profiles[0]['profile']) else: self.assertIsNone( self.machine.shots[shot_name].profiles[0]['profile']) def assertShotProfileState(self, shot_name, state_name): """Assert that the highest priority profile for a shot is in a certain state.""" if shot_name not in self.machine.shots: raise AssertionError( "Shot {} is not a valid shot".format(shot_name)) if state_name: self.assertIsNotNone(self.machine.shots[shot_name].profiles) self.assertIsNotNone(self.machine.shots[shot_name].profiles[0] ['current_state_name']) self.assertEqual( state_name, self.machine.shots[shot_name].profiles[0] ['current_state_name']) else: self.assertIsNone(self.machine.shots[shot_name].profiles[0] ['current_state_name']) def assertShotEnabled(self, shot_name): if shot_name not in self.machine.shots: raise AssertionError( "Shot {} is not a valid shot".format(shot_name)) self.assertTrue(self.machine.shots[shot_name].profiles[0]['enable']) def assertShowRunning(self, show_name): for running_show in self.machine.show_controller.running_shows: if self.machine.shows[show_name] == running_show.show: return self.fail("Show {} not running".format(show_name)) def assertShowNotRunning(self, show_name): for running_show in self.machine.show_controller.running_shows: if self.machine.shows[show_name] == running_show.show: self.fail("Show {} should not be running".format(show_name)) def assertColorAlmostEqual(self, color1, color2, delta=6): if isinstance(color1, RGBColor) and isinstance(color2, RGBColor): difference = abs(color1.red - color2.red) +\ abs(color1.blue - color2.blue) +\ abs(color1.green - color2.green) else: difference = abs(color1[0] - color2[0]) +\ abs(color1[1] - color2[1]) +\ abs(color1[2] - color2[2]) self.assertLessEqual( difference, delta, "Colors do not match: " + str(color1) + " " + str(color2)) def get_timer(self, timer): for mode in self.machine.modes: for t in mode.timers: if t == timer: return mode.timers[t] raise AssertionError("Timer {} not found".format(timer)) def reset_mock_events(self): for event in self._events.keys(): self._events[event] = 0 def hit_switch_and_run(self, name, delta): self.machine.switch_controller.process_switch(name, 1, True) self.advance_time_and_run(delta) def release_switch_and_run(self, name, delta): self.machine.switch_controller.process_switch(name, 0, True) self.advance_time_and_run(delta) def hit_and_release_switch(self, name): self.machine.switch_controller.process_switch(name, 1, True) self.machine.switch_controller.process_switch(name, 0, True) self.machine_run() def tearDown(self): duration = time.time() - self.test_start_time if duration > self.expected_duration: print("Test {}.{} took {} > {}s".format(self.__class__, self._testMethodName, round(duration, 2), self.expected_duration)) self.machine.log.debug("Test ended") if sys.exc_info != (None, None, None): # disable teardown logging after error logging.basicConfig(level=99) else: # fire all delays self.min_frame_time = 20.0 self.advance_time_and_run(300) self.machine.stop() self.machine._do_stop() self.machine.clock.loop.close() self.machine = None self.restore_sys_path() asyncio.get_event_loop = self._get_event_loop self._get_event_loop = None events.get_event_loop = self._get_event_loop2 self._get_event_loop2 = None def add_to_config_validator(self, key, new_dict): if mpf.core.config_validator.ConfigValidator.config_spec: mpf.core.config_validator.ConfigValidator.config_spec[key] = ( new_dict) else: mpf.core.config_validator.mpf_config_spec += '\n' + yaml.dump( {key: new_dict}, default_flow_style=False)
def setUp(self): self._get_event_loop = asyncio.get_event_loop asyncio.get_event_loop = None self._get_event_loop2 = asyncio.events.get_event_loop events.get_event_loop = None # we want to reuse config_specs to speed tests up mpf.core.config_validator.ConfigValidator.unload_config_spec = ( MagicMock()) self._events = {} self._last_event_kwargs = {} self._exception = None # print(threading.active_count()) self.test_start_time = time.time() if self.unittest_verbosity() > 1: logging.basicConfig(level=logging.DEBUG, format='%(asctime)s : %(levelname)s : %(' 'name)s : %(message)s') else: # no logging by default logging.basicConfig(level=99) self.save_and_prepare_sys_path() # init machine machine_path = self.getAbsoluteMachinePath() self.loop = TimeTravelLoop() self.loop.set_exception_handler(self._exception_handler) self.clock = TestClock(self.loop) self._mock_loop() try: self.machine = TestMachineController( os.path.abspath(os.path.join(mpf.core.__path__[0], os.pardir)), machine_path, self.getOptions(), self.machine_config_patches, self.clock, self._get_mock_data(), self.get_enable_plugins()) start = time.time() while not self.machine.test_init_complete and time.time( ) < start + 20: self.advance_time_and_run(0.01) self.machine.events.process_event_queue() self.advance_time_and_run(1) except Exception as e: # todo temp until I can figure out how to stop the asset loader # thread automatically. try: self.machine.stop() except AttributeError: pass if self._exception and 'exception' in self._exception: raise self._exception['exception'] elif self._exception: raise Exception(self._exception, e) raise e self.assertTrue(self.machine.test_init_complete, "Machine crashed during start")
class AflRunner(object): """AFL fuzzer.""" def __init__(self, use_virtual): """Initialize fuzzer.""" self.loop = None self.clock = None self.machine = None # type: TestMachineController self.machine_config_patches = dict() self.machine_config_patches['mpf'] = dict() self.machine_config_patches['mpf']['default_platform_hz'] = 1 self.machine_config_patches['bcp'] = [] self.machine_config_defaults = {} self.switch_list = [] self.use_virtual = use_virtual self._invalid_input = False def _exception_handler(self, loop, context): try: loop.stop() except RuntimeError: pass self._exception = context def get_platform(self): """Return platform.""" if self.use_virtual: return "virtual" else: return "smart_virtual" def getConfigFile(self): # noqa """Return config file.""" return "config.yaml" def getAbsoluteMachinePath(self): # noqa """Return an absolute path based on machine_path.""" return "/home/kantert/cloud/flipper/src/good_vs_evil" def getOptions(self): # noqa """Return option arrays.""" mpfconfig = os.path.abspath( os.path.join(mpf.core.__path__[0], os.pardir, 'mpfconfig.yaml')) return { 'force_platform': self.get_platform(), 'mpfconfigfile': mpfconfig, 'configfile': Util.string_to_list(self.getConfigFile()), 'debug': True, 'bcp': False, 'no_load_cache': False, 'create_config_cache': True, 'text_ui': False, } def advance_time_and_run(self, delta=1.0): """Advance time and run.""" try: self.loop.run_until_complete( asyncio.sleep(delay=delta, loop=self.loop)) return except RuntimeError as e: if self._exception and "exception" in self._exception: raise self._exception['exception'] elif self._exception: raise Exception(self._exception, e) raise e def setUp(self, machine_path): """Set up fuzzer.""" self.loop = TimeTravelLoop() self.loop.set_exception_handler(self._exception_handler) self.clock = TestClock(self.loop) self.machine = TestMachineController( os.path.abspath(os.path.join(mpf.core.__path__[0], os.pardir)), machine_path, self.getOptions(), self.machine_config_patches, self.machine_config_defaults, self.clock, dict(), True) self.loop.run_until_complete(self.machine.initialise()) self.machine.events.process_event_queue() self.advance_time_and_run(1) self.switch_list = sorted(self.machine.switches.keys()) for switch in self.machine.switches: self.machine.switch_controller.process_switch_obj(switch, 0, True) def add_balls(self): """Add balls.""" for device in self.machine.ball_devices: if "trough" in device.tags: for switch in device.config['ball_switches']: self.machine.switch_controller.process_switch_obj( switch, 1, True) if device.config['entrance_switch']: self.machine.switch_controller.process_switch_obj( device.config['entrance_switch'], 1, True) # let balls settle self.advance_time_and_run(10) def start_game(self): """Start game.""" for switch in self.machine.switches: if "start" in switch.tags: self.machine.switch_controller.process_switch_obj( switch, 1, True) self.machine.switch_controller.process_switch_obj( switch, 0, True) def _abort(self, **kwargs): """Abort fuzzer run.""" del kwargs self._invalid_input = True def run(self, actions, find_logic_bugs): """Run fuzzer.""" if find_logic_bugs: self.machine.events.add_handler("balldevice_ball_missing", self._abort) self.machine.events.add_handler("found_new_ball", self._abort) self.machine.events.add_handler("mode_game_stopped", self._abort) for action in actions: if self._invalid_input: # bail out if we hit an invalid input. afl will notice this return if action & 0b10000000: ms = int(action & 0b01111111) ms *= ms self.advance_time_and_run(ms / 1000.0) else: switch = int(action & 0b01111111) if switch >= len(self.switch_list): continue switch_obj = self.machine.switches[self.switch_list[switch]] state = switch_obj.hw_state ^ 1 # print(switch_list[switch], state, switch_obj.hw_state) self.machine.switch_controller.process_switch_by_num( switch_obj.hw_switch.number, state, self.machine.default_platform) if find_logic_bugs: self.advance_time_and_run(60) if self._invalid_input: # might happen late return balls = 0 for playfield in self.machine.playfields: balls += playfield.balls if balls != self.machine.game.balls_in_play: print("Balls in play:", self.machine.game.balls_in_play) print("Playfields:") for playfield in self.machine.playfields: print(playfield.name, playfield.balls) print("Devices:") for device in self.machine.ball_devices: print(device.name, device.balls, device.available_balls) raise AssertionError( "Balls in play do not match balls on playfields.") def dump(self, wait, add_balls, start_game, actions): """Dump fuzzer input.""" if add_balls: for device in self.machine.ball_devices: if "trough" in device.tags: for switch in device.config['ball_switches']: print("Enable switch {}".format(switch.name)) if device.config['entrance_switch']: print("Enable switch {}".format(switch.name)) print("Advance time 10s") if start_game: for switch in self.machine.switches: if "start" in switch.tags: print("Enable/disable switch {}".format(switch.name)) if wait > 0: print("Advance time {}s".format(wait)) for action in actions: if action & 0b10000000: ms = int(action & 0b01111111) ms *= ms print("Advance time by {} ms".format(ms)) else: switch = int(action & 0b01111111) if switch >= len(self.switch_list): continue switch_obj = self.machine.switches[self.switch_list[switch]] state = switch_obj.hw_state ^ 1 # print(switch_list[switch], state, switch_obj.hw_state) print("Toggle switch {}. New state: {}".format( switch_obj.name, state)) self.machine.switch_controller.process_switch_by_num( switch_obj.hw_switch.number, state, self.machine.default_platform)