def test_maintains_updated_field_on_label(app, mock_test_feed): label = LabelRef('foo', 'test_machine') with mock_test_feed(), app.new_session(): state_machine.create_label( app, label, {}, ) first_updated = app.session.query(Label.updated, ).filter_by( name=label.name, state_machine=label.state_machine, ).scalar() with mock_test_feed(), app.new_session(): state_machine.update_metadata_for_label( app, label, {'foo': 'bar'}, ) second_updated = app.session.query(Label.updated, ).filter_by( name=label.name, state_machine=label.state_machine, ).scalar() assert second_updated > first_updated
def test_labels_needing_metadata_update_retry_in_gate(app, mock_test_feed, create_label, create_deleted_label, current_state): label_unprocessed = create_label('label_unprocessed', 'test_machine', {}) label_processed = create_label('label_processed', 'test_machine', {}) label_deleted = create_deleted_label('label_deleted', 'test_machine') test_machine = app.config.state_machines['test_machine'] gate = test_machine.states[0] with mock_test_feed(), app.new_session(): with mock.patch( 'routemaster.context.Context._pre_warm_feeds', side_effect=RequestException, ): state_machine.update_metadata_for_label( app, label_unprocessed, {'should_progress': True}, ) # Both should be in the start state... assert current_state(label_processed) == 'start' assert current_state(label_unprocessed) == 'start' assert current_state(label_deleted) is None # But only label_unprocessed should be pending a metadata update with app.new_session(): assert utils.labels_needing_metadata_update_retry_in_gate( app, test_machine, gate, ) == [label_unprocessed.name]
def test_metadata_update_gate_evaluations_dont_race_processing_subsequent_metadata_triggered_gate( create_label, app, assert_history): test_machine_2 = app.config.state_machines['test_machine_2'] gate_1 = test_machine_2.states[0] gate_2 = test_machine_2.states[1] label = create_label('foo', 'test_machine_2', {}) with mock.patch( 'routemaster.state_machine.api.needs_gate_evaluation_for_metadata_change', return_value=(True, gate_1), ), mock.patch( 'routemaster.state_machine.api.get_current_state', return_value=gate_2, ), app.new_session(): state_machine.update_metadata_for_label( app, label, {'should_progress': True}, ) # We should have no history entry 1->2 (as we mocked out the current state) # so the state machine should have considered us up-to-date and not moved. assert_history([ (None, 'gate_1'), ])
def test_concurrent_metadata_update_gate_evaluations_dont_race( create_label, app, assert_history, current_state): test_machine_2 = app.config.state_machines['test_machine_2'] gate_1 = test_machine_2.states[0] label = create_label('foo', 'test_machine_2', {}) with app.new_session(): state_machine.update_metadata_for_label( app, label, {'should_progress': True}, ) assert current_state(label) == 'gate_2' with mock.patch( 'routemaster.state_machine.api.needs_gate_evaluation_for_metadata_change', return_value=(True, gate_1), ), app.new_session(): state_machine.update_metadata_for_label( app, label, {'should_progress': True}, ) assert_history([ (None, 'gate_1'), ('gate_1', 'gate_2'), ])
def test_stays_in_gate_if_gate_processing_fails(app, mock_test_feed, current_state): label = LabelRef('foo', 'test_machine') with mock_test_feed(), app.new_session(): state_machine.create_label( app, label, {}, ) assert current_state(label) == 'start' with mock_test_feed(), mock.patch( 'routemaster.context.Context._pre_warm_feeds', side_effect=RequestException, ), app.new_session(): state_machine.update_metadata_for_label( app, label, {'should_progress': True}, ) assert metadata_triggers_processed(app, label) is False assert current_state(label) == 'start'
def test_state_machine_simple(app, mock_test_feed): label = LabelRef('foo', 'test_machine') with mock_test_feed(), app.new_session(): state_machine.create_label( app, label, {}, ) state_machine.update_metadata_for_label( app, label, {'foo': 'bar'}, ) with app.new_session(): assert state_machine.get_label_metadata(app, label) == {'foo': 'bar'}
def test_metadata_update_gate_evaluations_dont_process_subsequent_metadata_triggered_gate( create_label, app, assert_history, current_state): label = create_label('foo', 'test_machine_2', {}) with app.new_session(): state_machine.update_metadata_for_label( app, label, {'should_progress': True}, ) assert current_state(label) == 'gate_2' assert_history([ (None, 'gate_1'), ('gate_1', 'gate_2'), # Note: has not progressed to end because there is no on entry trigger # on gate 2 and we were not processing a metadata trigger on gate 2, # only gate 1. ])
def test_state_machine_does_not_progress_when_not_eligible( app, mock_test_feed, current_state): label = LabelRef('foo', 'test_machine') with mock_test_feed(), app.new_session(): state_machine.create_label( app, label, {}, ) assert current_state(label) == 'start' with mock_test_feed(), app.new_session(): state_machine.update_metadata_for_label( app, label, {'should_progress': False}, ) assert current_state(label) == 'start'
def test_state_machine_progresses_on_update(app, mock_webhook, mock_test_feed, current_state): label = LabelRef('foo', 'test_machine') with mock_test_feed(), app.new_session(): state_machine.create_label( app, label, {}, ) assert current_state(label) == 'start' with mock_webhook() as webhook, mock_test_feed(), app.new_session(): state_machine.update_metadata_for_label( app, label, {'should_progress': True}, ) webhook.assert_called_once() assert metadata_triggers_processed(app, label) is True assert current_state(label) == 'end'
def update_label(state_machine_name, label_name): """ Update a label in a state machine. Triggering progression if necessary according to the state machine configuration. Updates are _merged_ with existing metadata. Returns: - 200 Ok: if the label is successfully updated. - 400 Bad Request: if the request body is not a valid metadata. - 404 Not Found: if the state machine or label does not exist. - 410 Gone: if the label once existed but has since been deleted. Successful return codes return the full new metadata for a label. """ app = server.config.app label = LabelRef(label_name, state_machine_name) try: patch_metadata = request.get_json()['metadata'] except KeyError: abort(400, "No new metadata") try: new_metadata = state_machine.update_metadata_for_label( app, label, patch_metadata, ) state = state_machine.get_label_state(app, label) return jsonify(metadata=new_metadata, state=state.name) except UnknownStateMachine: msg = f"State machine '{state_machine_name}' does not exist" abort(404, msg) except UnknownLabel as e: abort( 410 if e.deleted else 404, f"Label {label_name} does not exist in state machine " f"'{state_machine_name}'.", )
def test_update_metadata_for_label_raises_for_unknown_state_machine(app): label = LabelRef('foo', 'nonexistent_machine') with pytest.raises(UnknownStateMachine), app.new_session(): state_machine.update_metadata_for_label(app, label, {})