Commits

Kirill Simonov committed 75ee237

Implemented `sql` and `sql-include` test cases.

Also, fixed state management, suite filtering and other minor issues.

  • Participants
  • Parent commits f5ba920

Comments (0)

Files changed (3)

 syntax: glob
 *.pyc
-*.swp
+.*.sw?
 *.egg-info
 build
 .. autoclass:: EndCtlTestCase
 .. autoclass:: PythonCodeTestCase
 .. autoclass:: SQLTestCase
+   :members: load
 .. autoclass:: SQLIncludeTestCase
 .. autoclass:: WriteToFileTestCase
 .. autoclass:: ReadFromFileTestCase
 .. autoclass:: MakeDirTestCase
 .. autoclass:: RemoveDirTestCase
 .. autoclass:: TestState
-   :members: clone, merge
+   :members: push, pull
 .. autoclass:: RegressYAMLLoader
    :members: load
 .. autoclass:: RegressYAMLDumper

File src/htsql/ctl/regress.py

 """
 
 
+from __future__ import with_statement
 from .error import ScriptError
 from .routine import Argument, Routine
 from .option import (InputOption, TrainOption, PurgeOption,
 from .request import Request
 from ..validator import (Val, AnyVal, BoolVal, StrVal, WordVal, ChoiceVal,
                          IntVal, UFloatVal, DBVal, SeqVal, MapVal, ClassVal)
-from ..util import maybe, trim_doc
+from ..util import maybe, trim_doc, DB
 import traceback
 import StringIO
 import sys
                       hint="""the connection URI"""),
         ]
 
+    def out_header(self):
+        # Overriden to avoid printing the password to the database.
+
+        # Clone `input.db`, but omit the password.
+        db = self.input.db
+        sanitized_db = DB(engine=db.engine,
+                          username=db.username,
+                          password=None,
+                          host=db.host,
+                          port=db.port,
+                          database=db.database,
+                          options=db.options)
+
+        # Print:
+        # ---------------- ... -
+        #   APP {sanitized_db}
+        #   ({input.location})
+        self.out_sep()
+        self.out("%s %s" % (self.name.upper(), sanitized_db), indent=2)
+        if self.input.location is not None:
+            self.out("(%s)" % self.input.location, indent=2)
+
     def verify(self):
         # Display the header.
         self.out_header()
 
         # Generate a list of test cases.
         self.cases = []
-        self.cases_state = state.clone()
+        self.cases_state = TestState()
         self.init_cases()
 
     def init_cases(self):
 
     def get_suites(self):
         # Get a set of (this and) the nested suites.
-        suites = set([self.id])
+        suites = set([self.input.id])
         for case in self.cases:
             suites |= case.get_suites()
         return suites
         # - and the suite is not nested in some selected suite.
         if not self.routine.suites:
             return False
-        if self.id in self.routine.suites:
+        if self.state.with_all_suites:
+            return False
+        if self.input.id in self.routine.suites:
             self.cases_state.with_all_suites = True
             return False
         if self.get_suites() & set(self.routine.suites):
 
     def verify(self):
         # Run the suite.
+
+        # Push the current state to the cases state.
+        self.state.push(self.cases_state)
+        # Check if the user specified the suites to run and
+        # this one is not among them.
         if self.skipped():
             return
+        # Display the headers.
         self.out_header()
         # Run the nested test cases.
         for case in self.cases:
             # Check if the user asked to halt the testing.
             if self.cases_state.is_exiting:
                 break
-        # Update our state with the new statistics.
-        self.state.merge(self.cases_state)
+        # Pull the statistical information from the cases state.
+        self.state.pull(self.cases_state)
 
     def train(self):
         # Run the suite; update the test output if necessary.
+
+        # Push the current state to the cases state.
+        self.state.push(self.cases_state)
+        # Check if the user specified the suites to run and
+        # this one is not among them.
         if self.skipped():
             return self.output
         # A dictionary containing the output (or `None`) generated by test
         # cases when it differs from the existing test output.
         new_output_by_case = {}
+        # Display the header.
         self.out_header()
         # Run the nested tests.
         for case in self.cases:
             # Check if the user asked to halt the testing.
             if self.cases_state.is_exiting:
                 break
-        # Update the testing state with the new statistics.
-        self.state.merge(self.cases_state)
+        # Pull the statistical information from the cases state.
+        self.state.pull(self.cases_state)
         # Generate a new output record.
         output = self.make_output(new_output_by_case)
         # The output is kept in a separate file.
                         if idx >= next_idx:
                             next_idx = idx+1
 
+        # When there are no test output data, skip creating the output record.
+        if not tests:
+            return None
+
         # Now we need to check if the new output list coincides with the old
         # one, in which case we don't want to create a new output record.
         if self.input.output is not None:
     # TODO: Can't implement until the SQL splitter is done.
 
     name = "sql"
-    hint = """execute a SQL statement (not implemented)"""
+    hint = """execute a SQL statement"""
+    help = """
+    This test case executes one or multiple SQL statements.
+    """
 
     class Input(TestData):
         fields = [
                       hint="""ignore any errors"""),
         ]
 
-
-class SQLIncludeTestCase(TestCase):
+    def out_header(self):
+        # Print:
+        # ---------------- ... -
+        #   {first line of input.sql}
+        #   ({input.location})
+        self.out_sep()
+        first_line = self.input.sql.split('\n', 1)[0]
+        self.out(first_line, indent=2)
+        if self.input.location is not None:
+            self.out("(%s)" % self.input.location, indent=2)
+
+    def verify(self):
+        # Display the header.
+        self.out_header()
+
+        # Load the SQL input data.
+        sql = self.load()
+
+        # Generate an HTSQL application.  We need an application instance
+        # to split the SQL data and to connect to the database, but we
+        # never use it for executing HTSQL queries.
+        from htsql.application import Application
+        from htsql.connect import Connect, DBError
+        from htsql.split_sql import SplitSQL
+        try:
+            app = Application(self.input.connect)
+        except Exception, exc:
+            self.out_exception(sys.exc_info())
+            return self.failed("*** an exception occured while"
+                               " initializing an HTSQL application")
+
+        # Activate the application so that we could use the splitter
+        # and the connection adapters.
+        with app:
+            # Realize a splitter and split the input data to individual
+            # SQL statements.
+            split_sql = SplitSQL()
+            try:
+                statements = list(split_sql(sql))
+            except ValueError, exc:
+                return self.failed("*** invalid SQL: %s" % exc)
+
+            # Realize the connector and connect to the database.
+            connect = Connect()
+            try:
+                connection = connect(with_autocommit=self.input.autocommit)
+                cursor = connection.cursor()
+            except DBError, exc:
+                return self.failed("*** failed to connect to the database:"
+                                   " %s" % exc)
+
+            # Execute the given SQL statements.
+            for statement in statements:
+                try:
+                    # Execute the statement in the current connection.
+                    cursor.execute(statement)
+                except DBError, exc:
+                    # Display the statement that caused a problem.
+                    for line in statement.splitlines():
+                        self.out(line, indent=4)
+                    # Normally, we end the test case when an error occurs,
+                    # but if `ignore` is set, we just break the loop.
+                    if not self.input.ignore:
+                        return self.failed("*** failed to execute SQL:"
+                                           " %s" % exc)
+                    break
+
+            # No error occurred while executing the SQL statements.
+            else:
+                # Commit the transaction unless `autocommit` mode is set.
+                # Again, respect the `ignore` flag.
+                if not self.input.autocommit:
+                    try:
+                        connection.commit()
+                    except DBError, exc:
+                        if not self.input.ignore:
+                            return self.failed("*** failed to commit"
+                                               " a transaction: %s" % exc)
+
+            # Close the connection.  Note that we insist that connection
+            # is opened and closed successfully regardless of the value
+            # of the `ignore` flag.
+            try:
+                connection.close()
+            except DBError, exc:
+                return self.failed("*** failed to close the connection:"
+                                   " %s" % exc)
+
+        # If we reached that far, we passed the test.
+        return self.passed()
+
+    def load(self):
+        """
+        Returns the SQL data to execute.
+        """
+        # Override when subclassing.
+        return self.input.sql
+
+
+class SQLIncludeTestCase(SQLTestCase):
     """
-    Load SQL queries from a file and execute them.
+    Loads SQL queries from a file and executes them.
     """
-    # TODO: Can't implement until the SQL splitter is done.
 
     name = "sql-include"
-    hint = """load and execute SQL statements (not implemented)"""
+    hint = """load and execute SQL statements"""
+    help = """
+    This test case loads SQL statements from a file and execute them.
+    """
 
     class Input(TestData):
         fields = [
                       hint="""ignore any errors"""),
         ]
 
+    def out_header(self):
+        # Print:
+        # ---------------- ... -
+        #   SQL-INCLUDE {input.sql_include}
+        #   ({input.location})
+        self.out_sep()
+        self.out("%s %s" % (self.name.upper(), self.input.sql_include),
+                 indent=2)
+        if self.input.location is not None:
+            self.out("(%s)" % self.input.location, indent=2)
+
+    def load(self):
+        # Load SQL from the given file.
+        stream = open(self.input.sql_include, 'rb')
+        sql = stream.read()
+        stream.close()
+        return sql
+
 
 class WriteToFileTestCase(TestCase):
     """
         ]
 
     def verify(self):
+        # Display the header.
+        self.out_header()
         # Write the data to the file.
         stream = open(input.write, 'wb')
         stream.write(input.data)
         ]
 
     def verify(self):
+        # Display the header.
+        self.out_header()
         # Remove the given files.
         for path in self.input.remove:
             if os.path.exists(path):
         ]
 
     def verify(self):
+        # Display the header.
+        self.out_header()
         # Create the directory if it does not already exist.
         if not os.path.isdir(self.input.mkdir):
             os.makedirs(self.input.mkdir)
         ]
 
     def verify(self):
+        # Display the header.
+        self.out_header()
         # Remove the directory with all its content (DANGEROUS!).
         if os.path.exists(self.input.mkdir):
             shutil.rmtree(self.input.mkdir)
         self.updated = updated
         self.is_exiting = is_exiting
 
-    def clone(self):
+    def push(self, other):
         """
-        Creates a clone of the object.
+        Push the state data to a derived state.
+
+        `other` (:class:`TestState`)
+            A derived state, the state created by a suite for
+            the suite test cases.
         """
-        return self.__class__(app=self.app,
-                              forks=self.forks,
-                              with_all_suites=self.with_all_suites,
-                              passed=self.passed,
-                              failed=self.failed,
-                              updated=self.updated,
-                              is_exiting=self.is_exiting)
-
-    def merge(self, other):
+        other.app = self.app
+        other.forks = self.forks
+        other.with_all_suites = self.with_all_suites
+        other.passed = self.passed
+        other.failed = self.failed
+        other.updated = self.updated
+        other.is_exiting = self.is_exiting
+
+    def pull(self, other):
         """
-        Merges some data from another :class:`TestState` instance.
-
-        The following attributes are updated: `passed`, `failed`, `updated`,
-        and `is_exiting`.
+        Pull the state from a derived state.
+
+        Note that only statistical information is pulled from
+        the derived state.
+
+        `other` (:class:`TestState`)
+            A derived state, the state created by a suite for
+            the suite test cases.
         """
         self.passed = other.passed
         self.failed = other.failed
     name = 'regress'
     aliases = ['test']
     arguments = [
-            Argument('suites', WordVal(), None, is_list=True),
+            Argument('suites', SeqVal(WordVal()), None, is_list=True),
     ]
     options = [
             InputOption,