Beispiel #1
0
class TestEnvironment(object):

    @patch("sceptre.environment.Environment._load_stacks")
    @patch(
        "sceptre.environment.Environment.is_leaf", new_callable=PropertyMock
    )
    @patch("sceptre.environment.Environment._validate_path")
    def setup_method(
            self, test_method, mock_validate_path,
            mock_is_leaf, mock_load_stacks
    ):
        mock_is_leaf.return_value = True
        mock_load_stacks.return_value = sentinel.stacks
        mock_validate_path.return_value = "environment_path"

        self.environment = Environment(
            sceptre_dir="sceptre_dir",
            environment_path="environment_path",
            options=sentinel.options
        )

        # Run the rest of the tests against a leaf environment
        self.environment._is_leaf = True

    def test_initialise_environment(self):
        assert self.environment.sceptre_dir == "sceptre_dir"
        assert self.environment.path == "environment_path"
        assert self.environment._options == sentinel.options
        assert self.environment.is_leaf is True
        assert self.environment.stacks == sentinel.stacks

    @patch("sceptre.environment.Environment._load_environments")
    @patch(
        "sceptre.environment.Environment.is_leaf", new_callable=PropertyMock
    )
    @patch("sceptre.environment.Environment._validate_path")
    def test_initialise_environment_with_non_leaf_directory(
            self, mock_validate_path,
            mock_is_leaf, mock_load_environments
    ):
        mock_is_leaf.return_value = False
        mock_load_environments.return_value = sentinel.environments

        environment = Environment(
            sceptre_dir="sceptre_dir",
            environment_path="environment_path",
            options=sentinel.options
        )

        assert environment.environments == sentinel.environments

    def test_repr(self):
        self.environment.path = "path"
        self.environment.sceptre_dir = "sceptre_dir"
        self.environment._options = {}
        response = self.environment.__repr__()
        assert response == (
            "sceptre.environment.Environment(sceptre_dir='sceptre_dir', "
            "environment_path='path', options={})"
        )

    def test_validate_path_with_valid_path(self):
        path = self.environment._validate_path("valid/env/path")
        assert path == "valid/env/path"

    def test_validate_path_with_backslashes_in_path(self):
        path = self.environment._validate_path("valid\env\path")
        assert path == "valid/env/path"

    def test_validate_path_with_double_backslashes_in_path(self):
        path = self.environment._validate_path("valid\\env\\path")
        assert path == "valid/env/path"

    def test_validate_path_with_leading_slash(self):
        with pytest.raises(InvalidEnvironmentPathError):
            self.environment._validate_path(
                "/this/environment/path/is/invalid"
            )

    def test_validate_path_with_leading_backslash(self):
        with pytest.raises(InvalidEnvironmentPathError):
            self.environment._validate_path(
                "\\this\environment\path\is\invalid"
            )

    def test_validate_path_with_trailing_slash(self):
        with pytest.raises(InvalidEnvironmentPathError):
            self.environment._validate_path(
                "this/environment/path/is/invalid/"
            )

    def test_validate_path_with_trailing_backslash(self):
        with pytest.raises(InvalidEnvironmentPathError):
            self.environment._validate_path(
                "this\environment\path\is\invalid\\"
            )

    def test_is_leaf_with_leaf_dir(self):
        self.environment.sceptre_dir = os.path.join(
            os.getcwd(), "tests", "fixtures"
        )
        self.environment.path = os.path.join(
            "account", "environment", "region"
        )
        self.environment._is_leaf = None
        assert self.environment.is_leaf is True

    def test_is_leaf_with_non_leaf_dir(self):
        self.environment.sceptre_dir = os.path.join(
            os.getcwd(), "tests", "fixtures"
        )
        self.environment.path = os.path.join(
            "account", "environment"
        )
        self.environment._is_leaf = None
        assert self.environment.is_leaf is False

    @patch("sceptre.environment.Environment._build")
    @patch("sceptre.environment.Environment._check_for_circular_dependencies")
    @patch("sceptre.environment.Environment._get_launch_dependencies")
    @patch("sceptre.environment.Environment._get_initial_statuses")
    @patch("sceptre.environment.Environment._get_threading_events")
    def test_launch_calls_build_with_correct_args(
            self, mock_get_threading_events, mock_get_initial_statuses,
            mock_get_launch_dependencies, mock_check_for_circular_dependencies,
            mock_build
    ):
        mock_get_threading_events.return_value = sentinel.threading_events
        mock_get_initial_statuses.return_value = sentinel.stack_statuses
        mock_get_launch_dependencies.return_value = \
            sentinel.dependencies

        self.environment.launch()

        mock_check_for_circular_dependencies.assert_called_once_with()
        mock_build.assert_called_once_with(
            "launch", sentinel.threading_events,
            sentinel.stack_statuses, sentinel.dependencies
        )

    def test_launch_succeeds_with_empty_env(self):
        self.environment.stacks = {}
        response = self.environment.launch()
        assert response == {}

    @patch("sceptre.environment.Environment._build")
    @patch("sceptre.environment.Environment._check_for_circular_dependencies")
    @patch("sceptre.environment.Environment._get_delete_dependencies")
    @patch("sceptre.environment.Environment._get_initial_statuses")
    @patch("sceptre.environment.Environment._get_threading_events")
    def test_delete_calls_build_with_correct_args(
            self, mock_get_threading_events, mock_get_initial_statuses,
            mock_get_delete_dependencies, mock_check_for_circular_dependencies,
            mock_build
    ):
        mock_get_threading_events.return_value = sentinel.threading_events
        mock_get_initial_statuses.return_value = sentinel.stack_statuses
        mock_get_delete_dependencies.return_value = \
            sentinel.dependencies

        self.environment.delete()

        mock_check_for_circular_dependencies.assert_called_once_with()
        mock_build.assert_called_once_with(
            "delete", sentinel.threading_events,
            sentinel.stack_statuses, sentinel.dependencies
        )

    def test_delete_succeeds_with_empty_env(self):
        self.environment.stacks = {}
        response = self.environment.delete()
        assert response == {}

    def test_describe_with_running_stack(self):
        mock_stack = Mock()
        mock_stack.name = "stack"
        mock_stack.get_status.return_value = "status"
        self.environment.stacks = {"name": mock_stack}

        response = self.environment.describe()
        assert response == {"stack": "status"}

    def test_describe_with_missing_stack(self):
        mock_stack = Mock()
        mock_stack.name = "stack"
        mock_stack.get_status.side_effect = StackDoesNotExistError
        self.environment.stacks = {"stack": mock_stack}

        response = self.environment.describe()
        assert response == {"stack": "PENDING"}

    def test_describe_resources_forms_response(self):
        mock_stack = Mock()
        mock_stack.name = "stack-name"
        mock_stack.describe_resources.return_value = [
            {
                "LogicalResourceId": sentinel.logical_resource_id,
                "PhysicalResourceId": sentinel.physical_resource_id
            }
        ]

        self.environment.stacks = {"stack-name": mock_stack}
        response = self.environment.describe_resources()
        assert response == {
            "stack-name": [
                {
                    "LogicalResourceId": sentinel.logical_resource_id,
                    "PhysicalResourceId": sentinel.physical_resource_id
                }
            ]
        }

    def test_describe_resources_ignores_stack_does_not_exist_exception(self):
        mock_stack = Mock()
        mock_stack.full_stack_name = sentinel.full_stack_name
        mock_stack.describe_resources.side_effect = ClientError(
            {
                "Error": {
                    "Code": 500,
                    "Message": "stack does not exist"
                }
            },
            sentinel.operation
        )

        self.environment.stacks = {"name": mock_stack}
        response = self.environment.describe_resources()
        assert response == {}

    def test_describe_resources_raises_other_client_errors(self):
        mock_stack = Mock()
        mock_stack.full_stack_name = sentinel.full_stack_name
        mock_stack.describe_resources.side_effect = ClientError(
            {
                "Error": {
                    "Code": 500,
                    "Message": "Boom!"
                }
            },
            sentinel.operation
        )

        self.environment.stacks = {"mock_stack": mock_stack}
        with pytest.raises(ClientError):
            self.environment.describe_resources()

    @patch("sceptre.environment.wait")
    @patch("sceptre.environment.ThreadPoolExecutor")
    def test_build(self, mock_ThreadPoolExecutor, mock_wait):
        self.environment.stacks = {"mock_stack": sentinel.stack}
        mock_ThreadPoolExecutor.return_value.__enter__.return_value\
            .submit.return_value = sentinel.future

        self.environment._build(
            sentinel.command, sentinel.threading_events,
            sentinel.stack_statuses, sentinel.dependencies
        )

        mock_wait.assert_called_once_with([sentinel.future])

    def test_manage_stack_build_with_failed_dependency(self):
        threading_events = {"stack_1": Mock(), "stack_2": Mock()}
        stack_statuses = {
            "stack_1": StackStatus.FAILED,
            "stack_2": StackStatus.PENDING
        }
        dependencies = {"stack_1": [], "stack_2": ["stack_1"]}

        mock_stack_2 = Mock()
        mock_stack_2.name = "stack_2"

        self.environment._manage_stack_build(
            mock_stack_2,
            sentinel.command,
            threading_events,
            stack_statuses,
            dependencies
        )

        assert stack_statuses["stack_2"] == StackStatus.FAILED
        # Check that that stack's event is set
        threading_events["stack_2"].set.assert_called_once_with()

    def test_manage_stack_build_with_sucessful_command(self):
        threading_events = {"stack": Mock()}
        stack_statuses = {"stack": StackStatus.PENDING}
        dependencies = {"stack": []}

        mock_stack = Mock()
        mock_stack.name = "stack"
        mock_stack.launch.return_value = StackStatus.COMPLETE

        self.environment._manage_stack_build(
            mock_stack,
            "launch",
            threading_events,
            stack_statuses,
            dependencies
        )

        assert stack_statuses["stack"] == StackStatus.COMPLETE
        # Check that that stack's event is set
        threading_events["stack"].set.assert_called_once_with()

    def test_manage_stack_build_with_unsucessful_command(self):
        threading_events = {"stack": Mock()}
        stack_statuses = {"stack": StackStatus.PENDING}
        dependencies = {"stack": []}

        mock_stack = Mock()
        mock_stack.name = "stack"
        mock_stack.launch.side_effect = Exception()

        self.environment._manage_stack_build(
            mock_stack,
            "launch",
            threading_events,
            stack_statuses,
            dependencies
        )

        assert stack_statuses["stack"] == StackStatus.FAILED
        # Check that that stack's event is set
        threading_events["stack"].set.assert_called_once_with()

    @patch("sceptre.environment.threading.Event")
    def test_get_threading_events(self, mock_Event):
        mock_stack = Mock()
        mock_stack.name = "name"

        self.environment.stacks = {"mock_stack": mock_stack}

        mock_Event.return_value = sentinel.event

        response = self.environment._get_threading_events()
        assert response == {
            "name": sentinel.event
        }

    def test_get_initial_statuses(self):
        mock_stack = Mock()
        mock_stack.name = "name"

        self.environment.stacks = {"name": mock_stack}

        response = self.environment._get_initial_statuses()
        assert response == {
            "name": StackStatus.PENDING
        }

    def test_get_launch_dependencies(self):
        mock_stack = Mock()
        mock_stack.name = "dev/mock_stack"
        mock_stack.dependencies = [
            "dev/vpc",
            "dev/subnets",
            "prod/sg"
        ]

        self.environment.stacks = {"mock_stack": mock_stack}

        response = self.environment._get_launch_dependencies("dev")

        # Note that "prod/sg" is filtered out, because it's not under the
        # top level environment path "dev".
        assert response == {
            "dev/mock_stack": ["dev/vpc", "dev/subnets"]
        }

    @patch("sceptre.environment.Environment._get_launch_dependencies")
    def test_get_delete_dependencies(self, mock_get_launch_dependencies):
        mock_get_launch_dependencies.return_value = {
            "dev/mock_stack_1": [],
            "dev/mock_stack_2": [],
            "dev/mock_stack_3": ["dev/mock_stack_1", "dev/mock_stack_2"],
        }

        dependencies = self.environment._get_delete_dependencies()
        assert dependencies == {
            "dev/mock_stack_1": ["dev/mock_stack_3"],
            "dev/mock_stack_2": ["dev/mock_stack_3"],
            "dev/mock_stack_3": [],
        }

    def test_check_for_circular_dependencies_with_circular_dependencies(self):
        stack1 = MagicMock(Spec=Stack)
        stack2 = MagicMock(Spec=Stack)
        stack1.dependencies = ["stack2"]
        stack1.name = "stack1"
        stack2.dependencies = ["stack1"]
        stack2.name = "stack2"
        stacks = {
            "stack1": stack1,
            "stack2": stack2
        }
        self.environment.stacks = stacks
        with pytest.raises(CircularDependenciesError) as ex:
            self.environment._check_for_circular_dependencies()
        assert all(x in str(ex) for x in ['stack2', 'stack1'])

    def test_circular_dependencies_with_3_circular_dependencies(self):
        stack1 = MagicMock(Spec=Stack)
        stack2 = MagicMock(Spec=Stack)
        stack3 = MagicMock(Spec=Stack)
        stack1.dependencies = ["stack2"]
        stack1.name = "stack1"
        stack2.dependencies = ["stack3"]
        stack2.name = "stack2"
        stack3.dependencies = ["stack1"]
        stack3.name = "stack3"
        stacks = {
            "stack1": stack1,
            "stack2": stack2,
            "stack3": stack3
        }
        self.environment.stacks = stacks
        with pytest.raises(CircularDependenciesError) as ex:
            self.environment._check_for_circular_dependencies()
        assert all(x in str(ex) for x in ['stack3', 'stack2', 'stack1'])

    def test_no_circular_dependencies_throws_no_error(self):
        stack1 = MagicMock(Spec=Stack)
        stack2 = MagicMock(Spec=Stack)
        stack1.dependencies = ["stack2"]
        stack1.name = "stack1"
        stack2.dependencies = []
        stack2.name = "stack2"
        stacks = {
            "stack1": stack1,
            "stack2": stack2
        }
        self.environment.stacks = stacks
        # Check this runs without throwing an exception
        self.environment._check_for_circular_dependencies()

    def test_DAG_diamond_throws_no_circ_dependencies_error(self):
        """
        Ensures
            o
           / \
          o   o
           \ /
            o
        throws no circular dependency error
        """
        stack1 = MagicMock(Spec=Stack)
        stack2 = MagicMock(Spec=Stack)
        stack3 = MagicMock(Spec=Stack)
        stack4 = MagicMock(Spec=Stack)
        stack1.dependencies = ["stack2", "stack3"]
        stack1.name = "stack1"
        stack2.dependencies = ["stack4"]
        stack2.name = "stack2"
        stack3.dependencies = ["stack4"]
        stack3.name = "stack3"
        stack4.dependencies = []
        stack4.name = "stack4"
        stacks = {
            "stack1": stack1,
            "stack2": stack2,
            "stack3": stack3,
            "stack4": stack4
        }
        self.environment.stacks = stacks
        self.environment._check_for_circular_dependencies()

    def test_modified_DAG_diamond_throws_no_circ_dependencies_error(self):
        """
        Ensures
            o
           / \
          o   o
           \ / \
            o   o
        throws no circular dependency error
        """
        stack1 = MagicMock(Spec=Stack)
        stack2 = MagicMock(Spec=Stack)
        stack3 = MagicMock(Spec=Stack)
        stack4 = MagicMock(Spec=Stack)
        stack5 = MagicMock(Spec=Stack)
        stack1.dependencies = ["stack2", "stack3"]
        stack1.name = "stack1"
        stack2.dependencies = ["stack4"]
        stack2.name = "stack2"
        stack3.dependencies = ["stack4", "stack5"]
        stack3.name = "stack3"
        stack4.dependencies = []
        stack4.name = "stack4"
        stack5.dependencies = []
        stack5.name = "stack5"
        stacks = {
            "stack1": stack1,
            "stack2": stack2,
            "stack3": stack3,
            "stack4": stack4,
            "stack5": stack5
        }
        self.environment.stacks = stacks
        self.environment._check_for_circular_dependencies()

    def test_DAG_diamond_with_triangle_throws_no_circ_dependencies_error(self):
        """
        Ensures
            o
           / \
          o   o
           \ / \
            o ->o
        throws no circular dependency error
        """
        stack1 = MagicMock(Spec=Stack)
        stack2 = MagicMock(Spec=Stack)
        stack3 = MagicMock(Spec=Stack)
        stack4 = MagicMock(Spec=Stack)
        stack5 = MagicMock(Spec=Stack)
        stack1.dependencies = ["stack2", "stack3"]
        stack1.name = "stack1"
        stack2.dependencies = ["stack4"]
        stack2.name = "stack2"
        stack3.dependencies = ["stack4", "stack5"]
        stack3.name = "stack3"
        stack4.dependencies = ["stack5"]
        stack4.name = "stack4"
        stack5.dependencies = []
        stack5.name = "stack5"
        stacks = {
            "stack1": stack1,
            "stack2": stack2,
            "stack3": stack3,
            "stack4": stack4,
            "stack5": stack5
        }
        self.environment.stacks = stacks
        self.environment._check_for_circular_dependencies()

    def test_4_cycle_throws_circ_dependencies_error(self):
        """
        Ensures
            o - o
            |   |
            o - o
        throws circular dependency error
        """
        stack1 = MagicMock(Spec=Stack)
        stack2 = MagicMock(Spec=Stack)
        stack3 = MagicMock(Spec=Stack)
        stack4 = MagicMock(Spec=Stack)
        stack1.dependencies = ["stack4"]
        stack1.name = "stack1"
        stack2.dependencies = ["stack1"]
        stack2.name = "stack2"
        stack3.dependencies = ["stack2"]
        stack3.name = "stack3"
        stack4.dependencies = ["stack3"]
        stack4.name = "stack4"
        stacks = {
            "stack1": stack1,
            "stack2": stack2,
            "stack3": stack3,
            "stack4": stack4
        }
        self.environment.stacks = stacks
        with pytest.raises(CircularDependenciesError) as ex:
            self.environment._check_for_circular_dependencies()
        assert all(x in str(ex) for x in ['stack4', 'stack3', 'stack2',
                                          'stack1'])

    def test_modified_3_cycle_throws_circ_dependencies_error(self):
        """
        Ensures
            o   o
             \ / \
              o - o
              (right triangle is a 3 cycle)
        throws circular dependency error
        """
        stack1 = MagicMock(Spec=Stack)
        stack2 = MagicMock(Spec=Stack)
        stack3 = MagicMock(Spec=Stack)
        stack4 = MagicMock(Spec=Stack)
        stack1.dependencies = ["stack2"]
        stack1.name = "stack1"
        stack2.dependencies = ["stack3"]
        stack2.name = "stack2"
        stack3.dependencies = ["stack4"]
        stack3.name = "stack3"
        stack4.dependencies = ["stack2"]
        stack4.name = "stack4"
        stacks = {
            "stack1": stack1,
            "stack2": stack2,
            "stack3": stack3,
            "stack4": stack4
        }
        self.environment.stacks = stacks
        with pytest.raises(CircularDependenciesError) as ex:
            self.environment._check_for_circular_dependencies()
        assert (all(x in str(ex) for x in ['stack4', 'stack3', 'stack2']) and
                'stack1' not in str(ex))

    @patch("sceptre.environment.Config")
    def test_get_config(self, mock_Config):
        mock_Config.return_value.read.return_value = {}

        self.environment.sceptre_dir = "sceptre_dir"
        self.environment.path = "environment_path"
        self.environment._options = {
            "cli_option": "value",
            "user_variables": sentinel.user_variables
        }

        self.environment._get_config()
        mock_Config.assert_called_once_with(
            sceptre_dir="sceptre_dir",
            environment_path="environment_path",
            base_file_name="config"
        )
        mock_Config.return_value.read.assert_called_once_with(
            sentinel.user_variables
        )

    def test_get_available_stacks(self):
        self.environment.path = os.path.join(
            "account", "environment", "region"
        )
        self.environment.sceptre_dir = os.path.join(
            os.getcwd(), "tests", "fixtures"
        )
        response = self.environment._get_available_stacks()
        assert sorted(response) == sorted([
            "account/environment/region/vpc",
            "account/environment/region/subnets",
            "account/environment/region/security_groups"
        ])

    @patch("sceptre.environment.Stack")
    @patch("sceptre.environment.Environment._get_available_stacks")
    @patch("sceptre.environment.ConnectionManager")
    @patch("sceptre.environment.Environment._get_config")
    def test_load_stacks(
            self, mock_get_config, mock_ConnectionManager,
            mock_get_available_stacks, mock_Stack
    ):
        mock_config = {
            "region": sentinel.region,
            "iam_role": sentinel.iam_role,
            "profile": sentinel.profile
        }
        mock_get_config.return_value = mock_config
        mock_ConnectionManager.return_value = sentinel.connection_manager
        mock_get_available_stacks.return_value = ["stack_name"]
        mock_Stack.return_value = sentinel.stack

        response = self.environment._load_stacks()

        # Check ConnectionManager() is called with correct arguments
        mock_ConnectionManager.assert_called_once_with(
            region=sentinel.region,
            iam_role=sentinel.iam_role,
            profile=sentinel.profile
        )

        # Check Stack() is called with correct arguments
        mock_Stack.assert_called_once_with(
            name="stack_name",
            environment_config=mock_config,
            connection_manager=sentinel.connection_manager
        )
        # Check _load_stacks() returns list of stacks
        assert response == {'stack_name': sentinel.stack}

    def test_get_available_environments(self):
        self.environment.path = "account"
        self.environment.sceptre_dir = os.path.join(
            os.getcwd(), "tests", "fixtures"
        )
        response = self.environment._get_available_environments()
        assert response == ["account/environment"]

    @patch("sceptre.environment.Environment")
    @patch("sceptre.environment.Environment._get_available_environments")
    def test_load_environments(
            self, mock_get_available_environments, mock_Environment
    ):
        mock_get_available_environments.return_value = ["env"]
        mock_Environment.return_value = sentinel.environment

        response = self.environment._load_environments()
        assert response == {"env": sentinel.environment}
Beispiel #2
0
class TestEnvironment(object):

    @patch("sceptre.environment.Environment._load_stacks")
    @patch(
        "sceptre.environment.Environment.is_leaf", new_callable=PropertyMock
    )
    @patch("sceptre.environment.Environment._check_env_path_valid")
    def setup_method(
            self, test_method, mock_check_env_path_valid,
            mock_is_leaf, mock_load_stacks
    ):
        mock_is_leaf.return_value = True
        mock_load_stacks.return_value = sentinel.stacks

        self.environment = Environment(
            sceptre_dir="sceptre_dir",
            environment_path="environment_path",
            options=sentinel.options
        )

        # Run the rest of the tests against a leaf environment
        self.environment._is_leaf = True

    def test_initialise_environment(self):
        assert self.environment.sceptre_dir == "sceptre_dir"
        assert self.environment.path == "environment_path"
        assert self.environment._options == sentinel.options
        assert self.environment.is_leaf is True
        assert self.environment.stacks == sentinel.stacks

    @patch("sceptre.environment.Environment._load_environments")
    @patch(
        "sceptre.environment.Environment.is_leaf", new_callable=PropertyMock
    )
    @patch("sceptre.environment.Environment._check_env_path_valid")
    def test_initialsie_environment_with_non_leaf_directory(
            self, mock_check_env_path_valid,
            mock_is_leaf, mock_load_environments
    ):
        mock_is_leaf.return_value = False
        mock_load_environments.return_value = sentinel.environments

        environment = Environment(
            sceptre_dir="sceptre_dir",
            environment_path="environment_path",
            options=sentinel.options
        )

        assert environment.environments == sentinel.environments

    def test_repr(self):
        self.environment.path = "path"
        self.environment.sceptre_dir = "sceptre_dir"
        self.environment._options = {}
        response = self.environment.__repr__()
        assert response == (
            "sceptre.environment.Environment(sceptre_dir='sceptre_dir', "
            "environment_path='path', options={})"
        )

    def test_check_env_path_valid_with_valid_path(self):
        self.environment._check_env_path_valid("valid/env/path")

    def test_check_env_path_valid_with_invalid_path_leading_slash(self):
        with pytest.raises(InvalidEnvironmentPathError):
            self.environment._check_env_path_valid(
                "/this/environment/path/is/invalid"
            )

    def test_check_env_path_valid_with_invalid_path_trailing_slash(self):
        with pytest.raises(InvalidEnvironmentPathError):
            self.environment._check_env_path_valid(
                "this/environment/path/is/invalid/"
            )

    def test_is_leaf_with_leaf_dir(self):
        self.environment.sceptre_dir = os.path.join(
            os.getcwd(), "tests", "fixtures"
        )
        self.environment.path = os.path.join(
            "account", "environment", "region"
        )
        self.environment._is_leaf = None
        assert self.environment.is_leaf is True

    def test_is_leaf_with_non_leaf_dir(self):
        self.environment.sceptre_dir = os.path.join(
            os.getcwd(), "tests", "fixtures"
        )
        self.environment.path = os.path.join(
            "account", "environment"
        )
        self.environment._is_leaf = None
        assert self.environment.is_leaf is False

    @patch("sceptre.environment.Environment._build")
    @patch("sceptre.environment.Environment._check_for_circular_dependencies")
    @patch("sceptre.environment.Environment._get_launch_dependencies")
    @patch("sceptre.environment.Environment._get_initial_statuses")
    @patch("sceptre.environment.Environment._get_threading_events")
    def test_launch_calls_build_with_correct_args(
            self, mock_get_threading_events, mock_get_initial_statuses,
            mock_get_launch_dependencies, mock_check_for_circular_dependencies,
            mock_build
    ):
        mock_get_threading_events.return_value = sentinel.threading_events
        mock_get_initial_statuses.return_value = sentinel.stack_statuses
        mock_get_launch_dependencies.return_value = \
            sentinel.dependencies

        self.environment.launch()

        mock_check_for_circular_dependencies.assert_called_once_with(
            sentinel.dependencies
        )
        mock_build.assert_called_once_with(
            "launch", sentinel.threading_events,
            sentinel.stack_statuses, sentinel.dependencies
        )

    @patch("sceptre.environment.Environment._build")
    @patch("sceptre.environment.Environment._check_for_circular_dependencies")
    @patch("sceptre.environment.Environment._get_delete_dependencies")
    @patch("sceptre.environment.Environment._get_initial_statuses")
    @patch("sceptre.environment.Environment._get_threading_events")
    def test_delete_calls_build_with_correct_args(
            self, mock_get_threading_events, mock_get_initial_statuses,
            mock_get_delete_dependencies, mock_check_for_circular_dependencies,
            mock_build
    ):
        mock_get_threading_events.return_value = sentinel.threading_events
        mock_get_initial_statuses.return_value = sentinel.stack_statuses
        mock_get_delete_dependencies.return_value = \
            sentinel.dependencies

        self.environment.delete()

        mock_check_for_circular_dependencies.assert_called_once_with(
            sentinel.dependencies
        )
        mock_build.assert_called_once_with(
            "delete", sentinel.threading_events,
            sentinel.stack_statuses, sentinel.dependencies
        )

    def test_describe_with_running_stack(self):
        mock_stack = Mock()
        mock_stack.name = "stack"
        mock_stack.get_status.return_value = "status"
        self.environment.stacks = {"name": mock_stack}

        response = self.environment.describe()
        assert response == {"stack": "status"}

    def test_describe_with_missing_stack(self):
        mock_stack = Mock()
        mock_stack.name = "stack"
        mock_stack.get_status.side_effect = StackDoesNotExistError
        self.environment.stacks = {"stack": mock_stack}

        response = self.environment.describe()
        assert response == {"stack": "PENDING"}

    def test_describe_resources_forms_response(self):
        mock_stack = Mock()
        mock_stack.name = "stack-name"
        mock_stack.describe_resources.return_value = [
            {
                "LogicalResourceId": sentinel.logical_resource_id,
                "PhysicalResourceId": sentinel.physical_resource_id
            }
        ]

        self.environment.stacks = {"stack-name": mock_stack}
        response = self.environment.describe_resources()
        assert response == {
            "stack-name": [
                {
                    "LogicalResourceId": sentinel.logical_resource_id,
                    "PhysicalResourceId": sentinel.physical_resource_id
                }
            ]
        }

    def test_describe_resources_ignores_stack_does_not_exist_exception(self):
        mock_stack = Mock()
        mock_stack.full_stack_name = sentinel.full_stack_name
        mock_stack.describe_resources.side_effect = ClientError(
            {
                "Error": {
                    "Code": 500,
                    "Message": "stack does not exist"
                }
            },
            sentinel.operation
        )

        self.environment.stacks = {"name": mock_stack}
        response = self.environment.describe_resources()
        assert response == {}

    def test_describe_resources_raises_other_client_errors(self):
        mock_stack = Mock()
        mock_stack.full_stack_name = sentinel.full_stack_name
        mock_stack.describe_resources.side_effect = ClientError(
            {
                "Error": {
                    "Code": 500,
                    "Message": "Boom!"
                }
            },
            sentinel.operation
        )

        self.environment.stacks = {"mock_stack": mock_stack}
        with pytest.raises(ClientError):
            self.environment.describe_resources()

    @patch("sceptre.environment.wait")
    @patch("sceptre.environment.ThreadPoolExecutor")
    def test_build(self, mock_ThreadPoolExecutor, mock_wait):
        self.environment.stacks = {"mock_stack": sentinel.stack}
        mock_ThreadPoolExecutor.return_value.__enter__.return_value\
            .submit.return_value = sentinel.future

        self.environment._build(
            sentinel.command, sentinel.threading_events,
            sentinel.stack_statuses, sentinel.dependencies
        )

        mock_wait.assert_called_once_with([sentinel.future])

    def test_manage_stack_build_with_failed_dependency(self):
        threading_events = {"stack_1": Mock(), "stack_2": Mock()}
        stack_statuses = {
            "stack_1": StackStatus.FAILED,
            "stack_2": StackStatus.PENDING
        }
        dependencies = {"stack_1": [], "stack_2": ["stack_1"]}

        mock_stack_2 = Mock()
        mock_stack_2.name = "stack_2"

        self.environment._manage_stack_build(
            mock_stack_2,
            sentinel.command,
            threading_events,
            stack_statuses,
            dependencies
        )

        assert stack_statuses["stack_2"] == StackStatus.FAILED
        # Check that that stack's event is set
        threading_events["stack_2"].set.assert_called_once_with()

    def test_manage_stack_build_with_sucessful_command(self):
        threading_events = {"stack": Mock()}
        stack_statuses = {"stack": StackStatus.PENDING}
        dependencies = {"stack": []}

        mock_stack = Mock()
        mock_stack.name = "stack"
        mock_stack.launch.return_value = StackStatus.COMPLETE

        self.environment._manage_stack_build(
            mock_stack,
            "launch",
            threading_events,
            stack_statuses,
            dependencies
        )

        assert stack_statuses["stack"] == StackStatus.COMPLETE
        # Check that that stack's event is set
        threading_events["stack"].set.assert_called_once_with()

    def test_manage_stack_build_with_unsucessful_command(self):
        threading_events = {"stack": Mock()}
        stack_statuses = {"stack": StackStatus.PENDING}
        dependencies = {"stack": []}

        mock_stack = Mock()
        mock_stack.name = "stack"
        mock_stack.launch.side_effect = Exception()

        self.environment._manage_stack_build(
            mock_stack,
            "launch",
            threading_events,
            stack_statuses,
            dependencies
        )

        assert stack_statuses["stack"] == StackStatus.FAILED
        # Check that that stack's event is set
        threading_events["stack"].set.assert_called_once_with()

    @patch("sceptre.environment.threading.Event")
    def test_get_threading_events(self, mock_Event):
        mock_stack = Mock()
        mock_stack.name = "name"

        self.environment.stacks = {"mock_stack": mock_stack}

        mock_Event.return_value = sentinel.event

        response = self.environment._get_threading_events()
        assert response == {
            "name": sentinel.event
        }

    def test_get_initial_statuses(self):
        mock_stack = Mock()
        mock_stack.name = "name"

        self.environment.stacks = {"name": mock_stack}

        response = self.environment._get_initial_statuses()
        assert response == {
            "name": StackStatus.PENDING
        }

    def test_get_launch_dependencies(self):
        mock_stack = Mock()
        mock_stack.name = "dev/mock_stack"
        mock_stack.dependencies = [
            "dev/vpc",
            "dev/subnets",
            "prod/sg"
        ]

        self.environment.stacks = {"mock_stack": mock_stack}

        response = self.environment._get_launch_dependencies("dev")

        # Note that "prod/sg" is filtered out, because it's not under the
        # top level environment path "dev".
        assert response == {
            "dev/mock_stack": ["dev/vpc", "dev/subnets"]
        }

    @patch("sceptre.environment.Environment._get_launch_dependencies")
    def test_get_delete_dependencies(self, mock_get_launch_dependencies):
        mock_get_launch_dependencies.return_value = {
            "dev/mock_stack_1": [],
            "dev/mock_stack_2": [],
            "dev/mock_stack_3": ["dev/mock_stack_1", "dev/mock_stack_2"],
        }

        dependencies = self.environment._get_delete_dependencies()
        assert dependencies == {
            "dev/mock_stack_1": ["dev/mock_stack_3"],
            "dev/mock_stack_2": ["dev/mock_stack_3"],
            "dev/mock_stack_3": [],
        }

    def test_check_for_circular_dependencies_with_circular_dependencies(self):
        dependencies = {
            "stack-1": ["stack-2"],
            "stack-2": ["stack-1"]
        }

        with pytest.raises(CircularDependenciesError):
            self.environment._check_for_circular_dependencies(dependencies)

    def test_check_for_circular_dependencies_without_find_dependencies(self):
        dependencies = {
            "stack-1": ["stack-2"],
            "stack-2": []
        }

        # Check this runs without throwing an exception
        self.environment._check_for_circular_dependencies(dependencies)

    @patch("sceptre.environment.Config")
    def test_get_config(self, mock_Config):
        mock_Config.return_value.read.return_value = {}

        self.environment.sceptre_dir = "sceptre_dir"
        self.environment.path = "environment_path"
        self.environment._options = {
            "cli_option": "value",
            "user_variables": sentinel.user_variables
        }

        self.environment._get_config()
        mock_Config.assert_called_once_with(
            sceptre_dir="sceptre_dir",
            environment_path="environment_path",
            base_file_name="config"
        )
        mock_Config.return_value.read.assert_called_once_with(
            sentinel.user_variables
        )

    def test_get_available_stacks(self):
        self.environment.path = os.path.join(
            "account", "environment", "region"
        )
        self.environment.sceptre_dir = os.path.join(
            os.getcwd(), "tests", "fixtures"
        )
        response = self.environment._get_available_stacks()
        assert sorted(response) == sorted([
            "account/environment/region/vpc",
            "account/environment/region/subnets",
            "account/environment/region/security_groups"
        ])

    @patch("sceptre.environment.Stack")
    @patch("sceptre.environment.Environment._get_available_stacks")
    @patch("sceptre.environment.ConnectionManager")
    @patch("sceptre.environment.Environment._get_config")
    def test_load_stacks(
            self, mock_get_config, mock_ConnectionManager,
            mock_get_available_stacks, mock_Stack
    ):
        mock_config = {
            "region": sentinel.region,
            "iam_role": sentinel.iam_role
        }
        mock_get_config.return_value = mock_config
        mock_ConnectionManager.return_value = sentinel.connection_manager
        mock_get_available_stacks.return_value = ["stack_name"]
        mock_Stack.return_value = sentinel.stack

        response = self.environment._load_stacks()

        # Check ConnectionManager() is called with correct arguments
        mock_ConnectionManager.assert_called_once_with(
            region=sentinel.region,
            iam_role=sentinel.iam_role
        )

        # Check Stack() is called with correct arguments
        mock_Stack.assert_called_once_with(
            name="stack_name",
            environment_config=mock_config,
            connection_manager=sentinel.connection_manager
        )
        # Check _load_stacks() returns list of stacks
        assert response == {'stack_name': sentinel.stack}

    def test_get_available_environments(self):
        self.environment.path = "account"
        self.environment.sceptre_dir = os.path.join(
            os.getcwd(), "tests", "fixtures"
        )
        response = self.environment._get_available_environments()
        assert response == ["account/environment"]

    @patch("sceptre.environment.Environment")
    @patch("sceptre.environment.Environment._get_available_environments")
    def test_load_environments(
            self, mock_get_available_environments, mock_Environment
    ):
        mock_get_available_environments.return_value = ["env"]
        mock_Environment.return_value = sentinel.environment

        response = self.environment._load_environments()
        assert response == {"env": sentinel.environment}
class TestEnvironment(object):
    def setup_method(self, test_method):
        self.environment = Environment(path="path", options=sentinel.options)

        # Run the rest of the tests against a leaf environment
        self.environment._is_leaf = True

    def test_initialise_environment(self):
        assert self.environment.path == "path"
        assert self.environment._options == sentinel.options
        assert self.environment.stacks == []
        assert self.environment.sub_environments == []

    def test_initialise_environment_with_no_options(self):
        environment = Environment(path="path")
        assert environment.path == "path"
        assert environment._options == {}
        assert environment.stacks == []
        assert environment.sub_environments == []

    def test_repr(self):
        self.environment.path = "path"
        self.environment.sceptre_dir = "sceptre_dir"
        self.environment._options = {}
        response = self.environment.__repr__()
        assert response == (
            "sceptre.environment.Environment(path='path', options='{}')")

    @patch("sceptre.environment.Environment._build")
    @patch("sceptre.environment.Environment._check_for_circular_dependencies")
    @patch("sceptre.environment.Environment._get_launch_dependencies")
    @patch("sceptre.environment.Environment._get_initial_statuses")
    @patch("sceptre.environment.Environment._get_threading_events")
    def test_launch_calls_build_with_correct_args(
            self, mock_get_threading_events, mock_get_initial_statuses,
            mock_get_launch_dependencies, mock_check_for_circular_dependencies,
            mock_build):
        mock_get_threading_events.return_value = sentinel.threading_events
        mock_get_initial_statuses.return_value = sentinel.stack_statuses
        mock_get_launch_dependencies.return_value = \
            sentinel.dependencies

        self.environment.launch()

        mock_check_for_circular_dependencies.assert_called_once_with()
        mock_build.assert_called_once_with("launch", sentinel.threading_events,
                                           sentinel.stack_statuses,
                                           sentinel.dependencies)

    def test_launch_succeeds_with_empty_env(self):
        self.environment.stacks = {}
        response = self.environment.launch()
        assert response == {}

    @patch("sceptre.environment.Environment._build")
    @patch("sceptre.environment.Environment._check_for_circular_dependencies")
    @patch("sceptre.environment.Environment._get_delete_dependencies")
    @patch("sceptre.environment.Environment._get_initial_statuses")
    @patch("sceptre.environment.Environment._get_threading_events")
    def test_delete_calls_build_with_correct_args(
            self, mock_get_threading_events, mock_get_initial_statuses,
            mock_get_delete_dependencies, mock_check_for_circular_dependencies,
            mock_build):
        mock_get_threading_events.return_value = sentinel.threading_events
        mock_get_initial_statuses.return_value = sentinel.stack_statuses
        mock_get_delete_dependencies.return_value = \
            sentinel.dependencies

        self.environment.delete()

        mock_check_for_circular_dependencies.assert_called_once_with()
        mock_build.assert_called_once_with("delete", sentinel.threading_events,
                                           sentinel.stack_statuses,
                                           sentinel.dependencies)

    def test_delete_succeeds_with_empty_env(self):
        self.environment.stacks = {}
        response = self.environment.delete()
        assert response == {}

    def test_describe_with_running_stack(self):
        mock_stack = MagicMock(spec=Stack)
        mock_stack.name = "stack"
        mock_stack.get_status.return_value = "status"
        self.environment.stacks = [mock_stack]

        response = self.environment.describe()
        assert response == {"stack": "status"}

    def test_describe_with_missing_stack(self):
        mock_stack = MagicMock(spec=Stack)
        mock_stack.name = "stack"
        mock_stack.get_status.side_effect = StackDoesNotExistError()
        self.environment.stacks = [mock_stack]

        response = self.environment.describe()
        assert response == {"stack": "PENDING"}

    def test_describe_resources_forms_response(self):
        mock_stack = MagicMock(spec=Stack)
        mock_stack.name = "stack"
        mock_stack.describe_resources.return_value = [{
            "LogicalResourceId":
            sentinel.logical_resource_id,
            "PhysicalResourceId":
            sentinel.physical_resource_id
        }]

        self.environment.stacks = [mock_stack]
        response = self.environment.describe_resources()
        assert response == {
            "stack": [{
                "LogicalResourceId": sentinel.logical_resource_id,
                "PhysicalResourceId": sentinel.physical_resource_id
            }]
        }

    def test_describe_resources_ignores_stack_does_not_exist_exception(self):
        mock_stack = MagicMock(spec=Stack)
        mock_stack.name = "stack"
        mock_stack.describe_resources.side_effect = ClientError(
            {"Error": {
                "Code": 500,
                "Message": "stack does not exist"
            }}, sentinel.operation)

        self.environment.stacks = [mock_stack]
        response = self.environment.describe_resources()
        assert response == {}

    def test_describe_resources_raises_other_client_errors(self):
        mock_stack = MagicMock(spec=Stack)
        mock_stack.name = "stack"
        mock_stack.describe_resources.side_effect = ClientError(
            {"Error": {
                "Code": 500,
                "Message": "Boom!"
            }}, sentinel.operation)

        self.environment.stacks = [mock_stack]
        with pytest.raises(ClientError):
            self.environment.describe_resources()

    @patch("sceptre.environment.wait")
    @patch("sceptre.environment.ThreadPoolExecutor")
    def test_build(self, mock_ThreadPoolExecutor, mock_wait):
        self.environment.stacks = {"mock_stack": sentinel.stack}
        mock_ThreadPoolExecutor.return_value.__enter__.return_value\
            .submit.return_value = sentinel.future

        self.environment._build(sentinel.command, sentinel.threading_events,
                                sentinel.stack_statuses, sentinel.dependencies)

        mock_wait.assert_called_once_with([sentinel.future])

    def test_manage_stack_build_with_failed_dependency(self):
        threading_events = {"stack_1": Mock(), "stack_2": Mock()}
        stack_statuses = {
            "stack_1": StackStatus.FAILED,
            "stack_2": StackStatus.PENDING
        }
        dependencies = {"stack_1": [], "stack_2": ["stack_1"]}

        mock_stack_2 = Mock()
        mock_stack_2.name = "stack_2"

        self.environment._manage_stack_build(mock_stack_2, sentinel.command,
                                             threading_events, stack_statuses,
                                             dependencies)

        assert stack_statuses["stack_2"] == StackStatus.FAILED
        # Check that that stack's event is set
        threading_events["stack_2"].set.assert_called_once_with()

    def test_manage_stack_build_with_sucessful_command(self):
        threading_events = {"stack": Mock()}
        stack_statuses = {"stack": StackStatus.PENDING}
        dependencies = {"stack": []}

        mock_stack = Mock()
        mock_stack.name = "stack"
        mock_stack.launch.return_value = StackStatus.COMPLETE

        self.environment._manage_stack_build(mock_stack, "launch",
                                             threading_events, stack_statuses,
                                             dependencies)

        assert stack_statuses["stack"] == StackStatus.COMPLETE
        # Check that that stack's event is set
        threading_events["stack"].set.assert_called_once_with()

    def test_manage_stack_build_with_unsucessful_command(self):
        threading_events = {"stack": Mock()}
        stack_statuses = {"stack": StackStatus.PENDING}
        dependencies = {"stack": []}

        mock_stack = Mock()
        mock_stack.name = "stack"
        mock_stack.launch.side_effect = Exception()

        self.environment._manage_stack_build(mock_stack, "launch",
                                             threading_events, stack_statuses,
                                             dependencies)

        assert stack_statuses["stack"] == StackStatus.FAILED
        # Check that that stack's event is set
        threading_events["stack"].set.assert_called_once_with()

    @patch("sceptre.environment.threading.Event")
    def test_get_threading_events(self, mock_Event):
        mock_stack = MagicMock(spec=Stack)
        mock_stack.name = "stack"

        self.environment.stacks = [mock_stack]

        mock_Event.return_value = sentinel.event

        response = self.environment._get_threading_events()
        assert response == {"stack": sentinel.event}

    def test_get_initial_statuses(self):
        mock_stack = MagicMock(spec=Stack)
        mock_stack.name = "stack"

        self.environment.stacks = [mock_stack]

        response = self.environment._get_initial_statuses()
        assert response == {"stack": StackStatus.PENDING}

    def test_get_launch_dependencies(self):
        mock_stack = MagicMock(spec=Stack)
        mock_stack.name = "dev/mock_stack"

        mock_stack.dependencies = ["dev/vpc", "dev/subnets", "prod/sg"]

        self.environment.stacks = [mock_stack]

        response = self.environment._get_launch_dependencies("dev")

        # Note that "prod/sg" is filtered out, because it's not under the
        # top level environment path "dev".
        assert response == {"dev/mock_stack": ["dev/vpc", "dev/subnets"]}

    @patch("sceptre.environment.Environment._get_launch_dependencies")
    def test_get_delete_dependencies(self, mock_get_launch_dependencies):
        mock_get_launch_dependencies.return_value = {
            "dev/mock_stack_1": [],
            "dev/mock_stack_2": [],
            "dev/mock_stack_3": ["dev/mock_stack_1", "dev/mock_stack_2"],
        }

        dependencies = self.environment._get_delete_dependencies()
        assert dependencies == {
            "dev/mock_stack_1": ["dev/mock_stack_3"],
            "dev/mock_stack_2": ["dev/mock_stack_3"],
            "dev/mock_stack_3": [],
        }

    def test_check_for_circular_dependencies_with_circular_dependencies(self):
        stack1 = MagicMock(Spec=Stack)
        stack2 = MagicMock(Spec=Stack)
        stack1.dependencies = ["stack2"]
        stack1.name = "stack1"
        stack2.dependencies = ["stack1"]
        stack2.name = "stack2"
        stacks = [stack1, stack2]
        self.environment.stacks = stacks
        with pytest.raises(CircularDependenciesError) as ex:
            self.environment._check_for_circular_dependencies()
        assert all(x in str(ex) for x in ['stack2', 'stack1'])

    def test_circular_dependencies_with_3_circular_dependencies(self):
        stack1 = MagicMock(Spec=Stack)
        stack2 = MagicMock(Spec=Stack)
        stack3 = MagicMock(Spec=Stack)
        stack1.dependencies = ["stack2"]
        stack1.name = "stack1"
        stack2.dependencies = ["stack3"]
        stack2.name = "stack2"
        stack3.dependencies = ["stack1"]
        stack3.name = "stack3"
        stacks = [stack1, stack2, stack3]
        self.environment.stacks = stacks
        with pytest.raises(CircularDependenciesError) as ex:
            self.environment._check_for_circular_dependencies()
        assert all(x in str(ex) for x in ['stack3', 'stack2', 'stack1'])

    def test_no_circular_dependencies_throws_no_error(self):
        stack1 = MagicMock(Spec=Stack)
        stack2 = MagicMock(Spec=Stack)
        stack1.dependencies = ["stack2"]
        stack1.name = "stack1"
        stack2.dependencies = []
        stack2.name = "stack2"
        stacks = [stack1, stack2]

        self.environment.stacks = stacks
        # Check this runs without throwing an exception
        self.environment._check_for_circular_dependencies()

    def test_no_circular_dependencies_with_nested_stacks(self):
        stack1 = MagicMock(Spec=Stack)
        stack2 = MagicMock(Spec=Stack)
        stack1.dependencies = ["env1/stack2"]
        stack1.name = "stack1"
        stack2.dependencies = []
        stack2.name = "env1/stack2"
        stacks = [stack1, stack2]

        self.environment.stacks = stacks
        # Check this runs without throwing an exception
        self.environment._check_for_circular_dependencies()

    def test_DAG_diamond_throws_no_circ_dependencies_error(self):
        """
        Ensures
            o
           / \
          o   o
           \ /
            o
        throws no circular dependency error
        """
        stack1 = MagicMock(Spec=Stack)
        stack2 = MagicMock(Spec=Stack)
        stack3 = MagicMock(Spec=Stack)
        stack4 = MagicMock(Spec=Stack)
        stack1.dependencies = ["stack2", "stack3"]
        stack1.name = "stack1"
        stack2.dependencies = ["stack4"]
        stack2.name = "stack2"
        stack3.dependencies = ["stack4"]
        stack3.name = "stack3"
        stack4.dependencies = []
        stack4.name = "stack4"
        stacks = [stack1, stack2, stack3, stack4]

        self.environment.stacks = stacks
        self.environment._check_for_circular_dependencies()

    def test_modified_DAG_diamond_throws_no_circ_dependencies_error(self):
        """
        Ensures
            o
           / \
          o   o
           \ / \
            o   o
        throws no circular dependency error
        """
        stack1 = MagicMock(Spec=Stack)
        stack2 = MagicMock(Spec=Stack)
        stack3 = MagicMock(Spec=Stack)
        stack4 = MagicMock(Spec=Stack)
        stack5 = MagicMock(Spec=Stack)
        stack1.dependencies = ["stack2", "stack3"]
        stack1.name = "stack1"
        stack2.dependencies = ["stack4"]
        stack2.name = "stack2"
        stack3.dependencies = ["stack4", "stack5"]
        stack3.name = "stack3"
        stack4.dependencies = []
        stack4.name = "stack4"
        stack5.dependencies = []
        stack5.name = "stack5"
        stacks = [stack1, stack2, stack3, stack4, stack5]

        self.environment.stacks = stacks
        self.environment._check_for_circular_dependencies()

    def test_DAG_diamond_with_triangle_throws_no_circ_dependencies_error(self):
        """
        Ensures
            o
           / \
          o   o
           \ / \
            o ->o
        throws no circular dependency error
        """
        stack1 = MagicMock(Spec=Stack)
        stack2 = MagicMock(Spec=Stack)
        stack3 = MagicMock(Spec=Stack)
        stack4 = MagicMock(Spec=Stack)
        stack5 = MagicMock(Spec=Stack)
        stack1.dependencies = ["stack2", "stack3"]
        stack1.name = "stack1"
        stack2.dependencies = ["stack4"]
        stack2.name = "stack2"
        stack3.dependencies = ["stack4", "stack5"]
        stack3.name = "stack3"
        stack4.dependencies = ["stack5"]
        stack4.name = "stack4"
        stack5.dependencies = []
        stack5.name = "stack5"
        stacks = [stack1, stack2, stack3, stack4, stack5]

        self.environment.stacks = stacks
        self.environment._check_for_circular_dependencies()

    def test_4_cycle_throws_circ_dependencies_error(self):
        """
        Ensures
            o - o
            |   |
            o - o
        throws circular dependency error
        """
        stack1 = MagicMock(Spec=Stack)
        stack2 = MagicMock(Spec=Stack)
        stack3 = MagicMock(Spec=Stack)
        stack4 = MagicMock(Spec=Stack)
        stack1.dependencies = ["stack4"]
        stack1.name = "stack1"
        stack2.dependencies = ["stack1"]
        stack2.name = "stack2"
        stack3.dependencies = ["stack2"]
        stack3.name = "stack3"
        stack4.dependencies = ["stack3"]
        stack4.name = "stack4"
        stacks = [stack1, stack2, stack3, stack4]

        self.environment.stacks = stacks
        with pytest.raises(CircularDependenciesError) as ex:
            self.environment._check_for_circular_dependencies()
        assert all(x in str(ex)
                   for x in ['stack4', 'stack3', 'stack2', 'stack1'])

    def test_modified_3_cycle_throws_circ_dependencies_error(self):
        """
        Ensures
            o   o
             \ / \
              o - o
              (right triangle is a 3 cycle)
        throws circular dependency error
        """
        stack1 = MagicMock(Spec=Stack)
        stack2 = MagicMock(Spec=Stack)
        stack3 = MagicMock(Spec=Stack)
        stack4 = MagicMock(Spec=Stack)
        stack1.dependencies = ["stack2"]
        stack1.name = "stack1"
        stack2.dependencies = ["stack3"]
        stack2.name = "stack2"
        stack3.dependencies = ["stack4"]
        stack3.name = "stack3"
        stack4.dependencies = ["stack2"]
        stack4.name = "stack4"
        stacks = [stack1, stack2, stack3, stack4]

        self.environment.stacks = stacks
        with pytest.raises(CircularDependenciesError) as ex:
            self.environment._check_for_circular_dependencies()
        assert (all(x in str(ex) for x in ['stack4', 'stack3', 'stack2'])
                and 'stack1' not in str(ex))