def setUp(self): self.cloudformation = MagicMock(spec=CloudFormationConnection) self.stack = MagicMock(spec=Stack) self.stack.stack_status = 'CREATE_COMPLETE' self.cloudformation.describe_stacks.return_value = [self.stack] self.coreos = MagicMock(spec=CoreOsAmiIndex) self.service = {'service_name': NAME} self.cf = FlotillaCloudFormation(ENVIRONMENT, DOMAIN, self.coreos, backoff=0.001) self.service_template = { 'Parameters': { 'PublicSubnet01': {}, 'PrivateSubnet01': {} }, 'Resources': { 'PublicSubnet01': {}, 'PrivateSubnet01': {}, 'Elb': { 'Properties': { 'Subnets': [] } }, 'Asg': { 'Properties': { 'VPCZoneIdentifier': [] } } } }
def setUp(self): self.cloudformation = MagicMock(spec=CloudFormationConnection) self.stack = MagicMock(spec=Stack) self.stack.stack_status = 'CREATE_COMPLETE' self.cloudformation.describe_stacks.return_value = [self.stack] self.coreos = MagicMock(spec=CoreOsAmiIndex) self.service = { 'service_name': NAME } self.cf = FlotillaCloudFormation(ENVIRONMENT, DOMAIN, self.coreos, backoff=0.001) self.service_template = { 'Parameters': { 'PublicSubnet01': { }, 'PrivateSubnet01': { } }, 'Resources': { 'PublicSubnet01': { }, 'PrivateSubnet01': { }, 'Elb': { 'Properties': { 'Subnets': [ ] } }, 'Asg': { 'Properties': { 'VPCZoneIdentifier': [ ] } } } }
class TestFlotillaCloudFormation(unittest.TestCase): def setUp(self): self.cloudformation = MagicMock(spec=CloudFormationConnection) self.stack = MagicMock(spec=Stack) self.stack.stack_status = 'CREATE_COMPLETE' self.cloudformation.describe_stacks.return_value = [self.stack] self.coreos = MagicMock(spec=CoreOsAmiIndex) self.service = {'service_name': NAME} self.cf = FlotillaCloudFormation(ENVIRONMENT, DOMAIN, self.coreos, backoff=0.001) self.service_template = { 'Parameters': { 'PublicSubnet01': {}, 'PrivateSubnet01': {} }, 'Resources': { 'PublicSubnet01': {}, 'PrivateSubnet01': {}, 'Elb': { 'Properties': { 'Subnets': [] } }, 'Asg': { 'Properties': { 'VPCZoneIdentifier': [] } } } } @patch('boto.cloudformation.connect_to_region') def test_client_cache(self, mock_connect): mock_connect.return_value = self.cloudformation self.assertEqual(0, len(self.cf._clients)) self.cf._client(REGION_NAME) self.assertEqual(1, len(self.cf._clients)) self.assertEqual(self.cloudformation, self.cf._clients[REGION_NAME]) def test_service_hash(self): service_hash = self.cf._service_hash(self.service, {'foo': 'bar'}) self.assertIsNotNone(service_hash) def test_service_hash_string_fields(self): hash_base = self.cf._service_hash(self.service, {'foo': 'bar'}) self.service['instance_type'] = 't2.micro' hash_with_string = self.cf._service_hash(self.service, {'foo': 'bar'}) self.assertNotEqual(hash_base, hash_with_string) def test_service_hash_iterable_fields(self): hash_base = self.cf._service_hash(self.service, {'foo': 'bar'}) self.service['regions'] = ['us-east-1', 'us-west-2'] hash_with_list = self.cf._service_hash(self.service, {'foo': 'bar'}) self.assertNotEqual(hash_base, hash_with_list) self.service['regions'] = ['us-west-2', 'us-east-1'] hash_with_list_order = self.cf._service_hash(self.service, {'foo': 'bar'}) self.assertNotEqual(hash_base, hash_with_list_order) self.assertEqual(hash_with_list, hash_with_list_order) def test_stack_does_not_exists(self): self.mock_client() not_found = BotoServerError(400, 'Not Found') self.cloudformation.describe_stacks.side_effect = not_found self.cloudformation.create_stack.return_value = STACK_ARN stack = self.cf._stack(REGION_NAME, NAME, TEMPLATE, {}) self.cloudformation.create_stack. \ assert_called_with(NAME, capabilities=CAPABILITIES, template_body=TEMPLATE, parameters=ANY) self.cloudformation.update_stack.assert_not_called() self.assertEqual(stack.stack_id, STACK_ARN) def test_stack_existing_in_progress(self): self.mock_client() self.stack.stack_status = 'CREATE_IN_PROGRESS' stack = self.cf._stack(REGION_NAME, NAME, '{}', {}) self.assertEqual(stack.stack_status, 'CREATE_IN_PROGRESS') self.cloudformation.create_stack.assert_not_called() self.cloudformation.update_stack.assert_not_called() def test_stack_existing_update(self): self.mock_client() self.cloudformation.update_stack.return_value = STACK_ARN stack = self.cf._stack(REGION_NAME, NAME, TEMPLATE, {}) self.cloudformation.create_stack.assert_not_called() self.cloudformation.update_stack. \ assert_called_with(NAME, capabilities=CAPABILITIES, template_body=TEMPLATE, parameters=ANY) self.assertEqual(stack.stack_id, STACK_ARN) def test_stack_existing_update_exception(self): self.mock_client() unknown_error = BotoServerError(400, 'Unknown error') self.cloudformation.update_stack.side_effect = unknown_error self.assertRaises(BotoServerError, self.cf._stack, REGION_NAME, NAME, TEMPLATE, {}) def test_stack_existing_update_noop(self): self.mock_client() no_updates = BotoServerError(400, 'Unknown error', body='<Message>No updates are to be' ' performed.</Message>') self.cloudformation.update_stack.side_effect = no_updates stack = self.cf._stack(REGION_NAME, NAME, TEMPLATE, {}) self.assertEqual(stack, self.stack) def test_service(self): self.cf._stack = MagicMock() self.cf.service(REGION, self.service, {}, None) self.cf._stack.assert_called_with(REGION_NAME, SERVICE_STACK, ANY, ANY) def test_service_complete(self): self.cf._complete = MagicMock(return_value=True) service = self.cf.service(REGION, self.service, {}, None) self.assertIsNone(service) def test_service_public_ports(self): self.cf._stack = MagicMock() self.service['public_ports'] = {'9200': 'HTTP'} self.cf.service(REGION, self.service, {}, None) self.cf._stack.assert_called_with(REGION_NAME, SERVICE_STACK, ANY, ANY) def test_service_private_ports(self): self.cf._stack = MagicMock() self.service['private_ports'] = {'9300': ['TCP']} self.cf.service(REGION, self.service, {}, None) self.cf._stack.assert_called_with(REGION_NAME, SERVICE_STACK, ANY, ANY) def test_service_params_generate_dns(self): stack_params = self.cf._service_params(REGION, self.service, {}, self.service_template) self.assertEqual(stack_params['VirtualHostDomain'], DOMAIN + '.') self.assertEqual(stack_params['VirtualHost'], 'service-test.test.com') def test_service_params_dns_parameter(self): self.service['dns_name'] = 'testapp.test.com' stack_params = self.cf._service_params(REGION, self.service, {}, self.service_template) self.assertEqual(stack_params['VirtualHostDomain'], DOMAIN + '.') self.assertEqual(stack_params['VirtualHost'], 'testapp.test.com') def test_service_params_custom_container(self): region = REGION.copy() region['flotilla_container'] = 'pwagner/flotilla' stack_params = self.cf._service_params(region, self.service, {}, self.service_template) self.assertEqual(stack_params['FlotillaContainer'], 'pwagner/flotilla') def test_service_params_subnets(self): self.cf._service_params( REGION, self.service, { 'PrivateSubnet01': 'subnet-123456', 'PrivateSubnet02': 'subnet-654321', 'PublicSubnet01': 'subnet-234561', 'PublicSubnet02': 'subnet-165432', }, self.service_template) self.assertEqual(4, len(self.service_template['Parameters'])) def test_vpc(self): self.cf._stack = MagicMock() self.cf.vpc({'region_name': REGION_NAME}, None) self.cf._stack.assert_called_with(REGION_NAME, 'flotilla-test-vpc', ANY, ANY) def test_vpc_complete(self): self.cf._complete = MagicMock(return_value=True) vpc = self.cf.vpc({'region_name': REGION_NAME}, None) self.assertIsNone(vpc) def test_vpc_params_empty(self): params = self.cf._vpc_params({'region_name': REGION_NAME}) self.assertEquals(params['FlotillaEnvironment'], ENVIRONMENT) self.assertEquals(params['BastionInstanceType'], 't2.nano') self.assertIn('BastionAmi', params) self.assertTrue('FlotillaContainer' not in params) def test_vpc_params_container(self): params = self.cf._vpc_params({ 'region_name': REGION_NAME, 'flotilla_container': 'pwagner/flotilla' }) self.assertEqual(params['FlotillaContainer'], 'pwagner/flotilla') def test_vpc_params_az(self): params = self.cf._vpc_params({ 'region_name': REGION_NAME, 'az1': 'us-east-1a', 'az2': 'us-east-1b' }) self.assertEqual(params['Az01'], 'us-east-1a') self.assertEqual(params['Az02'], 'us-east-1b') def test_vpc_params_nat_per_az(self): params = self.cf._vpc_params({ 'region_name': REGION_NAME, 'nat_per_az': 'true' }) self.assertEqual(params['NatPerAz'], 'true') def test_vpc_params_nat_per_az_invalid(self): params = self.cf._vpc_params({ 'region_name': REGION_NAME, 'nat_per_az': 'meow' }) self.assertEqual(params['NatPerAz'], 'false') def test_tables_done(self): self.mock_client() self.cf._stack = MagicMock(return_value=self.stack) self.cf.tables(REGIONS) self.assertEqual(self.cf._stack.call_count, len(REGIONS)) self.cloudformation.describe_stacks.assert_not_called() def test_tables_wait(self): self.mock_client() self.cf._stack = MagicMock() self.cf.tables(REGIONS) self.assertEqual(self.cloudformation.describe_stacks.call_count, len(REGIONS)) def test_scheduler_for_regions(self): template = self.cf._scheduler_for_regions(('ap-northeast-1', )) self.assertTrue(template.find('ap-northeast-1') != 1) self.assertTrue(template.find('us-east-1') != 1) def test_schedulers_every(self): self.mock_client() self.cf._stack = MagicMock() regions = { REGION_NAME: { 'scheduler': True, 'scheduler_instance_type': 't2.nano', 'scheduler_coreos_channel': 'stable', 'scheduler_coreos_version': 'current', 'az1': 'us-east-1a', 'az2': 'us-east-1b', 'az3': 'us-east-1c', 'flotilla_container': 'pwagner/flotilla' } } self.cf.schedulers(regions) self.cf._stack.assert_called_with(REGION_NAME, 'flotilla-test-scheduler', self.cf._template('scheduler'), ANY) def test_schedulers_light(self): self.mock_client() self.cf._stack = MagicMock() self.cf._scheduler_for_regions = MagicMock() regions = { REGION_NAME: { 'scheduler': True, 'scheduler_instance_type': 't2.nano', 'scheduler_coreos_channel': 'stable', 'scheduler_coreos_version': 'current', 'az1': 'us-east-1a', 'az2': 'us-east-1b', 'az3': 'us-east-1c', }, 'us-west-2': {} } self.cf.schedulers(regions) self.cf._scheduler_for_regions.assert_called_with(ANY) self.cf._stack.assert_called_with(REGION_NAME, 'flotilla-test-scheduler', ANY, ANY) def test_complete_missing(self): self.assertFalse(self.cf._complete(None, 'foo')) def test_complete_hash_mismatch(self): stack = {'stack_hash': 'bar'} self.assertFalse(self.cf._complete(stack, 'foo')) def test_complete_no_outputs(self): stack = {'stack_hash': 'foo'} self.assertFalse(self.cf._complete(stack, 'foo')) def test_complete(self): stack = {'stack_hash': 'foo', 'outputs': {'foo': 'bar'}} self.assertTrue(self.cf._complete(stack, 'foo')) def test_setup_azs_single(self): template = self._run_setup_azs(Az01='us-east-1a') self.assertNotIn('Az2', template['Parameters']) self.assertNotIn('Az02', template['Parameters']) def test_setup_azs_double(self): template = self._run_setup_azs(Az01='us-east-1a', Az02='us-east-1b') self.assertIn('Az02', template['Parameters']) def test_setup_azs_variable(self): double_template = self._run_setup_azs(Az01='us-east-1a', Az02='us-east-1b') double_resources = len(double_template['Resources']) triple_template = self._run_setup_azs(Az01='us-east-1a', Az02='us-east-1b', Az03='us-east-1c') triple_resources = len(triple_template['Resources']) # With a duplicate AZ (but unique indexes: 1 and 4) quad_template = self._run_setup_azs(Az01='us-east-1a', Az02='us-east-1b', Az03='us-east-1c', Az04='us-east-1a') quad_resources = len(quad_template['Resources']) self.assertEquals(triple_resources - double_resources, quad_resources - triple_resources) def _run_setup_azs(self, **kwargs): template = self.cf._setup_azs(kwargs, self.cf._template('vpc')) return json.loads(template) def mock_client(self): self.cf._client = MagicMock(return_value=self.cloudformation)
class TestFlotillaCloudFormation(unittest.TestCase): def setUp(self): self.cloudformation = MagicMock(spec=CloudFormationConnection) self.stack = MagicMock(spec=Stack) self.stack.stack_status = 'CREATE_COMPLETE' self.cloudformation.describe_stacks.return_value = [self.stack] self.coreos = MagicMock(spec=CoreOsAmiIndex) self.service = { 'service_name': NAME } self.cf = FlotillaCloudFormation(ENVIRONMENT, DOMAIN, self.coreos, backoff=0.001) self.service_template = { 'Parameters': { 'PublicSubnet01': { }, 'PrivateSubnet01': { } }, 'Resources': { 'PublicSubnet01': { }, 'PrivateSubnet01': { }, 'Elb': { 'Properties': { 'Subnets': [ ] } }, 'Asg': { 'Properties': { 'VPCZoneIdentifier': [ ] } } } } @patch('boto.cloudformation.connect_to_region') def test_client_cache(self, mock_connect): mock_connect.return_value = self.cloudformation self.assertEqual(0, len(self.cf._clients)) self.cf._client(REGION_NAME) self.assertEqual(1, len(self.cf._clients)) self.assertEqual(self.cloudformation, self.cf._clients[REGION_NAME]) def test_service_hash(self): service_hash = self.cf._service_hash(self.service, {'foo': 'bar'}) self.assertIsNotNone(service_hash) def test_service_hash_string_fields(self): hash_base = self.cf._service_hash(self.service, {'foo': 'bar'}) self.service['instance_type'] = 't2.micro' hash_with_string = self.cf._service_hash(self.service, {'foo': 'bar'}) self.assertNotEqual(hash_base, hash_with_string) def test_service_hash_iterable_fields(self): hash_base = self.cf._service_hash(self.service, {'foo': 'bar'}) self.service['regions'] = ['us-east-1', 'us-west-2'] hash_with_list = self.cf._service_hash(self.service, {'foo': 'bar'}) self.assertNotEqual(hash_base, hash_with_list) self.service['regions'] = ['us-west-2', 'us-east-1'] hash_with_list_order = self.cf._service_hash(self.service, {'foo': 'bar'}) self.assertNotEqual(hash_base, hash_with_list_order) self.assertEqual(hash_with_list, hash_with_list_order) def test_stack_does_not_exists(self): self.mock_client() not_found = BotoServerError(400, 'Not Found') self.cloudformation.describe_stacks.side_effect = not_found self.cloudformation.create_stack.return_value = STACK_ARN stack = self.cf._stack(REGION_NAME, NAME, TEMPLATE, {}) self.cloudformation.create_stack. \ assert_called_with(NAME, capabilities=CAPABILITIES, template_body=TEMPLATE, parameters=ANY) self.cloudformation.update_stack.assert_not_called() self.assertEqual(stack.stack_id, STACK_ARN) def test_stack_existing_in_progress(self): self.mock_client() self.stack.stack_status = 'CREATE_IN_PROGRESS' stack = self.cf._stack(REGION_NAME, NAME, '{}', {}) self.assertEqual(stack.stack_status, 'CREATE_IN_PROGRESS') self.cloudformation.create_stack.assert_not_called() self.cloudformation.update_stack.assert_not_called() def test_stack_existing_update(self): self.mock_client() self.cloudformation.update_stack.return_value = STACK_ARN stack = self.cf._stack(REGION_NAME, NAME, TEMPLATE, {}) self.cloudformation.create_stack.assert_not_called() self.cloudformation.update_stack. \ assert_called_with(NAME, capabilities=CAPABILITIES, template_body=TEMPLATE, parameters=ANY) self.assertEqual(stack.stack_id, STACK_ARN) def test_stack_existing_update_exception(self): self.mock_client() unknown_error = BotoServerError(400, 'Unknown error') self.cloudformation.update_stack.side_effect = unknown_error self.assertRaises(BotoServerError, self.cf._stack, REGION_NAME, NAME, TEMPLATE, {}) def test_stack_existing_update_noop(self): self.mock_client() no_updates = BotoServerError(400, 'Unknown error', body='<Message>No updates are to be' ' performed.</Message>') self.cloudformation.update_stack.side_effect = no_updates stack = self.cf._stack(REGION_NAME, NAME, TEMPLATE, {}) self.assertEqual(stack, self.stack) def test_service(self): self.cf._stack = MagicMock() self.cf.service(REGION, self.service, {}, None) self.cf._stack.assert_called_with(REGION_NAME, SERVICE_STACK, ANY, ANY) def test_service_complete(self): self.cf._complete = MagicMock(return_value=True) service = self.cf.service(REGION, self.service, {}, None) self.assertIsNone(service) def test_service_public_ports(self): self.cf._stack = MagicMock() self.service['public_ports'] = {'9200': 'HTTP'} self.cf.service(REGION, self.service, {}, None) self.cf._stack.assert_called_with(REGION_NAME, SERVICE_STACK, ANY, ANY) def test_service_private_ports(self): self.cf._stack = MagicMock() self.service['private_ports'] = {'9300': ['TCP']} self.cf.service(REGION, self.service, {}, None) self.cf._stack.assert_called_with(REGION_NAME, SERVICE_STACK, ANY, ANY) def test_service_params_generate_dns(self): stack_params = self.cf._service_params(REGION, self.service, {}, self.service_template) self.assertEqual(stack_params['VirtualHostDomain'], DOMAIN + '.') self.assertEqual(stack_params['VirtualHost'], 'service-test.test.com') def test_service_params_dns_parameter(self): self.service['dns_name'] = 'testapp.test.com' stack_params = self.cf._service_params(REGION, self.service, {}, self.service_template) self.assertEqual(stack_params['VirtualHostDomain'], DOMAIN + '.') self.assertEqual(stack_params['VirtualHost'], 'testapp.test.com') def test_service_params_custom_container(self): region = REGION.copy() region['flotilla_container'] = 'pwagner/flotilla' stack_params = self.cf._service_params(region, self.service, {}, self.service_template) self.assertEqual(stack_params['FlotillaContainer'], 'pwagner/flotilla') def test_service_params_subnets(self): self.cf._service_params(REGION, self.service, { 'PrivateSubnet01': 'subnet-123456', 'PrivateSubnet02': 'subnet-654321', 'PublicSubnet01': 'subnet-234561', 'PublicSubnet02': 'subnet-165432', }, self.service_template) self.assertEqual(4, len(self.service_template['Parameters'])) def test_vpc(self): self.cf._stack = MagicMock() self.cf.vpc({'region_name': REGION_NAME}, None) self.cf._stack.assert_called_with(REGION_NAME, 'flotilla-test-vpc', ANY, ANY) def test_vpc_complete(self): self.cf._complete = MagicMock(return_value=True) vpc = self.cf.vpc({'region_name': REGION_NAME}, None) self.assertIsNone(vpc) def test_vpc_params_empty(self): params = self.cf._vpc_params({'region_name': REGION_NAME}) self.assertEquals(params['FlotillaEnvironment'], ENVIRONMENT) self.assertEquals(params['BastionInstanceType'], 't2.nano') self.assertIn('BastionAmi', params) self.assertTrue('FlotillaContainer' not in params) def test_vpc_params_container(self): params = self.cf._vpc_params({'region_name': REGION_NAME, 'flotilla_container': 'pwagner/flotilla'}) self.assertEqual(params['FlotillaContainer'], 'pwagner/flotilla') def test_vpc_params_az(self): params = self.cf._vpc_params({'region_name': REGION_NAME, 'az1': 'us-east-1a', 'az2': 'us-east-1b'}) self.assertEqual(params['Az01'], 'us-east-1a') self.assertEqual(params['Az02'], 'us-east-1b') def test_vpc_params_nat_per_az(self): params = self.cf._vpc_params({'region_name': REGION_NAME, 'nat_per_az': 'true'}) self.assertEqual(params['NatPerAz'], 'true') def test_vpc_params_nat_per_az_invalid(self): params = self.cf._vpc_params({'region_name': REGION_NAME, 'nat_per_az': 'meow'}) self.assertEqual(params['NatPerAz'], 'false') def test_tables_done(self): self.mock_client() self.cf._stack = MagicMock(return_value=self.stack) self.cf.tables(REGIONS) self.assertEqual(self.cf._stack.call_count, len(REGIONS)) self.cloudformation.describe_stacks.assert_not_called() def test_tables_wait(self): self.mock_client() self.cf._stack = MagicMock() self.cf.tables(REGIONS) self.assertEqual(self.cloudformation.describe_stacks.call_count, len(REGIONS)) def test_scheduler_for_regions(self): template = self.cf._scheduler_for_regions(('ap-northeast-1',)) self.assertTrue(template.find('ap-northeast-1') != 1) self.assertTrue(template.find('us-east-1') != 1) def test_schedulers_every(self): self.mock_client() self.cf._stack = MagicMock() regions = { REGION_NAME: { 'scheduler': True, 'scheduler_instance_type': 't2.nano', 'scheduler_coreos_channel': 'stable', 'scheduler_coreos_version': 'current', 'az1': 'us-east-1a', 'az2': 'us-east-1b', 'az3': 'us-east-1c', 'flotilla_container': 'pwagner/flotilla' } } self.cf.schedulers(regions) self.cf._stack.assert_called_with(REGION_NAME, 'flotilla-test-scheduler', self.cf._template('scheduler'), ANY) def test_schedulers_light(self): self.mock_client() self.cf._stack = MagicMock() self.cf._scheduler_for_regions = MagicMock() regions = { REGION_NAME: { 'scheduler': True, 'scheduler_instance_type': 't2.nano', 'scheduler_coreos_channel': 'stable', 'scheduler_coreos_version': 'current', 'az1': 'us-east-1a', 'az2': 'us-east-1b', 'az3': 'us-east-1c', }, 'us-west-2': {} } self.cf.schedulers(regions) self.cf._scheduler_for_regions.assert_called_with(ANY) self.cf._stack.assert_called_with(REGION_NAME, 'flotilla-test-scheduler', ANY, ANY) def test_complete_missing(self): self.assertFalse(self.cf._complete(None, 'foo')) def test_complete_hash_mismatch(self): stack = {'stack_hash': 'bar'} self.assertFalse(self.cf._complete(stack, 'foo')) def test_complete_no_outputs(self): stack = {'stack_hash': 'foo'} self.assertFalse(self.cf._complete(stack, 'foo')) def test_complete(self): stack = {'stack_hash': 'foo', 'outputs': {'foo': 'bar'}} self.assertTrue(self.cf._complete(stack, 'foo')) def test_setup_azs_single(self): template = self._run_setup_azs(Az01='us-east-1a') self.assertNotIn('Az2', template['Parameters']) self.assertNotIn('Az02', template['Parameters']) def test_setup_azs_double(self): template = self._run_setup_azs(Az01='us-east-1a', Az02='us-east-1b') self.assertIn('Az02', template['Parameters']) def test_setup_azs_variable(self): double_template = self._run_setup_azs(Az01='us-east-1a', Az02='us-east-1b') double_resources = len(double_template['Resources']) triple_template = self._run_setup_azs(Az01='us-east-1a', Az02='us-east-1b', Az03='us-east-1c') triple_resources = len(triple_template['Resources']) # With a duplicate AZ (but unique indexes: 1 and 4) quad_template = self._run_setup_azs(Az01='us-east-1a', Az02='us-east-1b', Az03='us-east-1c', Az04='us-east-1a') quad_resources = len(quad_template['Resources']) self.assertEquals(triple_resources - double_resources, quad_resources - triple_resources) def _run_setup_azs(self, **kwargs): template = self.cf._setup_azs(kwargs, self.cf._template('vpc')) return json.loads(template) def mock_client(self): self.cf._client = MagicMock(return_value=self.cloudformation)