automation/tincrepo/main/mpp/models/sql_tc.py (603 lines of code) (raw):

import new import os import re import sys import shutil import unittest2 as unittest from fnmatch import fnmatch import tinctest from tinctest import TINCTestLoader from tinctest.case import _TINCProductVersion from tinctest.lib import Gpdiff from tinctest.lib.system import TINCSystem from qautils.gppylib.gparray import GpArray from qautils.gppylib.db.dbconn import DbURL from qautils.gppylib.commands.gp import GpStop, GpStart from mpp.lib.PSQL import PSQL from mpp.lib.gpConfig import GpConfig from mpp.models import MPPTestCase from mpp.models.mpp_tc import _MPPTestCaseResult @tinctest.dataProvider('optimizer_handling') def optimizer_handling_data_provider(): data = {'planner': 'off', 'orca': 'on'} return data @tinctest.skipLoading("Test model. No tests loaded.") class SQLTestCase(MPPTestCase): """ SQLTestCase consumes a SQL file and expected output, performs the psql, and returns success/failure based on the ensuing gpdiff. @metadata: gucs: gucs in the form of key=value pairs separated by ';' @metadata: orcagucs: ORCA specific gucs @metadata: optimizer_mode: Optimizer mode to run the sql test case with. Valid values are on, off, both, None. @metadata: gpdiff: If set to true, the output is matched with expected output and the test is failed if there is a difference. If set to false, the output is not matched with expected output (default: True) """ # Relative path w.r.t test module, where to look for sql files and ans files. # Sub-classes can override this to specify a different location. #: Relative path w.r.t test module, where to look for sql files sql_dir = '' #: Relative path w.r.t test module, where to look for sql files ans_dir = '' #: Class variable to determine if ans file have to be generated. #: Valid values: yes, no, force. Default is 'no'. generate_ans = 'no' # Class variable to keep track of valid values of generate_ans _valid_generate_ans_values = ['yes', 'no', 'force'] #: Class variable to specify the name of the template directory in the sql directory template_dir = 'template' #: Class variable dictionary to store substitution. Key is the original. Value is the substitution string. template_subs = dict() # Class variable (hidden) to determine if there are sql template. This is determined automatically. _template_exist = False # global optimizer mode, as set in postgresql.conf _global_optimizer_mode = None @classmethod def setUpClass(cls): """ SQLTestCase's setUpClass is responsible for the following: -> If a 'setup' directory exists in the cls's sql_dir, all the sqls within that directory are executed against the database configured for this test class. Note that this database should be configured through the metadata 'db_name' in the class' docstring. """ tinctest.logger.trace_in() # Call superclass setup first super(SQLTestCase, cls).setUpClass() cls._global_optimizer_mode = cls.get_global_optimizer_mode() setup_dir = os.path.join(cls.get_sql_dir(), 'setup') setup_out_dir = os.path.join(cls.get_out_dir(), 'setup') if os.path.exists(setup_dir): TINCSystem.make_dirs(setup_out_dir, ignore_exists_error = True) tinctest.logger.info("Running the setup directory sqls for the test class: %s" %cls.__name__) for setup_sql_file in os.listdir(setup_dir): if setup_sql_file.endswith('.sql'): out_file = os.path.join(setup_out_dir, setup_sql_file.replace('.sql', '.out')) setup_sql_file = os.path.join(setup_dir, setup_sql_file) tinctest.logger.info("Running setup sql for test - %s" %setup_sql_file) PSQL.run_sql_file(setup_sql_file, dbname=cls.db_name, out_file=out_file) # TODO: Should we support gpdiff for setup sqls ? tinctest.logger.trace_out() @classmethod def get_global_optimizer_mode(cls): # TODO: May be , do all this once in MPPTestCase instead of for every test class # if we expect tests to not change certain configurations in postgresql.conf gpconfig = GpConfig() try: (master, segment) = gpconfig.getParameter('optimizer') except Exception, e: tinctest.logger.error("Failed to retrive optimizer mode") tinctest.logger.exception(e) return None return master @classmethod def get_sql_dir(cls): """ Returns the absolute directory path for the sql directory configured for this class. Computed relative to where the test class exists. @rtype: string @return: Absolute directory path of the class' sql directory, if template are not found. Else, return out_dir/sql_dir """ tinctest.logger.trace_in() source_dir = os.path.dirname(sys.modules[cls.__module__].__file__) return_sql_dir = os.path.join(source_dir, cls.sql_dir) if cls._template_exist: out_dir = cls.get_out_dir() return_sql_dir = os.path.join(out_dir, cls.__name__, cls.sql_dir) tinctest.logger.trace_out("return_sql_dir: %s" % return_sql_dir) return return_sql_dir @classmethod def get_ans_dir(cls): """ Returns the absolute directory path for the ans directory configured for this class. Computed relative to where the test class exists. @rtype: string @return: Absolute directory path of the class' ans directory, if template are not found. Else, return out_dir/ans_dir """ tinctest.logger.trace_in() source_dir = os.path.dirname(sys.modules[cls.__module__].__file__) return_ans_dir = os.path.join(source_dir, cls.ans_dir) if cls._template_exist: out_dir = cls.get_out_dir() return_ans_dir = os.path.join(out_dir, cls.__name__, cls.ans_dir) tinctest.logger.trace_out("return_ans_dir: %s" % return_ans_dir) return return_ans_dir @classmethod def tearDownClass(cls): """ SQLTestCase's tearDownClass is responsible for the following: -> If a 'teardown' directory exists in the cls's sql_dir, all the sqls within that directory are exectued against the database configured for this test class. Note that this database should be configured through the metadata 'db_name' in the class' docstring. """ tinctest.logger.trace_in() teardown_dir = os.path.join(cls.get_sql_dir(), 'teardown') teardown_out_dir = os.path.join(cls.get_out_dir(), 'teardown') if os.path.exists(teardown_dir): TINCSystem.make_dirs(teardown_out_dir, ignore_exists_error = True) tinctest.logger.info("Running the teardown directory sqls for the test class: %s" %cls.__name__) for teardown_sql_file in os.listdir(teardown_dir): if teardown_sql_file.endswith('.sql'): out_file = os.path.join(teardown_out_dir, teardown_sql_file.replace('.sql', '.out')) teardown_sql_file = os.path.join(teardown_dir, teardown_sql_file) tinctest.logger.info("Running teardown sql for test - %s" %teardown_sql_file) PSQL.run_sql_file(teardown_sql_file, dbname=cls.db_name, out_file=out_file) # TODO: Should we support gpdiff for teardown sqls ? # Call super class teardown super(SQLTestCase, cls).tearDownClass() tinctest.logger.trace_out() @classmethod def _find_tests(cls, directory): """ This definition finds the sql files in the directory specified. Then, it calls init of the object that creates the implicit test method. Following are the sqls that will belong to one test case for which we generate a test method dynamically: (from within the SQLTestCase constructor) test1.sql - main test sql test1_part1.sql, test1_part2.sql etc - supplementary sqls that will be run along with the main test sql sequentially. test1_setup.sql - Will be run as a part of the setup test1_teardown.sql - Will be run as a part of the teardown. setup.sql - Will be run as a part of the setup before test1_setup.sql teardown.sql - Will be run as a part of the teardown after test1_teardown.sql """ tinctest.logger.trace_in("directory: %s" % directory) tests = [] for filename in os.listdir(directory): if not fnmatch(filename, "*.sql"): continue sql_file = os.path.join(directory, filename) # Ignore setup and teardown sqls if fnmatch(filename, 'setup.sql') or fnmatch(filename, 'teardown.sql'): tinctest.logger.debug("Ignoring setup or teardown sql - %s" %sql_file) continue if re.match(".*_part\d+.sql", sql_file): tinctest.logger.debug("Ignoring part sql %s" %sql_file) continue if re.search(".*_setup.sql", sql_file): tinctest.logger.debug("Ignoring test setup sql %s" %sql_file) continue if re.match(".*_teardown.sql", sql_file): tinctest.logger.debug("Ignoring test teardown sql %s" %sql_file) continue partial_test_name = filename[:-4] # Test name is test_<name of the sql file without the extension> # query01.sql will be test_query91 # template_query01.sql will be test_template_query01 test_name = TINCTestLoader.testMethodPrefix + partial_test_name # The constructor is responsible for generating this test method dynamically if it doesn't exist tinctest.logger.debug("Trying to add test %s to dynamic tests list" %test_name) try: test_instance = cls(test_name) tinctest.logger.debug("Added test case %s to dynamic tests list" % test_name) except Exception, e: tinctest.logger.error("Couldn't add test case %s to dynamic tests list" % test_name) tinctest.logger.error(e) test_instance = tinctest.loader.make_failed_test(None, cls.__module__ + "." + cls.__name__ + "." + test_name, e) finally: tests.append(test_instance) tinctest.logger.trace_out("tests: (as reported above)") return tests @classmethod def handle_templates(cls): """ This class method checks if any template directory exists. If they do, copy/parse all the sql and ans directories to out_dir. @rtype: boolean @return: Return True, if templates are found. False, otherwise """ # Variables to store non-template directories sql_dir = cls.get_sql_dir() sql_setup_dir = os.path.join(sql_dir, "setup") sql_teardown_dir = os.path.join(sql_dir, "teardown") ans_dir = cls.get_ans_dir() ans_setup_dir = os.path.join(ans_dir, "setup") ans_teardown_dir = os.path.join(ans_dir, "teardown") # Variables to store template directories sql_template_dir = os.path.join(sql_dir, cls.template_dir) sql_setup_template_dir = os.path.join(sql_setup_dir, cls.template_dir) sql_teardown_template_dir = os.path.join(sql_teardown_dir, cls.template_dir) ans_template_dir = os.path.join(ans_dir, cls.template_dir) ans_setup_template_dir = os.path.join(ans_setup_dir, cls.template_dir) ans_teardown_template_dir = os.path.join(ans_teardown_dir, cls.template_dir) # Define a set for all template dir # Check if any exists, while creating a set. template_dir_set = set([sql_template_dir, sql_setup_template_dir, sql_teardown_template_dir, ans_template_dir, ans_setup_template_dir, ans_teardown_template_dir]) for each_dir in template_dir_set: # Check if any of the template directory exists. tinctest.logger.debug("Checking if directory %s exists..." % each_dir) if os.path.exists(each_dir): cls._template_exist = True break if not cls._template_exist: # Nothing to do tinctest.logger.debug("No template directories found.") return False #################################################### # If here, we have at least one template directory # #################################################### # Define local variables to store all the out directories # SQL files go in out/sql directory, and ans file go in out/ans directory out_dir = cls.get_out_dir() out_sql_dir = os.path.join(out_dir, cls.__name__, cls.sql_dir) out_sql_setup_dir = os.path.join(out_dir, cls.__name__, cls.sql_dir, "setup") out_sql_teardown_dir = os.path.join(out_dir, cls.__name__, cls.sql_dir, "teardown") out_ans_dir = os.path.join(out_dir, cls.__name__, cls.ans_dir) out_ans_setup_dir = os.path.join(out_dir, cls.__name__, cls.ans_dir, "setup") out_ans_teardown_dir = os.path.join(out_dir, cls.__name__, cls.ans_dir, "teardown") out_dir_set = sorted(set([out_sql_dir, out_sql_setup_dir, out_sql_teardown_dir, out_ans_dir, out_ans_setup_dir, out_ans_teardown_dir])) tinctest.logger.debug("Templates are found. Copy or parse sql files/dirs to directory %s. Copy or parse ans files/dirs to directory %s" % (out_sql_dir, out_ans_dir)) for each_dir in out_dir_set: if os.path.exists(each_dir): tinctest.logger.warning("Directory %s already exists. Deleting it to start from scratch. Use different output directory to avoid this behavior!" % each_dir) shutil.rmtree(each_dir, ignore_errors = True) TINCSystem.make_dirs(each_dir, ignore_exists_error = True) # Define several sets to store all the sql dirs, ans dirs, setup dirs, etc. # Define sets for all sql and ans dirs sql_dir_set = set([sql_dir, sql_setup_dir, sql_teardown_dir, sql_template_dir, sql_setup_template_dir, sql_teardown_template_dir]) ans_dir_set = set([ans_dir, ans_setup_dir, ans_teardown_dir, ans_template_dir, ans_setup_template_dir, ans_teardown_template_dir]) # Define sets for all setup and teardown dirs setup_dir_set = set([sql_setup_dir, ans_setup_dir, sql_setup_template_dir, ans_setup_template_dir]) teardown_dir_set = set([sql_teardown_dir, ans_teardown_dir, sql_teardown_template_dir, ans_teardown_template_dir]) # Define a set for all dirs as union of sql and ans dir dir_set = sql_dir_set.union(ans_dir_set) # Type of files that will be copied or parsed file_types = ["*.sql", "*.ans", "*.ans.orca", "*.ans.planner"] # Go through each directory to either copy it or parse it for each_dir in dir_set: if not os.path.exists(each_dir): continue # SQL dir goes to out_dir/sql; ANS dir goes to out_dir/ans destination_dir = out_sql_dir if each_dir in ans_dir_set: destination_dir = out_ans_dir # Setup and Teardown files go in out/sql/setup, out/ans/teardown, etc. if each_dir in setup_dir_set: destination_dir = os.path.join(destination_dir, 'setup') elif each_dir in teardown_dir_set: destination_dir = os.path.join(destination_dir, 'teardown') # If template directory, parse it. Else, copy it. if each_dir in template_dir_set: tinctest.logger.debug("Directory %s is a template directory. Parse it and save the results in %s." % (each_dir, destination_dir)) TINCSystem.substitute_strings_in_directory(each_dir, destination_dir, cls.template_subs, file_types, destination_file_prefix = "template_") else: tinctest.logger.debug("Directory %s is not a template directory. Copy its sql and ans files as is in %s." % (each_dir, destination_dir)) TINCSystem.copy_directory(each_dir, destination_dir, file_types) return True @classmethod def _check_generate_ans(cls): cls.generate_ans = cls.generate_ans.lower() # Check that generate_ans value is valid: 'yes', 'no', or 'force' if cls.generate_ans not in cls._valid_generate_ans_values: raise SQLTestCaseException("Invalid value for generate_ans specified: %s. It should be one of the following: %s" % (cls.generate_ans, ', '.join(cls._valid_generate_ans_values))) @classmethod def loadTestsFromTestCase(cls): """ This method customizes the loading of test cases for SQLTestCase. Generates test cases for all the test sql files within the sql directory of the test class. If templates are found, copy the sql files and the parsed out template files into out_dir. @rtype: list @return: List of SQLTestCase instances constructed for sql files in the sql directory """ tinctest.logger.trace_in() # Check if generate_ans value is valid cls._check_generate_ans() # If template exists, copy/parse all the directories to out_dir cls.handle_templates() # Call get_sql_dir only after handling templates. It could point to out_dir, if templates exist. sql_dir = cls.get_sql_dir() # Time to browse directory and find tests tinctest.logger.debug("Finding tests in sql_dir %s..." %(sql_dir)) tests = cls._find_tests(sql_dir) tinctest.logger.trace_out("tests: (as reported above)") return tests def __init__(self, methodName, baseline_result = None, sql_file=None, db_name = None): """ This is an unconventional constructor. By design, the methodName may be implicit and may not yet exist in the class definition of whomever is subclassing SQLTestCase. So, our approach is to discover the intended test method and dynamically generate it; then, we will defer to traditional construction in the parent. """ # Test case metadata self.gucs = None self.orcagucs = None self.optimizer_mode = None self.gpdiff = True self._optimizer_mode_values = ['on', 'off', 'both'] # To track the current execution mode self._current_optimizer_mode = None # Default gucs to be added when optimizer is on self._optimizer_gucs = ['optimizer_log=on'] self.sql_file = sql_file self.ans_file = None # Paths to original sql file and ans file of the test. # When a test is generated from a template, self.sql_file and self.ans_file will point # to the generated sql and ans file through out this class. This maintains a ref to # the original sql file and ans file of the test case. self._original_sql_file = self.sql_file self._original_ans_file = self.ans_file # if the test method is explicit and already defined, construction is trivial if methodName.startswith(tinctest.TINCTestLoader.testMethodPrefix) and \ hasattr(self.__class__, methodName) and \ hasattr(getattr(self.__class__, methodName), '__call__'): super(SQLTestCase, self).__init__(methodName) return # otherwise, do dynamic test generation self._generate_test_method_dynamically(methodName, sql_file) super(SQLTestCase, self).__init__(methodName, baseline_result) def _generate_test_method_dynamically(self, methodName, sql_file): """ Given a test method name and a sql file for which the test method is to be constructed, this method generates and adds the test method dynamically to the class. Assumptions: Assume test method name is the name of the sql file without the extension prefixed by 'test_'. For eg: a sql file query01.sql, we will generate a test method 'test_query01' TODO: This should also be provided as a service at TINCTestCase level so that sub-classes can leverage dynamic test method construction if they are customizing the loading of tests. """ assert methodName.startswith(tinctest.TINCTestLoader.testMethodPrefix) partial_test_name = methodName[len(tinctest.TINCTestLoader.testMethodPrefix):] # implicit sql tests are generated from *.sql/*.ans files # found in the current working directory # To enable tinc_client to construct a test case for a specific sql file # TODO: This piece of code gets executed only through tincdb. Needs a cleanup later. if sql_file is not None: self.sql_file = sql_file partial_file_name = os.path.basename(sql_file)[:-4] # Order in which ans files are located # 1. Same as sql file location. 2. sql_file/../expected/ self.ans_file = os.path.join(os.path.dirname(sql_file), "%s.ans" %partial_file_name) if not os.path.exists(self.ans_file): ans_dir = os.path.join(self.get_source_dir(), self.__class__.ans_dir) self.ans_file = os.path.join(ans_dir, "%s.ans" %partial_file_name) if not os.path.exists(self.ans_file): self.ans_file = os.path.join(os.path.dirname(sql_file), "../expected/", "%s.ans" %partial_file_name) else: # Normal execution (sql_file is None) if not self.__class__._template_exist and partial_test_name.startswith("template_"): # Dealing with templates that are not parsed. # Call class method handle_templates to move sql/ans to out_dir self.handle_templates() if self.__class__._template_exist and partial_test_name.startswith("template_"): base_file = partial_test_name[len("template_"):] self._original_sql_file = os.path.join(os.path.dirname(sys.modules[self.__class__.__module__].__file__), self.__class__.sql_dir, self.__class__.template_dir, "%s.sql" % base_file) self._original_ans_file = os.path.join(os.path.dirname(sys.modules[self.__class__.__module__].__file__), self.__class__.ans_dir, self.__class__.template_dir, "%s.ans" % base_file) else: self._original_sql_file = os.path.join(os.path.dirname(sys.modules[self.__class__.__module__].__file__), self.__class__.sql_dir, "%s.sql" % partial_test_name) self._original_ans_file = os.path.join(os.path.dirname(sys.modules[self.__class__.__module__].__file__), self.__class__.ans_dir, "%s.ans" % partial_test_name) # At this point, regular non-template sql files would assume that the sql file is the one in sql_dir # For template sql file, get_sql_dir will automatically point to the one in out_dir self.sql_file = os.path.join(self.get_sql_dir(), "%s.sql" % partial_test_name) self.ans_file = os.path.join(self.get_ans_dir(), "%s.ans" % partial_test_name) tinctest.logger.debug("sql_file: %s", self.sql_file) tinctest.logger.debug("ans_file: %s", self.ans_file) intended_docstring = self._read_metadata_as_docstring() # this is the dynamic test function we will bind into the # generated test intance. (note the use of closures!) def implied_test_function(my_self): # This used to be an assert on the return value from run_test # By default we fail when there are diffs, however some sub-classes # have overriden this method and are returning False when there is a # failure result = my_self.run_test() if result is not None: my_self.assertTrue(result) implied_test_function.__doc__ = intended_docstring method = new.instancemethod(implied_test_function, self, self.__class__) self.__dict__[methodName] = method def _read_metadata_as_docstring(self): """ pull out the intended docstring from the implied sql file parent instantiation will conveniently look to that docstring to glean metadata. Assumptions: Assume that the metadata is given as comments in the sql file at the top. TODO: Provide this as an API at TINCTestCase level so that every class can implement a custom way of reading metadata. """ intended_docstring = "" with open(self.sql_file, 'r') as f: for line in f: line = line.strip() if line.find('--') != 0: break intended_docstring += line[2:].strip() intended_docstring += "\n" return intended_docstring def handle_optimizer_mode_metadata(self): # metadata optimizer_mode if self._metadata.get('optimizer_mode', None): if not self._metadata.get('optimizer_mode').lower() in self._optimizer_mode_values: self.load_fail(SQLTestCaseException, "Invalid metadata specified for optimizer_mode: %s. Valid values are %s" %(self._metadata.get('optimizer_mode'), self._optimizer_mode_values)) self.optimizer_mode = self._metadata.get('optimizer_mode').lower() if self.optimizer_mode == "both": if self.data_provider: self.data_provider += " optimizer_handling" else: self.data_provider = "optimizer_handling" def _infer_metadata(self): super(SQLTestCase, self)._infer_metadata() # metadata gpdiff if self._metadata.get('gpdiff') and self._metadata.get('gpdiff').lower() == 'false': self.gpdiff = False # metadata gucs if self._metadata.get('gucs', None) == None: self.gucs = set() else: self.gucs = set(self._metadata['gucs'].split(';')) # add metadata orcagucs for orca related gucs 'select disable_xform(XXXX)' if self._metadata.get('orcagucs', None) == None: self.orcagucs = set() else: self.orcagucs = set(self._metadata['orcagucs'].split(';')) self.handle_optimizer_mode_metadata() def setUp(self): """ Runs the setup sql for the test case. For a test sql 'query01.sql', this will run setup.sql and query01_setup.sql in that order if it exists in the same location as the test sql file. """ tinctest.logger.trace_in() super(SQLTestCase, self).setUp() if self.sql_file is not None: self._run_setup_sql() tinctest.logger.trace_out() def _run_setup_sql(self): """ Run test case specific setup sql file.If 'setup.sql' exists in the same location as the sql file, run that first and then run <test_sql_file>_setup.sql if it exists. For a test sql 'query01.sql', this will run setup.sql and query01_setup.sql in that order if it exists in the same location as the test sql file. TODO: Incorporate ans file checks for setup sqls if they exist. """ # Check if a common setup.sql file exists in the same location as the test sql setup_sql_file = os.path.join(os.path.dirname(self.sql_file), 'setup.sql') if os.path.exists(setup_sql_file): tinctest.logger.info("Running setup sql for test - %s" %setup_sql_file) self._run_and_verify_sql_file(setup_sql_file) test_case_setup_sql_file = self.sql_file.replace('.sql', '_setup.sql') if os.path.exists(test_case_setup_sql_file): tinctest.logger.info("Running setup sql for test - %s" %test_case_setup_sql_file) self._run_and_verify_sql_file(test_case_setup_sql_file) def tearDown(self): """ Run teardown sql files for the test case. For a test sql 'query01.sql', this will run teardown.sql and query01_teardown.sql in that order if it exists in the same location as the test sql file. """ tinctest.logger.trace_in() super(SQLTestCase, self).tearDown() if self.sql_file is not None: self._run_teardown_sql() tinctest.logger.trace_out() def _run_teardown_sql(self): """ Run test specific teardown sql. If 'teardown.sql' exists in the same location as the sql file, run that first and then run <test_sql_file>_teardown.sql if it exists. """ # Check if a test case specific teardown sql exists teardown_sql_file = self.sql_file.replace('.sql', '_teardown.sql') if os.path.exists(teardown_sql_file): tinctest.logger.info("Running teardown sql for test - %s" %teardown_sql_file) self._run_and_verify_sql_file(teardown_sql_file) # Check if a common teardown exists in the same location as the test sql teardown_sql_file = os.path.join(os.path.dirname(self.sql_file), 'teardown.sql') if os.path.exists(teardown_sql_file): tinctest.logger.info("Running teardown sql for test - %s" %teardown_sql_file) self._run_and_verify_sql_file(teardown_sql_file) def run_test(self): """ The method that subclasses should override to execute a sql test case differently. This encapsulates the execution mechanism of SQLTestCase. Runs all the sql files that belong to this test case and compare with ans files. Note that this also runs the other part sqls that make up the test case. For eg: if the base sql is query1.sql, the part sqls are of the form query1_part*.sql in the same location as the base sql. TODO: Provide this as an api at TINCTestCase level. This can be the default test method that will be called for implicitly generated test methods. """ tinctest.logger.trace_in() optimizer_mode = self.optimizer_mode if optimizer_mode == 'both': # test_data will have the real value if isinstance(self.test_data, tuple): optimizer_type, optimizer_mode = self.test_data if isinstance(self.test_data, list): for each_tuple in self.test_data: data_provider, optimizer_type, optimizer_mode = each_tuple if data_provider == "optimizer_handling": break if optimizer_mode == 'both': raise SQLTestCaseException("Data provider for optimizer_handling didn't work as expected") if not os.path.exists(self.sql_file): raise SQLTestCaseException('sql file for this test case does not exist - %s' %self.sql_file ) if (self.__class__.generate_ans == 'no' and not os.path.exists(self.ans_file)) and self.gpdiff: tinctest.logger.error('ans file for this test case does not exist - %s' %self.ans_file ) raise SQLTestCaseException('ans file for this test case does not exist - %s' %self.ans_file ) sql_file_list = self.form_sql_file_list() if optimizer_mode == 'on': self._current_optimizer_mode = True tinctest.logger.info("Running sql files for the test with optimizer on") elif optimizer_mode == 'off': self._current_optimizer_mode = False tinctest.logger.info("Running sql files for the test with optimizer off") else: tinctest.logger.info("Running sql files for the test without modifying optimizer") for sql_file in sql_file_list: tinctest.logger.info("Running sql file %s" %sql_file) self._run_and_verify_sql_file(sql_file, self._current_optimizer_mode) tinctest.logger.trace_out() def _optimizer_suffix(self, optimizer): return 'orca' if optimizer else 'planner' def _which_ans_file(self, sql_file, optimizer): ''' selects the right answer file depending on whether optimizer_mode is on or not if optimizer is True, answer file = .ans.orca and if not present, .ans if optimizer is False, answer file = .ans.planner and if not present, .ans if optimizer is None, answer file = .ans ''' base_sql_file = os.path.basename(sql_file) ans_file = None if optimizer == True or self.__class__._global_optimizer_mode == 'on': ans_file = os.path.join(self.get_ans_dir(), base_sql_file.replace('.sql', '.ans.orca')) elif optimizer == False: ans_file = os.path.join(self.get_ans_dir(), base_sql_file.replace('.sql', '.ans.planner')) if not ans_file: ans_file = os.path.join(self.get_ans_dir(), base_sql_file.replace('.sql', '.ans')) else: if not os.path.exists(ans_file): ans_file = os.path.join(self.get_ans_dir(), base_sql_file.replace('.sql', '.ans')) return ans_file def _run_and_verify_sql_file(self, sql_file, optimizer = None): ans_file = self._which_ans_file(sql_file, optimizer) if optimizer is None: out_file = os.path.join(self.get_out_dir(), os.path.basename(sql_file).replace('.sql', '.out')) else: # out file will be *_opt.out or *_planner.out based on optimizer out_file = os.path.join(self.get_out_dir(), os.path.basename(sql_file).replace('.sql', '_%s.out' %self._optimizer_suffix(optimizer))) self.run_sql_file(sql_file, out_file=out_file, optimizer=optimizer) if self.__class__.generate_ans == 'no': if self.gpdiff: if not os.path.exists(ans_file): tinctest.logger.warning("ans file %s for sql file %s does not exist" % (ans_file, sql_file)) elif not self.verify_out_file(out_file, ans_file): tinctest.logger.exception("Failed with diffs during execution of sql: %s, out_file: %s, ans_file: %s" %(sql_file, out_file, ans_file)) if optimizer is None: self.fail("Failed: diffs during execution of sql: %s, out_file: %s, ans_file: %s" %(sql_file, out_file, ans_file)) if optimizer is True: self.fail("Failed with optimizer on: diffs during execution of sql: %s, out_file: %s, ans_file: %s" %(sql_file, out_file, ans_file)) if optimizer is False: self.fail("Failed with optimizer off: diffs during execution of sql: %s, out_file: %s, ans_file: %s" %(sql_file, out_file, ans_file)) elif self.__class__.generate_ans == 'yes': if os.path.exists(ans_file): tinctest.logger.warning("ans_file %s already exists! Since generate_ans is not set to force, ans_file will not be overwritten." % ans_file) else: tinctest.logger.info("Copying out_file %s to ans_file %s." % (out_file, ans_file)) shutil.copyfile(out_file, ans_file) elif self.__class__.generate_ans == 'force': try: tinctest.logger.info("Force-copying out_file %s to ans_file %s." % (out_file, ans_file)) shutil.copyfile(out_file, ans_file) except Exception: raise SQLTestCaseException("Cannot copy %s to %s. Permission denied. Verify that %s is writeable!" % (out_file, ans_file, ans_file)) else: raise SQLTestCaseException("Invalid value for generate_ans specified: %s. It should be one of the following: %s" % (self.__class__.generate_ans, ', '.join(self.__class__._valid_generate_ans_values))) def form_sql_file_list(self, sql_dir = None): """ Forms a list of all sql files belonging to this test case. @return: list of sql file names belonging to this test case @rtype: list """ tinctest.logger.trace_in("sql_dir: %s" % str(sql_dir)) if sql_dir is None: sql_dir = self.get_sql_dir() file_pattern = os.path.splitext(os.path.basename(self.sql_file))[0] tinctest.logger.debug("Forming sql file list from sql_dir %s, with file_pattern %s" % (sql_dir, file_pattern)) # Find out the list of sqls to be run for this test case. sql_file_list = [] # Look for additional parts to run for f in os.listdir(sql_dir): part_sql_file = os.path.join(sql_dir, f) if not fnmatch(f, file_pattern + "_part*.sql") and not fnmatch(f, file_pattern + ".sql"): continue sql_file_list.append(os.path.join(sql_dir, f)) tinctest.logger.trace_out("sql_file_list: %s" % str(sql_file_list)) return sql_file_list def _add_gucs_to_sql_file(self, sql_file, gucs_sql_file=None, optimizer=None): """ Form test sql file by adding the defined gucs to the sql file @param sql_file Path to the test sql file @param gucs_sql_file Path where the guc sql file should be generated. @param optimizer Boolean that specifies whether optimizer is on or off. """ if not gucs_sql_file: gucs_sql_file = os.path.join(self.get_out_dir(), os.path.basename(sql_file)) with open(gucs_sql_file, 'w') as o: o.write('\n-- start_ignore\n') for guc_string in self.gucs: o.write("SET %s;\n" %guc_string) for orca_guc_string in self.orcagucs: o.write("%s;\n"%orca_guc_string) # Write optimizer mode optimizer_mode_str = '' if optimizer is not None: optimizer_mode_str = 'on' if optimizer else 'off' if optimizer_mode_str: o.write("SET optimizer=%s;\n" %optimizer_mode_str) if optimizer is not None and optimizer: for guc_string in self._optimizer_gucs: o.write("SET %s;\n" %guc_string) o.write('\n-- end_ignore\n') with open(sql_file, 'r') as f: for line in f: o.write(line) self.test_artifacts.append(gucs_sql_file) return gucs_sql_file def run_sql_file(self, sql_file, out_file = None, out_dir = None, optimizer = None): """ Given a sql file and an out file, runs the sql file against the test database (self.db_name) and saves the output to out_file. Before running the sql file, if 'gucs' are set for this test instance , runs the sql file by adding gucs to the sql file as session level configurations. @type sql_file: string @param sql_file: Absolute path to the sql file. @type out_file: string @param out_file: Absolute path to the out file to which the output of sql file execution is saved. @type out_dir: string @param out_dir: Absolute path to a directory location where all the generated artifacts will be written to. @type optimizer: boolean @param optimizer: Flag that determines if optimizer should be on or off while running the sql. Defaults to None which means that the sql will be run as is. """ tinctest.logger.trace_in("sql_file: " + str(sql_file) + "; out_file: " + str(out_file) + "; out_dir: " + str(out_dir) + "; optimizer: " + str(optimizer) + ";") # Add gucs to the test sql and form the actual sql file to be run if not out_dir: out_dir = self.get_out_dir() if not os.path.exists(out_dir): TINCSystem.make_dirs(out_dir, ignore_exists_error = True) if optimizer is None: gucs_sql_file = os.path.join(out_dir, os.path.basename(sql_file)) else: # sql file will be <basename>_opt.sql or <basename>_planner.sql based on optimizer gucs_sql_file = os.path.join(out_dir, os.path.basename(sql_file).replace('.sql', '_%s.sql' %self._optimizer_suffix(optimizer))) self._add_gucs_to_sql_file(sql_file, gucs_sql_file, optimizer) self.test_artifacts.append(gucs_sql_file) if not out_file: if optimizer is None: out_file = os.path.join(self.get_out_dir(), os.path.basename(sql_file).replace('.sql', '.out')) else: # out file will be *_opt.out or *_planner.out based on optimizer out_file = os.path.join(self.get_out_dir(), os.path.basename(sql_file).replace('.sql', '_%s.out' %self._optimizer_suffix(optimizer))) self.psql_run(gucs_sql_file, self.db_name, out_file) self.test_artifacts.append(out_file) tinctest.logger.trace_out("out_file: " + str(out_file)) return out_file def psql_run(self, sql_file, dbname, out_file): """ This is used for run_sql_file method and provide an easier way to override on how we want to run the sql file. One use case is running the sql files on a segment in utility mode. @param sql_file: Absolute path to the sql file. @param dbname: database name @param out_file: Absolute path to the output file """ PSQL.run_sql_file(sql_file, dbname = dbname, out_file = out_file) def verify_out_file(self, out_file, ans_file): """ Verify output file with ans file using gpdiff. If an 'init_file' exists in the same location as the sql_dir, this will be used while doing gpdiff. @type out_file: string @param out_file: Absolute path to the output file @type ans_file: string @param ans_file: Absolute path to the answer file. @rtype: boolean @return: True or False depending on whether gpdiff returned diffs """ tinctest.logger.trace_in("out_file: " + str(out_file) + "; ans_file: " + str(ans_file) + ";") # Check if an init file exists in the same location as the sql file init_files = [] init_file_path = os.path.join(self.get_sql_dir(), 'init_file') if os.path.exists(init_file_path): init_files.append(init_file_path) result = Gpdiff.are_files_equal(out_file, ans_file, match_sub = init_files) if result == False and os.path.exists(out_file.replace('.out', '.diff')): self.test_artifacts.append(out_file.replace('.out', '.diff')) tinctest.logger.trace_out("result: " + str(result)) return result def gather_mini_dump(self, sql_file=None, out_dir=None, minidump_file=None): """ Utility method to gather mini dumps for test queries. Set gp_opt_minidump to on, explain test query and gather mini-dump to out_dir """ tinctest.logger.trace_in("sql_file: " + str(sql_file) + "; out_dir: " + str(out_dir) + "; minidump_file: " + str(minidump_file) + ";") if not sql_file: sql_file = self.sql_file if not out_dir: out_dir = self.get_out_dir() # Handle where self.sql_file might be None if not sql_file: tinctest.logger.warning("SQL file is not defined.") tinctest.logger.trace_out() return None if not os.path.exists(sql_file): tinctest.logger.warning("SQL file %s does not exist." %sql_file) tinctest.logger.trace_out() return None if not os.path.exists(out_dir): TINCSystem.make_dirs(out_dir, ignore_exists_error = True) opt_md_sql_file = os.path.join(out_dir, os.path.basename(sql_file).replace('.sql', '_opt_md.sql')) with open(opt_md_sql_file, 'w') as mini_dump_sql: with open(sql_file, 'r') as original_sql: opt_write = False for line in original_sql: if not line.startswith('--') and not opt_write: # Add optimizer settings and then add the line mini_dump_sql.write('-- start_ignore\n') mini_dump_sql.write('set optimizer = on; \n') mini_dump_sql.write('set optimizer_minidump = always;\n') mini_dump_sql.write('-- end_ignore\n') # TODO - For now we assume only one valid SQL block following metadata # and blindly add explain. This should suffice for the first demo # with TPC-H tests. We will handle other cases later. mini_dump_sql.write('explain ' + line) opt_write = True else: mini_dump_sql.write(line) # Remove existing minidumps in $MASTER_DATA_DIRECTORY. Would be nice to have a GUC # to control location , name of the Mini-dump that gets generated. mdd = os.environ.get('MASTER_DATA_DIRECTORY', None) if not mdd: tinctest.logger.warning("Not gathering mini dump because MASTER_DATA_DIRECTORY is not set.") tinctest.logger.trace_out() return None mini_dump_directory = os.path.join(mdd, 'minidumps') if os.path.exists(mini_dump_directory): for f in os.listdir(mdd): if fnmatch(f, 'Minidump_*.mdp'): os.remove(os.path.join(mdd,f)) # Run opt_md_sql_file and copy the generated mini-dump over to out dir out_file = os.path.join(out_dir, os.path.basename(opt_md_sql_file).replace('.sql','.out')) tinctest.logger.info("Gathering mini-dump from sql : " + opt_md_sql_file) PSQL.run_sql_file(opt_md_sql_file, dbname = self.db_name, out_file = out_file) # Copy generated minidump from MASTER_DATA_DIRECTORY to out_dir if not os.path.exists(mini_dump_directory): tinctest.logger.warning("Minidump does not seem to be generated. Check %s for more information." %out_file) tinctest.logger.trace_out() return None for f in os.listdir(mini_dump_directory): if fnmatch(f, 'Minidump_*.mdp'): if not minidump_file: test_md_file = os.path.join(out_dir, os.path.basename(self.sql_file).replace('.sql', '_minidump.mdp')) else: test_md_file = minidump_file shutil.copyfile(os.path.join(mini_dump_directory, f), test_md_file) tinctest.logger.info("Minidump gathered at %s." %test_md_file) tinctest.logger.trace_out("test_md_file: " + str(test_md_file)) return test_md_file tinctest.logger.warning("Minidump does not seem to be generated. Check %s for more information." %out_file) tinctest.logger.trace_out() return None def _restart_cluster(self, refresh_cache=False): """ restart the cluster possibly refreshing cache """ msg = '' if refresh_cache: msg = '(refresh cache)' msg = 'Restarting cluster ' + msg tinctest.logger.info(msg + ': STARTED'); try: if refresh_cache: array = GpArray.initFromCatalog(DbURL(), True) GpStop.local('gpstop') seg_set = set() for seg in array.getDbList(): if not seg.getSegmentHostName() in seg_set: TINCSystem.drop_caches(seg.getSegmentHostName()) seg_set.add(seg.getSegmentHostName()) else: GpStop.local('gpstop') GpStart.local('gpstart') tinctest.logger.info(msg + ': DONE') except Exception, e: tinctest.logger.error(msg + ': FAILED') def defaultTestResult(self, stream=None, descriptions=None, verbosity=None): """ Return a custom result object for SQLTestCase. """ if stream and descriptions and verbosity: # TODO - Eliminate this check. We should not care about this and OptimizerSQLTestCase # should always return an SQLTestCaseResult. return _SQLTestCaseResult(stream, descriptions, verbosity) else: return unittest.TestResult() class SQLTestCaseException(Exception): """ Exception object thrown by SQLTestCase for failures / errors """ pass class _SQLTestCaseResult(_MPPTestCaseResult): """ A custom listener class for SQLTestCase. This is responsible for reacting appropriately to failures and errors of type SQLTestCase. Following is what this class does on failure: -> If the test failed with optimizer turned on, gather mini dumps for the test sqls. """ def addFailure(self, test, err): if test._current_optimizer_mode: test.gather_mini_dump() super(_SQLTestCaseResult, self).addFailure(test, err) @tinctest.skipLoading("Test model. No tests loaded.") class __gpdbSQLTestCase__(SQLTestCase): """ Overwrite handle_optimizer_mode_metadata to check if GPDB version is less than 4.3. If it is, force it off """ current_product_version_object = _TINCProductVersion(SQLTestCase.__version_string__) optimizer_introduction_version_object = _TINCProductVersion("4.3") overwrite_optimizer_mode = False if current_product_version_object < optimizer_introduction_version_object: overwrite_optimizer_mode = True def handle_optimizer_mode_metadata(self): # metadata optimizer_mode if self._metadata.get('optimizer_mode', None): if not self._metadata.get('optimizer_mode').lower() in self._optimizer_mode_values: self.load_fail(SQLTestCaseException, "Invalid metadata specified for optimizer_mode: %s. Valid values are %s" %(self._metadata.get('optimizer_mode'), self._optimizer_mode_values)) self.optimizer_mode = self._metadata.get('optimizer_mode').lower() if __gpdbSQLTestCase__.overwrite_optimizer_mode: self.optimizer_mode = None if self.optimizer_mode == "both": if self.data_provider: self.data_provider += " optimizer_handling" else: self.data_provider = "optimizer_handling" @tinctest.skipLoading("Test model. No tests loaded.") class __gpdbSQLTestCase__(SQLTestCase): """ Since HAWQ doesn't support planner, we should always set it to on """ def handle_optimizer_mode_metadata(self): self.optimizer_mode = "on"