def _load_gate(path: Path, yaml_state: Yaml, feed_names: List[str]) -> Gate: yaml_exit_condition = yaml_state['exit_condition'] if yaml_exit_condition is True: str_exit_condition = 'true' elif yaml_exit_condition is False: str_exit_condition = 'false' else: str_exit_condition = str(yaml_exit_condition).strip() exit_condition = ExitConditionProgram(str_exit_condition) _validate_context_lookups( path + ['exit_condition'], exit_condition.accessed_variables(), feed_names, ) return Gate( name=yaml_state['gate'], exit_condition=exit_condition, triggers=[ _load_trigger(path + ['triggers', str(idx)], yaml_trigger) for idx, yaml_trigger in enumerate(yaml_state.get('triggers', [])) ], next_states=_load_next_states( path + ['next'], yaml_state.get('next'), feed_names, ), )
def test_nonexistent_node_destination_invalid(app): state_machine = StateMachine( name='example', feeds=[], webhooks=[], states=[ Gate( name='start', triggers=[], next_states=ContextNextStates( path='foo.bar', destinations=[ ContextNextStatesOption( state='nonexistent', value='1', ), ContextNextStatesOption( state='end', value='2', ), ], default='end', ), exit_condition=ExitConditionProgram('false'), ), Gate( name='end', triggers=[], next_states=NoNextStates(), exit_condition=ExitConditionProgram('false'), ), ], ) with pytest.raises(ValidationError): _validate_state_machine(app, state_machine)
def test_non_unique_states_invalid(app): state_machine = StateMachine( name='example', feeds=[], webhooks=[], states=[ Gate( name='start', triggers=[], next_states=ConstantNextState('gate1'), exit_condition=ExitConditionProgram('false'), ), Gate( name='gate1', triggers=[], next_states=ConstantNextState('start'), exit_condition=ExitConditionProgram('false'), ), Gate( name='gate1', triggers=[], next_states=NoNextStates(), exit_condition=ExitConditionProgram('false'), ), ], ) with pytest.raises(ValidationError): _validate_state_machine(app, state_machine)
def test_evaluate(program, expected, variables, make_context): program = ExitConditionProgram(program) context = make_context( label='label1', metadata=VARIABLES, now=NOW, current_history_entry=HISTORY_ENTRY, accessed_variables=program.accessed_variables(), ) assert program.run(context) == expected
def test_errors(source, error, make_context): with pytest.raises(ValueError) as compile_error: program = ExitConditionProgram(source) context = make_context( label='label1', metadata=VARIABLES, now=NOW, current_history_entry=HISTORY_ENTRY, accessed_variables=program.accessed_variables(), ) program.run(context) message = str(compile_error.value) assert textwrap.dedent(message).strip() == textwrap.dedent(error).strip()
def test_gate_at_fixed_time_with_specific_timezone(custom_app): gate = Gate( 'fixed_time_gate', next_states=NoNextStates(), exit_condition=ExitConditionProgram('false'), triggers=[TimezoneAwareTrigger( datetime.time(12, 1), timezone='Europe/London', )], ) app = create_app(custom_app, [gate]) def processor(*, state, **kwargs): assert state == gate processor.called = True processor.called = False scheduler = schedule.Scheduler() configure_schedule(app, scheduler, processor) assert len(scheduler.jobs) == 1, "Should have scheduled a single job" job, = scheduler.jobs assert job.next_run == datetime.datetime(2018, 1, 1, 12, 1) assert processor.called is False with freezegun.freeze_time(job.next_run): job.run() assert processor.called is True assert job.next_run == datetime.datetime(2018, 1, 1, 12, 2)
def test_cron_job_gracefully_exit_signalling(custom_app): gate = Gate( 'gate', next_states=NoNextStates(), exit_condition=ExitConditionProgram('false'), triggers=[SystemTimeTrigger(datetime.time(12, 0))], ) app = create_app(custom_app, [gate]) state_machine = app.config.state_machines['test_machine'] items_to_process = ['one', 'two', 'should_not_process'] def is_terminating(): return len(items_to_process) == 1 def processor(app, state, state_machine, label): for item in items_to_process: items_to_process.pop(0) with mock.patch( 'routemaster.state_machine.api.get_current_state', return_value=gate, ), mock.patch('routemaster.state_machine.api.lock_label'): process_job( app=app, is_terminating=is_terminating, fn=processor, label_provider=lambda x, y, z: items_to_process, state=gate, state_machine=state_machine, ) assert items_to_process == ['should_not_process']
def test_gate_metadata_retry(custom_app): gate = Gate( 'fixed_time_gate', next_states=NoNextStates(), exit_condition=ExitConditionProgram('false'), triggers=[MetadataTrigger(metadata_path='foo.bar')], ) app = create_app(custom_app, [gate]) def processor(*, state, **kwargs): assert state == gate processor.called = True processor.called = False scheduler = schedule.Scheduler() configure_schedule(app, scheduler, processor) assert len(scheduler.jobs) == 1, "Should have scheduled a single job" job, = scheduler.jobs assert job.next_run == datetime.datetime(2018, 1, 1, 12, 1) assert processor.called is False with freezegun.freeze_time(job.next_run): job.run() assert processor.called is True assert job.next_run == datetime.datetime(2018, 1, 1, 12, 2)
def test_trivial_config(): data = yaml_data('trivial') expected = Config( state_machines={ 'example': StateMachine( name='example', feeds=[], webhooks=[], states=[ Gate( name='start', triggers=[], next_states=NoNextStates(), exit_condition=ExitConditionProgram('false'), ), ], ), }, database=DatabaseConfig( host='localhost', port=5432, name='routemaster', username='******', password='', ), logging_plugins=[], ) with reset_environment(): assert load_config(data) == expected
def test_disconnected_state_machine_invalid(app): state_machine = StateMachine( name='example', feeds=[], webhooks=[], states=[ Gate( name='start', triggers=[], next_states=NoNextStates(), exit_condition=ExitConditionProgram('false'), ), Gate( name='end', triggers=[], next_states=NoNextStates(), exit_condition=ExitConditionProgram('false'), ), ], ) with pytest.raises(ValidationError): _validate_state_machine(app, state_machine)
def test_valid(app): _validate_state_machine( app, StateMachine( name='example', feeds=[], webhooks=[], states=[ Gate( name='start', triggers=[], next_states=ConstantNextState('end'), exit_condition=ExitConditionProgram('false'), ), Gate( name='end', triggers=[], next_states=NoNextStates(), exit_condition=ExitConditionProgram('false'), ), ], ))
def test_get_current_state_for_label_in_invalid_state(custom_app, create_label): state_to_be_removed = Gate( name='start', triggers=[], next_states=ConstantNextState('end'), exit_condition=ExitConditionProgram('false'), ) end_state = Gate( name='end', triggers=[], next_states=NoNextStates(), exit_condition=ExitConditionProgram('false'), ) app = custom_app( state_machines={ 'test_machine': StateMachine( name='test_machine', states=[state_to_be_removed, end_state], feeds=[], webhooks=[], ), }) label = create_label('foo', 'test_machine', {}) state_machine = app.config.state_machines['test_machine'] # Remove the state which we expect the label to be in from the state # machine; this is logically equivalent to loading a new config which does # not have the state del state_machine.states[0] with app.new_session(): with pytest.raises(Exception): utils.get_current_state(app, label, state_machine)
def test_label_in_deleted_state_invalid(app, create_label): create_label('foo', 'test_machine', {}) # Created in "start" implicitly state_machine = StateMachine( name='test_machine', feeds=[], webhooks=[], states=[ # Note: state "start" from "test_machine" is gone. Gate( name='end', triggers=[], next_states=NoNextStates(), exit_condition=ExitConditionProgram('false'), ), ], ) with pytest.raises(ValidationError): _validate_state_machine(app, state_machine)
def test_feeds_for_state_machine(): state_machine = StateMachine( name='example', feeds=[ FeedConfig(name='test_feed', url='http://localhost/<label>'), ], webhooks=[], states=[ Gate( name='start', triggers=[], next_states=NoNextStates(), exit_condition=ExitConditionProgram('false'), ), ], ) feeds = feeds_for_state_machine(state_machine) assert 'test_feed' in feeds assert feeds['test_feed'].data is None assert feeds['test_feed'].url == 'http://localhost/<label>' assert feeds['test_feed'].state_machine == 'example'
def test_label_in_deleted_state_on_per_state_machine_basis( app, create_label, ): create_label('foo', 'test_machine', {}) # Created in "start" implicitly state_machine = StateMachine( name='other_machine', feeds=[], webhooks=[], states=[ # Note: state "start" is not present, but that we're in a different # state machine. Gate( name='end', triggers=[], next_states=NoNextStates(), exit_condition=ExitConditionProgram('false'), ), ], ) # Should not care about our label as it is in a different state machine. _validate_state_machine(app, state_machine)
def test_cron_job_does_not_forward_exceptions(custom_app): gate = Gate( 'gate', next_states=NoNextStates(), exit_condition=ExitConditionProgram('false'), triggers=[SystemTimeTrigger(datetime.time(12, 0))], ) app = create_app(custom_app, [gate]) state_machine = app.config.state_machines['test_machine'] # To get aroung inability to change reference in this scope from # `raise_value_error`. raised = {'raised': False} def raise_value_error(*args): raised['raised'] = True raise ValueError() def processor(*args, **kwargs): pass with mock.patch( 'routemaster.state_machine.api.get_current_state', return_value=gate, ), mock.patch('routemaster.state_machine.api.lock_label'): process_job( app=app, is_terminating=raise_value_error, fn=processor, label_provider=lambda x, y, z: [1], state=gate, state_machine=state_machine, ) assert raised['raised'], \ "Test did not trigger exception correctly in cron system"
def test_realistic_config(): data = yaml_data('realistic') expected = Config( state_machines={ 'example': StateMachine( name='example', feeds=[ FeedConfig(name='data_feed', url='http://localhost/<label>'), ], webhooks=[ Webhook( match=re.compile('.+\\.example\\.com'), headers={ 'x-api-key': 'Rahfew7eed1ierae0moa2sho3ieB1et3ohhum0Ei', }, ), ], states=[ Gate( name='start', triggers=[ SystemTimeTrigger(time=datetime.time(18, 30)), TimezoneAwareTrigger( time=datetime.time(12, 25), timezone='Europe/London', ), MetadataTimezoneAwareTrigger( time=datetime.time(13, 37), timezone_metadata_path=['timezone'], ), MetadataTrigger(metadata_path='foo.bar'), IntervalTrigger( interval=datetime.timedelta(hours=1), ), OnEntryTrigger(), ], next_states=ConstantNextState(state='stage2'), exit_condition=ExitConditionProgram('true'), ), Gate( name='stage2', triggers=[], next_states=ContextNextStates( path='metadata.foo.bar', destinations=[ ContextNextStatesOption( state='stage3', value='1', ), ContextNextStatesOption( state='stage3', value='2', ), ], default='end', ), exit_condition=ExitConditionProgram( 'metadata.foo.bar is defined', ), ), Action( name='stage3', webhook='https://localhost/hook', next_states=ConstantNextState(state='end'), ), Gate( name='end', triggers=[], exit_condition=ExitConditionProgram('false'), next_states=NoNextStates(), ), ], ), }, database=DatabaseConfig( host='localhost', port=5432, name='routemaster', username='******', password='', ), logging_plugins=[ LoggingPluginConfig( dotted_path='routemaster_prometheus:PrometheusLogger', kwargs={'prometheus_gateway': 'localhost'}, ), LoggingPluginConfig( dotted_path='routemaster_sentry:SentryLogger', kwargs={'raven_dsn': 'nai8ioca4zeeb2ahgh4V'}, ), ], ) with reset_environment(): assert load_config(data) == expected
def test_accessed_variables(program, expected, variables): program = ExitConditionProgram(program) assert sorted(program.accessed_variables()) == sorted(variables)