/* * Copyright 2011-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ package com.amazonaws.services.stepfunctions.builder.internal.validation; import com.amazonaws.services.stepfunctions.builder.StateMachine; import org.junit.Test; import static com.amazonaws.services.stepfunctions.builder.StepFunctionBuilder.branch; import static com.amazonaws.services.stepfunctions.builder.StepFunctionBuilder.choice; import static com.amazonaws.services.stepfunctions.builder.StepFunctionBuilder.choiceState; import static com.amazonaws.services.stepfunctions.builder.StepFunctionBuilder.end; import static com.amazonaws.services.stepfunctions.builder.StepFunctionBuilder.eq; import static com.amazonaws.services.stepfunctions.builder.StepFunctionBuilder.next; import static com.amazonaws.services.stepfunctions.builder.StepFunctionBuilder.parallelState; import static com.amazonaws.services.stepfunctions.builder.StepFunctionBuilder.passState; import static com.amazonaws.services.stepfunctions.builder.StepFunctionBuilder.stateMachine; import static com.amazonaws.services.stepfunctions.builder.StepFunctionBuilder.succeedState; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; public class CycleTest { @Test public void singleTerminalState_HasNoCycle_IsValid() { assertNoCycle(stateMachine() .startAt("Initial") .state("Initial", succeedState())); } @Test(expected = ValidationException.class) public void simpleStateMachine_WithCycle_IsNotValid() { assertCycle(stateMachine() .startAt("Initial") .state("Initial", passState() .transition(next("Next"))) .state("Next", passState() .transition(next("Initial")))); } @Test(expected = ValidationException.class) public void choiceStateWithOnlyCycles_IsNotValid() { assertDoesNotHaveTerminalPath(stateMachine() .startAt("Initial") .state("Initial", passState() .transition(next("Choice"))) .state("Choice", choiceState() .defaultStateName("Default") .choice(choice() .transition(next("Initial")) .condition(eq("$.foo", "bar"))) .choice(choice() .transition(next("Default")) .condition(eq("$.foo", "bar")))) .state("Default", passState().transition(next("Choice")))); } @Test public void choiceStateWithPathToTerminal_IsValid() { assertHasPathToTerminal(stateMachine() .startAt("Initial") .state("Initial", passState() .transition(next("Choice"))) .state("Choice", choiceState() .defaultStateName("Default") .choice(choice() .transition(next("Initial")) .condition(eq("$.foo", "bar"))) .choice(choice() .transition(next("Default")) .condition(eq("$.foo", "bar")))) .state("Default", passState().transition(end()))); } @Test(expected = ValidationException.class) public void choiceStateWithClosedCycle_IsNotValid() { assertCycle(stateMachine() .startAt("Initial") .state("Initial", passState() .transition(next("Choice"))) .state("Choice", choiceState() .defaultStateName("Terminal") .choice(choice() .transition(next("Terminal")) .condition(eq("$.foo", "bar"))) .choice(choice() .transition(next("NonTerminal")) .condition(eq("$.foo", "bar")))) .state("Terminal", passState().transition(end())) .state("NonTerminal", passState().transition(next("Cyclic"))) .state("Cyclic", passState().transition(next("NonTerminal")))); } /** * While the nested ChoiceTwo state only has cycles, it has a cycle out of the choice state to * a state that contains a path to a terminal. The validator doesn't validate that the path out actually * has a path to the terminal so there are some invalid state machines that will pass validation. */ @Test public void choiceStateWithPathOut_IsValid() { assertNoCycle( stateMachine() .startAt("Initial") .state("Initial", passState() .transition(next("ChoiceOne"))) .state("ChoiceOne", choiceState() .defaultStateName("DefaultOne") .choice(choice() .transition(next("ChoiceTwo")) .condition(eq("$.foo", "bar")))) .state("DefaultOne", succeedState()) .state("ChoiceTwo", choiceState() .defaultStateName("DefaultTwo") .choice(choice() .transition(next("ChoiceOne")) .condition(eq("$.foo", "bar")))) .state("DefaultTwo", passState().transition(next("ChoiceTwo")))); } @Test public void parallelState_NoCycles() { assertNoCycle(stateMachine() .startAt("Initial") .state("Initial", parallelState() .branch(branch() .startAt("BranchOneStart") .state("BranchOneStart", succeedState())) .branch(branch() .startAt("BranchTwoStart") .state("BranchTwoStart", passState() .transition(next("NextState"))) .state("NextState", succeedState())) .transition(end()))); } @Test(expected = ValidationException.class) public void parallelState_WithCycles_IsNotValid() { assertCycle(stateMachine() .startAt("Parallel") .state("Parallel", parallelState() .branch(branch() .startAt("BranchOneInitial") .state("BranchOneInitial", passState() .transition(next("CyclicState"))) .state("CyclicState", passState() .transition(next("BranchOneInitial")))) .transition(end()))); } @Test(expected = ValidationException.class) public void parallelState_WithChoiceThatHasNoTerminalPath_IsNotValid() { assertDoesNotHaveTerminalPath( stateMachine() .startAt("Parallel") .state("Parallel", parallelState() .transition(end()) .branch(branch() .startAt("Initial") .state("Initial", passState() .transition(next("Choice"))) .state("Choice", choiceState() .defaultStateName("Default") .choice(choice() .transition(next("Initial")) .condition(eq("$.foo", "bar"))) .choice(choice() .transition(next("Default")) .condition(eq("$.foo", "bar")))) .state("Default", passState().transition(next("Choice")))))); } @Test public void parallelState_ChoiceStateWithTerminalPath_IsValid() { assertHasPathToTerminal( stateMachine() .startAt("Parallel") .state("Parallel", parallelState() .transition(end()) .branch(branch() .startAt("Initial") .state("Initial", passState() .transition(next("Choice"))) .state("Choice", choiceState() .defaultStateName("Default") .choice(choice() .transition(next("Initial")) .condition(eq("$.foo", "bar"))) .choice(choice() .transition(next("Default")) .condition(eq("$.foo", "bar")))) .state("Default", passState().transition(end()))))); } @Test(expected = ValidationException.class) public void parallelState_BranchContainsChoiceStateWithClosedCycle_IsNotValid() { assertCycle( stateMachine() .startAt("Initial") .state("Initial", passState() .transition(next("Choice"))) .state("Choice", choiceState() .defaultStateName("Terminal") .choice(choice() .transition(next("Terminal")) .condition(eq("$.foo", "bar"))) .choice(choice() .transition(next("NonTerminal")) .condition(eq("$.foo", "bar")))) .state("Terminal", passState().transition(end())) .state("NonTerminal", passState().transition(next("Cyclic"))) .state("Cyclic", passState().transition(next("NonTerminal")))); } private void assertCycle(StateMachine.Builder stateMachineBuilder) { try { validate(stateMachineBuilder); } catch (IllegalArgumentException expected) { assertThat(expected.getMessage(), containsString("Cycle detected")); } } private void assertDoesNotHaveTerminalPath(StateMachine.Builder stateMachineBuilder) { try { validate(stateMachineBuilder); } catch (IllegalArgumentException expected) { assertThat(expected.getMessage(), containsString("No path to a terminal state exists in the state machine")); } } private void assertHasPathToTerminal(StateMachine.Builder stateMachineBuilder) { validate(stateMachineBuilder); } private void assertNoCycle(StateMachine.Builder stateMachineBuilder) { validate(stateMachineBuilder); } private void validate(StateMachine.Builder stateMachineBuilder) { new StateMachineValidator(stateMachineBuilder.build()).validate(); } }