"""Helper classes for ETL integration tests.""" from copy import deepcopy from datetime import datetime from datetime import timedelta import importlib from multiprocessing import Process import time import unittest from boto.exception import SWFResponseError import boto.swf.layer1 as swf from flows import datastore from flows import util import flows.config class BaseTestCase(unittest.TestCase): """Conventional scaffolding for ETL integration tests.""" # required to change in subclasses as flow module name flow_name = None # SWF context to send to test with context = {} # optional flow module config override values flow_config_override = {} # swf execution timeout for tests swf_execution_timeout = 1200 # changes for these not needed in subclasses flow_config_original = {} flows_config_original = {} swf_domain = 'dev' @classmethod def setUpClass(cls): """Set up and run the ETL before any test(s) are executed. Explicit test subclasses can override the class method calls inside such as BaseTestCase.setup_environment() and BaseTestCase.monkey_patch_config() and call parent methods inside to fine tune test setups. """ if cls.flow_name is None: raise AttributeError('Missing flow_name for integration test.') cls.timestamp = datetime.now().strftime('%Y%m%d%H%M%S') cls.correlation_id = 'integration-test-{ts}'.format(ts=cls.timestamp) cls.database_ft = 'test_{timestamp}_film_transparency'.format( timestamp=cls.timestamp) cls.database_ar = 'test_{timestamp}_art_relations'.format( timestamp=cls.timestamp) flow_config_path = 'flows.{flow}.config'.format(flow=cls.flow_name) cls.flow_config = importlib.import_module(flow_config_path) cls.monkey_patch_config() flow_cli_path = 'flows.{flow}.cli'.format(flow=cls.flow_name) cls.flow_cli = importlib.import_module(flow_cli_path) cls.setup_environment() cls.execute_flow() @classmethod def tearDownClass(cls): """Cleanup.""" cls.terminate_flow() cls.undo_setup_environment() cls.undo_monkey_patch_config() @classmethod def monkey_patch_config(cls): """Change ETL configs to avoid namespace collisions.""" cls.flow_config_original = { 'SWF_WORKFLOW_NAME': cls.flow_config.SWF_WORKFLOW_NAME, 'SWF_WORKFLOW_TIMEOUT': cls.flow_config.SWF_WORKFLOW_TIMEOUT} cls.flow_config.SWF_WORKFLOW_NAME = 'test_{name}'.format( name=cls.flow_config.SWF_WORKFLOW_NAME) cls.flow_config.SWF_WORKFLOW_TIMEOUT = cls.swf_execution_timeout for attribute, value in cls.flow_config_override.items(): cls.flow_config_original[attribute] = getattr( cls.flow_config, attribute) setattr(cls.flow_config, attribute, value) cls.flows_config_original = deepcopy(flows.config) flows.config.ENVIRONMENT = flows.config.TEST_ENVIRONMENT flows.config.DATASTORE_CREDENTIALS['db'] = cls.database_ft flows.config.ART_RELATIONS_CREDENTIALS = deepcopy( flows.config.DATASTORE_CREDENTIALS) flows.config.ART_RELATIONS_CREDENTIALS['db'] = cls.database_ar @classmethod def undo_monkey_patch_config(cls): """Return the ETL configs.""" for attribute, value in cls.flow_config_original: setattr(cls.flow_config, attribute, value) flows.config = deepcopy(cls.flows_config_original) @classmethod def setup_environment(cls): """Create the database and environments needed for the ETL.""" create = 'CREATE SCHEMA IF NOT EXISTS {database}'.format( database=cls.database_ft) datastore.execute(create) use = 'USE {database}'.format(database=cls.database_ft) datastore.execute(use) cls.decider = Process(target=cls.flow_cli.run_decider) cls.decider.start() # let decider register tasklists, etc. before worker tries to hook in time.sleep(5) cls.worker = Process(target=cls.flow_cli.run_worker) cls.worker.start() @classmethod def undo_setup_environment(cls): """Cleanup any database and environment setups.""" cls.worker.terminate() cls.decider.terminate() drop_ft = 'DROP SCHEMA IF EXISTS {database}'.format( database=cls.database_ft) datastore.execute(drop_ft) drop_ar = 'DROP SCHEMA IF EXISTS {database}'.format( database=cls.database_ar) datastore.execute(drop_ar) @classmethod def execute_flow(cls): """Execute workflow.""" cls.terminate_flow() cls.context['correlation_id'] = cls.correlation_id run_response = util.start_swf_execution( cls.context, cls.flow_config.SWF_WORKFLOW_TIMEOUT, cls.flow_config.SWF_WORKFLOW_NAME, cls.flow_config.SWF_WORKFLOW_VERSION, cls.flow_config.SWF_WORKFLOW_NAME) run_id = run_response['runId'] # wait for workflow execution end start_time = datetime.now() max_wait = timedelta(seconds=cls.flow_config.SWF_WORKFLOW_TIMEOUT) while datetime.now() - start_time < max_wait: execution = swf.Layer1().describe_workflow_execution( domain=cls.swf_domain, workflow_id=cls.flow_config.SWF_WORKFLOW_NAME, run_id=run_id) if execution['executionInfo'].get('executionStatus') == 'CLOSED': return time.sleep(10) raise TimeoutError('Flow execution timeout') @classmethod def terminate_flow(cls): """Cleanup the workflow execution in SWF.""" try: swf.Layer1().terminate_workflow_execution( cls.swf_domain, cls.flow_config.SWF_WORKFLOW_NAME) except SWFResponseError: # if no flow with the same id running pass