[PATCH] Run metadata and first pass at SQL results storage.

Nathaniel Smith njs at pobox.com
Thu Jul 3 02:53:24 UTC 2003


Attached for review.  This patch also renames 'ResultSource' to
'ResultReader', since that better describes the interface we ended up
with.

-- Nathaniel

-- 
"...All of this suggests that if we wished to find a modern-day model
for British and American speech of the late eighteenth century, we could
probably do no better than Yosemite Sam."
-------------- next part --------------
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/ChangeLog qm-ResultSource/ChangeLog
--- qm-clean/ChangeLog	2003-06-27 20:16:03.000000000 -0700
+++ qm-ResultSource/ChangeLog	2003-07-02 19:50:20.000000000 -0700
@@ -1,3 +1,60 @@
+2003-07-02  Nathaniel Smith  <njs at codesourcery.com>
+
+	* qm/test/base.py (load_results): Rename 'ResultSource' to
+	'ResultReader' everywhere.
+	(extension_kinds): Rename "result_source" kind to
+	"result_reader".
+	(__extension_bases): Likewise.  And same for the imports just
+	above.
+	* qm/test/result_source.py: Rename to...
+	* qm/test/result_reader.py: ...this.  Also rename classes, etc.
+	* qm/test/file_result_source.py: Rename to...
+	* qm/test/file_result_reader.py: ...this.  Also rename classes,
+	etc.
+	
+	* qm/common.py (format_time_iso): New function.
+	* qm/db.py: New file.
+	* qm/extension.py (get_class_arguments): Mark's fixes to
+	support diamond inheritance.
+
+	* qm/test/classes/pickle_result_source.py: Remove.
+	(PickleResultSource): Move to...
+	* qm/test/classes/pickle_result_stream.py: ...here.
+	(PickleResultSource): Rename to 'PickleResultReader'.  Also
+	rewrite for new metadata handling regime.
+	(PickleResultStream): Rewrite for new metadata handling regime.
+	* qm/test/classes/xml_result_stream.py: Likewise.
+	(__init__): Use 'super'.
+	* qm/test/classes/xml_result_source.py: Rename to...
+	* qm/test/classes/xml_result_reader.py: ...this.  Also rename
+	classes, etc., and update for new metadata handling regime.
+	
+	* qm/test/result_stream.py (ResultStream.WriteAnnotation): New
+	function.
+	(ResultStream.WriteAllAnnotations): New function.
+	(ResultStream.WriteResult): More detail in docstring.
+	
+	* qm/test/classes/classes.qmc: Add SQL result handling classes,
+	rename 'ResultSource' to 'ResultReader'.
+	
+	* qm/test/cmdline.py (QMTest.__ExecuteSummarize): Copy
+	metadata.
+	* qm/test/execution_engine.py: import time.
+	(ExecutionEngine.Run): Create and write metadata.
+	* qm/test/web.py (StorageResultsStream.__init__): Take
+	arguments.  Use 'super'.  Handle annotations.
+	(StorageResultsStream.GetAnnotations): New method.
+	(StorageResultsStream.WriteAnnotations: New method.
+	(QMTestServer.__init__): Pass arguments to
+	'StorageResultsStream.__init__'.
+	(QMTestServer.HandleClearResults): Likewise.
+	(QMTestServer.HandleSubmitResults): Likewise.  Write out
+	annotations.
+	(QMTestServer.HandleSaveResults): Write out annotations.
+	
+	* scripts/create-test-database.py: New file.
+	* qm/test/classes/sql_result_stream.py: New file.
+	
 2003-06-27  Nathaniel Smith  <njs at codesourcery.com>
 
 	* qm/test/classes/classes.qmc: Add
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/common.py qm-ResultSource/qm/common.py
--- qm-clean/qm/common.py	2003-05-16 00:31:18.000000000 -0700
+++ qm-ResultSource/qm/common.py	2003-07-02 13:15:14.000000000 -0700
@@ -647,6 +647,20 @@
            "%(hour)02d:%(minute)02d %(time_zone)s" % locals()
 
 
+def format_time_iso(time_secs):
+    """Generate a ISO8601-compliant formatted date and time.
+
+    The output is in the format "YYYY-MM-DDThh:mm:ss+TZ", where TZ is
+    a timezone specifier.  We always normalize to UTC (and hence
+    always use the special timezone specifier "Z"), to get proper
+    sorting behaviour.
+
+    'time_secs' -- the time to be formatted, as returned by
+                   e.g. 'time.time()'."""
+
+    return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(time_secs))
+
+
 def make_unique_tag():
     """Return a unique tag string."""
 
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/db.py qm-ResultSource/qm/db.py
--- qm-clean/qm/db.py	1969-12-31 16:00:00.000000000 -0800
+++ qm-ResultSource/qm/db.py	2003-07-02 18:54:23.000000000 -0700
@@ -0,0 +1,96 @@
+########################################################################
+#
+# File:   db.py
+# Author: Nathaniel Smith <njs at codesourcery.com
+# Date:   2003-06-13
+#
+# Contents:
+#   A few simple functions to help with connecting to SQL databases
+#   and using the DB 2.0 API.
+#
+# Copyright (c) 2003 by CodeSourcery, LLC.  All rights reserved. 
+#
+# For license terms see the file COPYING.
+#
+########################################################################
+
+########################################################################
+# Imports
+########################################################################
+
+import os
+
+########################################################################
+# Functions
+########################################################################
+
+class Connection:
+    """A little wrapper around a DB 2.0 connection that preserves a
+    reference to the containing module, and provides a minimal
+    interface to said connection.  This is useful because it gives us
+    a hook to attach our SQL-quoting code in to."""
+
+    def __init__(self, module, connection):
+
+        self._module = module
+        self._connection = connection
+
+    def close(self):
+
+        self._connection.close()
+
+    def commit(self):
+
+        self._connection.commit()
+
+    def rollback(self):
+
+        self._connection.rollback()
+
+
+    def execute(self, sql):
+        """Creates a cursor in our database and uses it to execute
+        the given SQL; returns the cursor.
+
+        If this database requires any overall quoting of the given SQL
+        (for instance, doubling of %'s), this will be performed in this
+        method.
+        
+        """
+
+        if self._module.paramstyle in ["format", "pyformat"]:
+            sql = sql.replace("%", "%%")
+        cursor = self._connection.cursor()
+        
+        cursor.execute(sql)
+        return cursor
+        
+
+def connect(modname, *args, **moreargs):
+    """Use the given DB 2.0 module to connect to a database.  Does not
+    return a DB 2.0 database, but rather a `ModuledConnection', which
+    acts like a DB 2.0 database but supports `execute()' instead of
+    `cursor()'."""
+
+    module = __import__(modname,
+                        globals(),
+                        locals(),
+                        ["dummy element to make __import__ behave"])
+    cxn = module.connect(*args, **moreargs)
+    return Connection(module, cxn)
+    
+
+def quotestr(string):
+    """Quotes a string for SQL."""
+
+    # Replace each ' with '', then surround with more 's.  Also double
+    # backslashes.  It'd be nice to handle things like quoting non-ASCII
+    # characters (by replacing them with octal escapes), but we don't.
+    return "'" + string.replace("'", "''").replace("\\", "\\\\") + "'"
+
+########################################################################
+# Local Variables:
+# mode: python
+# indent-tabs-mode: nil
+# fill-column: 72
+# End:
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/extension.py qm-ResultSource/qm/extension.py
--- qm-clean/qm/extension.py	2003-06-16 16:33:29.000000000 -0700
+++ qm-ResultSource/qm/extension.py	2003-07-02 17:45:09.000000000 -0700
@@ -133,12 +133,7 @@
         arguments = []
         dictionary = {}
         # Start with the most derived class.
-        classes = [extension_class]
-        while classes:
-            # Pull the first class off the list.
-            c = classes.pop(0)
-            # Add all of the new base classes to the end of the list.
-            classes.extend(c.__bases__)
+        for c in extension_class.__mro__:
             # Add the arguments from this class.
             new_arguments = c.__dict__.get("arguments", [])
             for a in new_arguments:
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/test/base.py qm-ResultSource/qm/test/base.py
--- qm-clean/qm/test/base.py	2003-06-25 02:03:35.000000000 -0700
+++ qm-ResultSource/qm/test/base.py	2003-07-02 18:49:45.000000000 -0700
@@ -347,7 +347,7 @@
 
     'database' -- The current database.
 
-    returns -- A 'ResultSource' object."""
+    returns -- A 'ResultReader' object."""
 
     # For backwards compatibility, look at the first few bytes of the
     # file to see if it is an XML results file.
@@ -355,16 +355,16 @@
     file.seek(0)
     
     if tag == "<?xml":
-        source_cls = \
-         get_extension_class("xml_result_source.XMLResultSource",
-                             "result_source",
+        reader_cls = \
+         get_extension_class("xml_result_reader.XMLResultReader",
+                             "result_reader",
                              database)
     else:
-        source_cls = \
-         get_extension_class("pickle_result_source.PickleResultSource",
-                             "result_source",
+        reader_cls = \
+         get_extension_class("pickle_result_stream.PickleResultReader",
+                             "result_reader",
                              database)
-    return source_cls({"file": file})
+    return reader_cls({"file": file})
 
 
 def _result_from_dom(node):
@@ -425,7 +425,7 @@
 extension_kinds = [ 'database',
                     'label',
                     'resource',
-                    'result_source',
+                    'result_reader',
                     'result_stream',
                     'target',
                     'test', ]
@@ -444,7 +444,7 @@
 import qm.test.database
 import qm.label
 import qm.test.resource
-import qm.test.result_source
+import qm.test.result_reader
 import qm.test.result_stream
 import qm.test.target
 import qm.test.test
@@ -453,7 +453,7 @@
     'database' : qm.test.database.Database,
     'label' : qm.label.Label,
     'resource' : qm.test.resource.Resource,
-    'result_source' : qm.test.result_source.ResultSource,
+    'result_reader' : qm.test.result_reader.ResultReader,
     'result_stream' : qm.test.result_stream.ResultStream,
     'target' : qm.test.target.Target,
     'test' : qm.test.test.Test
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/test/classes/classes.qmc qm-ResultSource/qm/test/classes/classes.qmc
--- qm-clean/qm/test/classes/classes.qmc	2003-06-27 20:15:46.000000000 -0700
+++ qm-ResultSource/qm/test/classes/classes.qmc	2003-07-02 18:48:57.000000000 -0700
@@ -18,8 +18,10 @@
 <class kind="label">python_label.PythonLabel</class>
 <class kind="result_stream">text_result_stream.TextResultStream</class>
 <class kind="result_stream">xml_result_stream.XMLResultStream</class>
-<class kind="result_source">xml_result_source.XMLResultSource</class>
+<class kind="result_reader">xml_result_reader.XMLResultReader</class>
 <class kind="result_stream">pickle_result_stream.PickleResultStream</class>
-<class kind="result_source">pickle_result_source.PickleResultSource</class>
+<class kind="result_reader">pickle_result_stream.PickleResultReader</class>
+<class kind="result_stream">sql_result_stream.SQLResultStream</class>
+<class kind="result_reader">sql_result_stream.SQLResultReader</class>
 <class kind="result_stream">dejagnu_stream.DejaGNUStream</class>
 </class-directory>
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/test/classes/pickle_result_source.py qm-ResultSource/qm/test/classes/pickle_result_source.py
--- qm-clean/qm/test/classes/pickle_result_source.py	2003-06-25 02:03:35.000000000 -0700
+++ qm-ResultSource/qm/test/classes/pickle_result_source.py	1969-12-31 16:00:00.000000000 -0800
@@ -1,53 +0,0 @@
-########################################################################
-#
-# File:   pickle_result_source.py
-# Author: Nathaniel Smith
-# Date:   2003-06-23
-#
-# Contents:
-#   PickleResultSource
-#
-# Copyright (c) 2003 by CodeSourcery, LLC.  All rights reserved. 
-#
-# For license terms see the file COPYING.
-#
-########################################################################
-
-########################################################################
-# Imports
-########################################################################
-
-import cPickle
-from qm.test.file_result_source import FileResultSource
-
-########################################################################
-# Classes
-########################################################################
-
-class PickleResultSource(FileResultSource):
-    """A 'PickleResultSource' reads in results from pickle files.
-
-    See also 'PickleResultStream', which does the reverse."""
-
-    def __init__(self, arguments):
-
-        super(PickleResultSource, self).__init__(arguments)
-        self.__unpickler = cPickle.Unpickler(self.file)
-
-
-    def GetResult(self):
-
-        try:
-            return self.__unpickler.load()
-        except EOFError:
-            return None
-        except cPickle.UnpicklingError:
-            # This is raised at EOF if file is a StringIO.
-            return None
-
-########################################################################
-# Local Variables:
-# mode: python
-# indent-tabs-mode: nil
-# fill-column: 72
-# End:
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/test/classes/pickle_result_stream.py qm-ResultSource/qm/test/classes/pickle_result_stream.py
--- qm-clean/qm/test/classes/pickle_result_stream.py	2003-06-21 00:58:44.000000000 -0700
+++ qm-ResultSource/qm/test/classes/pickle_result_stream.py	2003-07-02 18:48:16.000000000 -0700
@@ -5,7 +5,7 @@
 # Date:   11/25/2002
 #
 # Contents:
-#   PickleResultStream
+#   PickleResultStream, PickleResultReader
 #
 # Copyright (c) 2002, 2003 by CodeSourcery, LLC.  All rights reserved. 
 #
@@ -16,15 +16,35 @@
 ########################################################################
 
 import cPickle
+import struct
 import qm.fields
 from   qm.test.file_result_stream import FileResultStream
+from   qm.test.file_result_reader import FileResultReader
+
+########################################################################
+# Constants
+########################################################################
+
+# A nice subtlety is that because of how extension classes are loaded,
+# we can't use the standard trick of using a nonce class for our
+# sentinel, because the unpickler won't be able to find the class
+# definition.  But 'None' has no other meaning in our format, so works
+# fine.
+_annotation_sentinel = None
+"""The sentinel value that marks the beginning of an annotation."""
+
+# Network byte order, 4 byte unsigned int
+_int_format = "!I"
+_int_size = struct.calcsize(_int_format)
 
 ########################################################################
 # Classes
 ########################################################################
 
 class PickleResultStream(FileResultStream):
-    """A 'PickleResultStream' writes out results as Python pickles."""
+    """A 'PickleResultStream' writes out results as Python pickles.
+
+    See also 'PickleResultReader', which does the reverse."""
 
     _max_pinned_results = 1000
     """A limit on how many `Result's to pin in memory at once.
@@ -40,6 +60,31 @@
     technique causes a very minor slowdown on small result streams,
     and a substantial speedup on large result streams."""
 
+    _format_version = 1
+    """The version number of the format we write.
+
+    This is bumped every time the format is changed, to make sure that
+    we can retain backwards compatibility.
+
+    "Version 0" contains no version number, and is simply a bunch
+    of 'Result's pickled one after another.
+
+    "Version 1", and all later versions, contain a pickled version
+    number as the first thing in the file.  In version 1, this is
+    followed by a 4-byte unsigned integer in network byte order giving
+    the address of the first annotation, followed by the file proper.
+    The file proper is composed of a bunch of pickled 'Result's,
+    followed by a pickled sentinel value (None), followed by a 4-byte
+    unsigned integer in network-byte order, followed by the beginning
+    of a new pickle whose first item is a annotation tuple.  An
+    annotation tuple is a tuple of n items, the first of which is a
+    string describing tagging the type of annotation, and the rest of
+    which have an interpretation that depends on the tag found.  The
+    only tag currently defined is "annotation", which is followed by
+    two string elements giving respectively the key and the value.
+    The 4-byte integers always point to the file address of the next
+    such integer, except for the last, which has a value of 0; they
+    are used to quickly find all annotations."""
 
     arguments = [
         qm.fields.IntegerField(
@@ -73,11 +118,42 @@
 
         # Initialize the base class.
         super(PickleResultStream, self).__init__(arguments)
-        # Create a pickler.
-        self.__pickler = cPickle.Pickler(self.file, self.protocol_version)
+        # Create initial pickler.
+        self._ResetPickler()
         # We haven't processed any `Result's yet.
         self.__processed = 0
 
+        # Write out version number.
+        self.__pickler.dump(self._format_version)
+        # We have no previous annotations.
+        self.__last_annotation = None
+        # Write out annotation header.
+        self._WriteAnnotationPtr()
+
+
+    def _ResetPickler(self):
+
+        self.__pickler = cPickle.Pickler(self.file, self.protocol_version)
+
+
+    def _WriteAnnotationPtr(self):
+
+        new_annotation = self.file.tell()
+        if self.__last_annotation is not None:
+            self.file.seek(self.__last_annotation)
+            self.file.write(struct.pack(_int_format, new_annotation))
+            self.file.seek(new_annotation)
+        self.file.write(struct.pack(_int_format, 0))
+        self.__last_annotation = new_annotation
+        self._ResetPickler()
+
+
+    def WriteAnnotation(self, key, value):
+
+        self.__pickler.dump(_annotation_sentinel)
+        self._WriteAnnotationPtr()
+        self.__pickler.dump(("annotation", key, value))
+
 
     def WriteResult(self, result):
 
@@ -87,3 +163,114 @@
         # cache.
         if not self.__processed % self._max_pinned_results:
             self.__pickler.clear_memo()
+
+
+            
+class PickleResultReader(FileResultReader):
+    """A 'PickleResultReader' reads in results from pickle files.
+
+    See also 'PickleResultStream', which does the reverse."""
+
+    def __init__(self, arguments):
+
+        super(PickleResultReader, self).__init__(arguments)
+        self._ResetUnpickler()
+
+        self._annotations = {}
+
+        # Check for a version number
+        try:
+            version = self.__unpickler.load()
+        except (EOFError, cPickle.UnpicklingError):
+            # This file is empty, no more handling needed.
+            return
+        
+        if not isinstance(version, int):
+            # Version 0 file, no version number; in fact, we're
+            # holding a 'Result'.  So we have no metadata to load and
+            # should just rewind.
+            self.file.seek(0)
+            self._ResetPickler()
+        elif version == 1:
+            self._ReadMetadata()
+        else:
+            raise QMException, "Unknown format version %i" % (version,)
+
+
+    def _ResetUnpickler(self):
+
+        self.__unpickler = cPickle.Unpickler(self.file)
+
+
+    def _ReadAddress(self):
+
+        raw = self.file.read(_int_size)
+        return struct.unpack(_int_format, raw)[0]
+        
+
+    def _ReadMetadata(self):
+
+        # We've read in the version number; next few bytes are the
+        # address of the first annotation.
+        addr = self._ReadAddress()
+        # That advanced the read head to the first 'Result'; save this
+        # spot to return to later.
+        first_result_addr = self.file.tell()
+        while addr:
+            # Go the the address.
+            self.file.seek(addr)
+            # First four bytes are the next address.
+            addr = self._ReadAddress()
+            # Then we restart the pickle stream...
+            self._ResetUnpickler()
+            # ...and read in the annotation here.
+            annotation_tuple = self.__unpickler.load()
+            kind = annotation_tuple[0]
+            if kind == "annotation":
+                (key, value) = annotation_tuple[1:]
+                self._annotations[key] = value
+            else:
+                print "Unknown annotation type '%s'; ignoring" % (kind,)
+            # Now loop back and jump to the next address.
+
+        # Finally, rewind back to the beginning for the reading of
+        # 'Result's.
+        self.file.seek(first_result_addr)
+        self._ResetUnpickler()
+
+
+    def GetAnnotations(self):
+
+        return self._annotations
+
+
+    def GetResult(self):
+
+        while 1:
+            try:
+                thing = self.__unpickler.load()
+            except EOFError:
+                return None
+            except cPickle.UnpicklingError:
+                # This is raised at EOF if file is a StringIO.
+                return None
+            else:
+                if thing is _annotation_sentinel:
+                    # We're looking for results, but this is an annotation,
+                    # so skip over it.
+                    # By skipping past the address...
+                    self.file.seek(_int_size, 1)
+                    self._ResetUnpickler()
+                    # ...and the annotation itself.
+                    self.__unpickler.noload()
+                    # Now loop.
+                else:
+                    # We actually got a 'Result'.
+                    return thing
+
+########################################################################
+# Local Variables:
+# mode: python
+# indent-tabs-mode: nil
+# fill-column: 72
+# End:
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/test/classes/sql_result_stream.py qm-ResultSource/qm/test/classes/sql_result_stream.py
--- qm-clean/qm/test/classes/sql_result_stream.py	1969-12-31 16:00:00.000000000 -0800
+++ qm-ResultSource/qm/test/classes/sql_result_stream.py	2003-07-02 19:15:19.000000000 -0700
@@ -0,0 +1,252 @@
+########################################################################
+#
+# File:   sql_result_stream.py
+# Author: Nathaniel Smith <njs at codesourcery.com>
+# Date:   2003-06-13
+#
+# Contents:
+#   SQLResultStream, SQLResultSource
+#
+# Copyright (c) 2003 by CodeSourcery, LLC.  All rights reserved. 
+#
+# For license terms see the file COPYING.
+#
+########################################################################
+
+########################################################################
+# Imports
+########################################################################
+
+import qm.fields
+from qm.extension          import Extension
+from qm.test.result_stream import ResultStream
+from qm.test.result_reader import ResultReader
+from qm.db                 import quotestr, connect
+from qm.test.result        import Result
+
+########################################################################
+# Classes
+########################################################################
+
+class SQLConnected(Extension):
+    """Mixin class for classes that need a database connection."""
+
+    arguments = [
+        qm.fields.TextField(
+            name = "db_name",
+            title = "Database name",
+            description = "The PostgreSQL database to connect to.",
+            verbatim = "true",
+            default_value = ""),
+        qm.fields.TextField(
+            name = "db_module",
+            title = "Database module",
+            description = "The DB 2.0 module to use.",
+            verbatim = "true",
+            default_value = "pgdb"),
+        qm.fields.PythonField(
+            name = "connection"),
+    ]
+
+    def __init__(self, arguments):
+        super(SQLConnected, self).__init__(arguments)
+
+        if not self.connection:
+            self.connection = connect(self.db_module,
+                                      database=self.db_name)
+
+
+
+class SQLResultStream(ResultStream, SQLConnected):
+    """A `SQLResultStream' writes results out to an SQL database.
+    
+    This class currently supports PostgreSQL only."""
+
+
+    def __init__(self, arguments):
+        super(SQLResultStream, self).__init__(arguments)
+
+        run_id_cursor = self.connection.execute("""
+            SELECT nextval('run_id_seq');
+            """)
+        (self._run_id,) = run_id_cursor.fetchone()
+
+        self.connection.execute("""
+            INSERT INTO runs (run_id) VALUES (%i)
+            """ % (self._run_id,))
+
+
+    def WriteAnnotation(self, key, value):
+
+        self.connection.execute("""
+            INSERT INTO run_annotations (run_id, key, value)
+            VALUES (%i, %s, %s)
+            """ % (self._run_id, quotestr(key), quotestr(value)))
+        
+
+    def WriteResult(self, result):
+
+        self.connection.execute("""
+            INSERT INTO results (run_id, result_id, kind, outcome)
+            VALUES (%i, %s, %s, %s)
+            """ % (self._run_id,
+                   quotestr(result.GetId()),
+                   quotestr(result.GetKind()),
+                   quotestr(result.GetOutcome())))
+
+        for key, value in result.items():
+            self.connection.execute("""
+                INSERT INTO result_annotations (run_id,
+                                                result_id,
+                                                result_kind,
+                                                key,
+                                                value)
+                VALUES (%i, %s, %s, %s, %s)
+                """ % (self._run_id,
+                       quotestr(result.GetId()),
+                       quotestr(result.GetKind()),
+                       quotestr(key),
+                       quotestr(value)))
+
+
+    def Summarize(self):
+
+        self.connection.commit()
+
+
+
+class _Buffer:
+    """A little buffering iterator with one-element rewind."""
+
+    def __init__(self, size, get_more):
+        """Create a '_Buffer'.
+
+        'size' -- the number of items to hold in the buffer at a time.
+
+        'get_more' -- a function taking a number as its sole argument;
+                      should return a list of that many new items (or as
+                      many items are left, whichever is less).
+        """
+
+        self.size = size
+        self.get_more = get_more
+        self.buffer = get_more(size)
+        self.idx = 0
+        # Needed for rewinding over buffer refills:
+        self.last = None
+
+
+    def next(self):
+        """Returns the next item, refilling the buffer if necessary."""
+
+        idx = self.idx
+        if idx == len(self.buffer):
+            self.buffer = self.get_more(self.size)
+            self.idx = 0
+            idx = 0
+        if not self.buffer:
+            raise StopIteration
+        self.idx += 1
+        self.last = self.buffer[idx]
+        return self.buffer[idx]
+
+
+    def rewind(self):
+
+        if self.idx == 0:
+            self.buffer.insert(0, self.last)
+        else:
+            self.idx -= 1
+
+
+    def __iter__(self):
+
+        return self
+
+
+
+class SQLResultReader(ResultReader, SQLConnected):
+    """A `SQLResultReader' reads result in from an SQL database.
+
+    This class currently supports PostgreSQL only."""
+
+    arguments = [
+        qm.fields.IntegerField(
+            name = "run_id",
+            title = "Run ID",
+        ),
+    ]
+
+    def __init__(self, arguments):
+        super(SQLResultReader, self).__init__(arguments)
+
+        self._batch_size = 1000
+
+        self._LoadAnnotations()
+        self._SetupResultCursors()
+
+
+    def _LoadAnnotations(self):
+
+        cursor = self.connection.execute("""
+            SELECT key, value FROM run_annotations
+                              WHERE run_id = %i
+            """ % (self.run_id))
+
+        self._annotations = dict(iter(cursor.fetchone, None))
+
+
+    def GetAnnotations(self):
+
+        return self._annotations
+
+
+    def _SetupResultCursors(self):
+    
+        # Set up our two result cursors.
+        self.connection.execute("""
+            DECLARE results_c CURSOR FOR
+                SELECT result_id, kind, outcome FROM results
+                                                WHERE run_id = %i
+                ORDER BY result_id, kind
+            """ % (self.run_id,))
+        self.connection.execute("""
+            DECLARE annote_c CURSOR FOR
+                SELECT result_id, result_kind, key, value
+                FROM result_annotations WHERE run_id = %i
+                ORDER BY result_id, result_kind
+            """ % (self.run_id,))
+
+        def get_more_results(num):
+            return self.connection.execute("""
+                       FETCH FORWARD %i FROM results_c
+                   """ % (num,)).fetchall()
+        def get_more_annotations(num):
+            return self.connection.execute("""
+                       FETCH FORWARD %i FROM annote_c
+                   """ % (num,)).fetchall()
+
+        self._r_buffer = _Buffer(self._batch_size, get_more_results)
+        self._a_buffer = _Buffer(self._batch_size, get_more_annotations)
+        
+
+    def GetResult(self):
+
+        try:
+            id, kind, outcome = self._r_buffer.next()
+        except StopIteration:
+            return None
+        annotations = {}
+        for result_id, result_kind, key, value in self._a_buffer:
+            if (result_id, result_kind) != (id, kind):
+                self._a_buffer.rewind()
+                break
+            annotations[key] = value
+        return Result(kind, id, outcome, annotations)
+
+########################################################################
+# Local Variables:
+# mode: python
+# indent-tabs-mode: nil
+# fill-column: 72
+# End:
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/test/classes/xml_result_reader.py qm-ResultSource/qm/test/classes/xml_result_reader.py
--- qm-clean/qm/test/classes/xml_result_reader.py	1969-12-31 16:00:00.000000000 -0800
+++ qm-ResultSource/qm/test/classes/xml_result_reader.py	2003-07-02 18:52:31.000000000 -0700
@@ -0,0 +1,95 @@
+########################################################################
+#
+# File:   xml_result_reader.py
+# Author: Nathaniel Smith
+# Date:   2003-06-23
+#
+# Contents:
+#   XMLResultReader
+#
+# Copyright (c) 2003 by CodeSourcery, LLC.  All rights reserved. 
+#
+# For license terms see the file COPYING.
+#
+########################################################################
+
+########################################################################
+# Imports
+########################################################################
+
+import qm.xmlutil
+from   qm.test.file_result_reader import FileResultReader
+from   qm.test.result import Result
+
+########################################################################
+# Classes
+########################################################################
+
+class XMLResultReader(FileResultReader):
+    """Reads in 'Result's from an XML-formatted results file.
+
+    To write such a file, see 'XMLResultStream'."""
+
+    def __init__(self, arguments):
+
+        super(XMLResultReader, self).__init__(arguments)
+
+        document = qm.xmlutil.load_xml(self.file)
+        node = document.documentElement
+        results = qm.xmlutil.get_children(node, "result")
+        self.__node_iterator = iter(results)
+
+        # Read out annotations
+        self._annotations = {}
+        annotation_nodes = qm.xmlutil.get_children(node, "annotation")
+        for node in annotation_nodes:
+            key = node.getAttribute("key")
+            value = qm.xmlutil.get_dom_text(node)
+            self._annotations[key] = value
+
+
+    def GetAnnotations(self):
+
+        return self._annotations
+
+
+    def _result_from_dom(self, node):
+        """Extract a result from a DOM node.
+
+        'node' -- A DOM node corresponding to a "result" element.
+
+        returns -- A 'Result' object."""
+
+        assert node.tagName == "result"
+        # Extract the outcome.
+        outcome = qm.xmlutil.get_child_text(node, "outcome")
+        # Extract the test ID.
+        test_id = node.getAttribute("id")
+        kind = node.getAttribute("kind")
+        # Build a Result.
+        result = Result(kind, test_id, outcome)
+        # Extract properties, one for each property element.
+        for property_node in node.getElementsByTagName("property"):
+            # The name is stored in an attribute.
+            name = property_node.getAttribute("name")
+            # The value is stored in the child text node.
+            value = qm.xmlutil.get_dom_text(property_node)
+            # Store it.
+            result[name] = value
+
+        return result
+
+
+    def GetResult(self):
+
+        try:
+            return self._result_from_dom(self.__node_iterator.next())
+        except StopIteration:
+            return None
+
+########################################################################
+# Local Variables:
+# mode: python
+# indent-tabs-mode: nil
+# fill-column: 72
+# End:
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/test/classes/xml_result_source.py qm-ResultSource/qm/test/classes/xml_result_source.py
--- qm-clean/qm/test/classes/xml_result_source.py	2003-06-25 02:03:35.000000000 -0700
+++ qm-ResultSource/qm/test/classes/xml_result_source.py	1969-12-31 16:00:00.000000000 -0800
@@ -1,82 +0,0 @@
-########################################################################
-#
-# File:   xml_result_source.py
-# Author: Nathaniel Smith
-# Date:   2003-06-23
-#
-# Contents:
-#   XMLResultSource
-#
-# Copyright (c) 2003 by CodeSourcery, LLC.  All rights reserved. 
-#
-# For license terms see the file COPYING.
-#
-########################################################################
-
-########################################################################
-# Imports
-########################################################################
-
-import qm.xmlutil
-from   qm.test.file_result_source import FileResultSource
-from   qm.test.result import Result
-
-########################################################################
-# Classes
-########################################################################
-
-class XMLResultSource(FileResultSource):
-    """Reads in 'Result's from an XML-formatted results file.
-
-    To write such a file, see 'XMLResultStream'."""
-
-    def __init__(self, arguments):
-
-        super(XMLResultSource, self).__init__(arguments)
-
-        document = qm.xmlutil.load_xml(self.file)
-        node = document.documentElement
-        results = qm.xmlutil.get_children(node, "result")
-        self.__node_iterator = iter(results)
-
-
-    def _result_from_dom(self, node):
-        """Extract a result from a DOM node.
-
-        'node' -- A DOM node corresponding to a "result" element.
-
-        returns -- A 'Result' object."""
-
-        assert node.tagName == "result"
-        # Extract the outcome.
-        outcome = qm.xmlutil.get_child_text(node, "outcome")
-        # Extract the test ID.
-        test_id = node.getAttribute("id")
-        kind = node.getAttribute("kind")
-        # Build a Result.
-        result = Result(kind, test_id, outcome)
-        # Extract properties, one for each property element.
-        for property_node in node.getElementsByTagName("property"):
-            # The name is stored in an attribute.
-            name = property_node.getAttribute("name")
-            # The value is stored in the child text node.
-            value = qm.xmlutil.get_dom_text(property_node)
-            # Store it.
-            result[name] = value
-
-        return result
-
-
-    def GetResult(self):
-
-        try:
-            return self._result_from_dom(self.__node_iterator.next())
-        except StopIteration:
-            return None
-
-########################################################################
-# Local Variables:
-# mode: python
-# indent-tabs-mode: nil
-# fill-column: 72
-# End:
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/test/classes/xml_result_stream.py qm-ResultSource/qm/test/classes/xml_result_stream.py
--- qm-clean/qm/test/classes/xml_result_stream.py	2003-04-13 23:06:40.000000000 -0700
+++ qm-ResultSource/qm/test/classes/xml_result_stream.py	2003-07-02 13:18:34.000000000 -0700
@@ -36,7 +36,7 @@
     def __init__(self, arguments):
 
         # Initialize the base class.
-        FileResultStream.__init__(self, arguments)
+        super(XMLResultStream, self).__init__(arguments)
         
         # Create an XML document, since the DOM API requires you
         # to have a document when you create a node.
@@ -53,6 +53,17 @@
         self.file.write("<results>\n")
 
 
+    def WriteAnnotation(self, key, value):
+
+            element = self.__document.createElement("annotation")
+            element.setAttribute("key", key)
+            text = self.__document.createTextNode(value)
+            element.appendChild(text)
+            element.writexml(self.file)
+            # Following increases readability of output:
+            self.file.write("\n")
+
+
     def WriteResult(self, result):
         """Output a test or resource result.
 
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/test/cmdline.py qm-ResultSource/qm/test/cmdline.py
--- qm-clean/qm/test/cmdline.py	2003-06-25 02:06:06.000000000 -0700
+++ qm-ResultSource/qm/test/cmdline.py	2003-07-02 00:42:20.000000000 -0700
@@ -1227,6 +1227,10 @@
         # written.
         streams = self.__GetResultStreams(suite_ids)
         
+        # Send the annotations through.
+        for s in streams:
+            s.WriteAllAnnotations(results.GetAnnotations())
+
         # Get the expected outcomes.
         outcomes = self.__GetExpectedOutcomes()
 
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/test/execution_engine.py qm-ResultSource/qm/test/execution_engine.py
--- qm-clean/qm/test/execution_engine.py	2003-06-16 16:33:29.000000000 -0700
+++ qm-ResultSource/qm/test/execution_engine.py	2003-07-02 02:03:25.000000000 -0700
@@ -27,6 +27,7 @@
 from   result import *
 import select
 import sys
+import time
 
 ########################################################################
 # Classes
@@ -133,6 +134,12 @@
 
         returns -- True if any tests had unexpected outcomes."""
 
+        # Write out all the currently known annotations.
+        start_time_str = qm.common.format_time_iso(time.time())
+        for rs in self.__result_streams:
+            rs.WriteAllAnnotations(self.__context)
+            rs.WriteAnnotation("qmtest.run.start_time", start_time_str)
+
         # Start all of the targets.
         for target in self.__targets:
             target.Start(self.__response_queue, self)
@@ -153,7 +160,9 @@
             
             # Let all of the result streams know that the test run is
             # complete.
+            end_time_str = qm.common.format_time_iso(time.time())
             for rs in self.__result_streams:
+                rs.WriteAnnotation("qmtest.run.end_time", end_time_str)
                 rs.Summarize()
 
         return self.__any_unexpected_outcomes
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/test/file_result_reader.py qm-ResultSource/qm/test/file_result_reader.py
--- qm-clean/qm/test/file_result_reader.py	1969-12-31 16:00:00.000000000 -0800
+++ qm-ResultSource/qm/test/file_result_reader.py	2003-07-02 18:50:34.000000000 -0700
@@ -0,0 +1,78 @@
+########################################################################
+#
+# File:   file_result_reader.py
+# Author: Nathaniel Smith
+# Date:   2003-06-23
+#
+# Contents:
+#   FileResultReader
+#
+# Copyright (c) 2003 by CodeSourcery, LLC.  All rights reserved. 
+#
+# For license terms see the file COPYING.
+#
+########################################################################
+
+########################################################################
+# Imports
+########################################################################
+
+import qm.fields
+from qm.test.result_reader import ResultReader
+import sys
+
+########################################################################
+# Classes
+########################################################################
+
+class FileResultReader(ResultReader):
+    """A 'FileResultReader' gets its input from a file.
+
+    A 'FileResultReader' is an abstract base class for other result
+    reader classes that read results from a single file.  The file
+    from which results should be read can be specified using either
+    the 'filename' argument or the 'file' argument.  The latter is for
+    use by QMTest internally."""
+
+
+    arguments = [
+        qm.fields.TextField(
+            name = "filename",
+            title = "File Name",
+            description = """The name of the file.
+
+            All results will be read from the file indicated.  If no
+            filename is specified, or the filename specified is "-",
+            the standard input will be used.""",
+            verbatim = "true",
+            default_value = ""),
+        qm.fields.PythonField(
+            name = "file"),
+    ]
+
+    _is_binary_file = 0
+    """If true, the file written is a binary file.
+
+    This flag can be overridden by derived classes."""
+    
+    def __init__(self, arguments):
+
+        super(FileResultReader, self).__init__(arguments)
+
+        if not self.file:
+            if self.filename and self.filename != "-":
+                if self._is_binary_file:
+                    mode = "rb"
+                else:
+                    mode = "r"
+                self.file = open(self.filename, mode, 0)
+            else:
+                self.file = sys.stdin
+
+
+########################################################################
+# Local Variables:
+# mode: python
+# indent-tabs-mode: nil
+# fill-column: 72
+# End:
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/test/file_result_source.py qm-ResultSource/qm/test/file_result_source.py
--- qm-clean/qm/test/file_result_source.py	2003-06-25 02:03:35.000000000 -0700
+++ qm-ResultSource/qm/test/file_result_source.py	1969-12-31 16:00:00.000000000 -0800
@@ -1,78 +0,0 @@
-########################################################################
-#
-# File:   file_result_source.py
-# Author: Nathaniel Smith
-# Date:   2003-06-23
-#
-# Contents:
-#   FileResultSource
-#
-# Copyright (c) 2003 by CodeSourcery, LLC.  All rights reserved. 
-#
-# For license terms see the file COPYING.
-#
-########################################################################
-
-########################################################################
-# Imports
-########################################################################
-
-import qm.fields
-from qm.test.result_source import ResultSource
-import sys
-
-########################################################################
-# Classes
-########################################################################
-
-class FileResultSource(ResultSource):
-    """A 'FileResultSource' gets its input from a file.
-
-    A 'FileResultSource' is an abstract base class for other result
-    source classes that read results from a single file.  The file
-    from which results should be read can be specified using either
-    the 'filename' argument or the 'file' argument.  The latter is for
-    use by QMTest internally."""
-
-
-    arguments = [
-        qm.fields.TextField(
-            name = "filename",
-            title = "File Name",
-            description = """The name of the file.
-
-            All results will be read from the file indicated.  If no
-            filename is specified, or the filename specified is "-",
-            the standard input will be used.""",
-            verbatim = "true",
-            default_value = ""),
-        qm.fields.PythonField(
-            name = "file"),
-    ]
-
-    _is_binary_file = 0
-    """If true, the file written is a binary file.
-
-    This flag can be overridden by derived classes."""
-    
-    def __init__(self, arguments):
-
-        super(FileResultSource, self).__init__(arguments)
-
-        if not self.file:
-            if self.filename and self.filename != "-":
-                if self._is_binary_file:
-                    mode = "rb"
-                else:
-                    mode = "r"
-                self.file = open(self.filename, mode, 0)
-            else:
-                self.file = sys.stdin
-
-
-########################################################################
-# Local Variables:
-# mode: python
-# indent-tabs-mode: nil
-# fill-column: 72
-# End:
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/test/result_reader.py qm-ResultSource/qm/test/result_reader.py
--- qm-clean/qm/test/result_reader.py	1969-12-31 16:00:00.000000000 -0800
+++ qm-ResultSource/qm/test/result_reader.py	2003-07-02 18:51:00.000000000 -0700
@@ -0,0 +1,64 @@
+########################################################################
+#
+# File:   result_reader.py
+# Author: Nathaniel Smith
+# Date:   2003-06-23
+#
+# Contents:
+#   QMTest ResultReader class.
+#
+# Copyright (c) 2003 by CodeSourcery, LLC.  All rights reserved. 
+#
+# For license terms see the file COPYING.
+#
+########################################################################
+
+########################################################################
+# Imports
+########################################################################
+
+import qm.extension
+
+########################################################################
+# Classes
+########################################################################
+
+class ResultReader(qm.extension.Extension):
+    """A 'ResultReader' provides access to stored test results.
+
+    For instance, a 'ResultReader' may load 'Result's from a pickle
+    file or an XML file.
+
+    This is an abstract class.
+
+    See also 'ResultStream'."""
+
+    kind = "result_reader"
+
+    def GetAnnotations(self):
+        """Return this run's dictionary of annotations."""
+
+        # For backwards compatibility, don't raise an exception.
+        return {}
+
+
+    def GetResult(self):
+        """Return the next 'Result' from this reader.
+
+        returns -- A 'Result', or 'None' if there are no more results.
+        """
+
+        raise NotImplementedError
+
+
+    def __iter__(self):
+        """A 'ResultReader' can be iterated over."""
+
+        return iter(self.GetResult, None)
+
+########################################################################
+# Local Variables:
+# mode: python
+# indent-tabs-mode: nil
+# fill-column: 72
+# End:
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/test/result_source.py qm-ResultSource/qm/test/result_source.py
--- qm-clean/qm/test/result_source.py	2003-06-25 02:03:35.000000000 -0700
+++ qm-ResultSource/qm/test/result_source.py	1969-12-31 16:00:00.000000000 -0800
@@ -1,57 +0,0 @@
-########################################################################
-#
-# File:   result_source.py
-# Author: Nathaniel Smith
-# Date:   2003-06-23
-#
-# Contents:
-#   QMTest ResultSource class.
-#
-# Copyright (c) 2003 by CodeSourcery, LLC.  All rights reserved. 
-#
-# For license terms see the file COPYING.
-#
-########################################################################
-
-########################################################################
-# Imports
-########################################################################
-
-import qm.extension
-
-########################################################################
-# Classes
-########################################################################
-
-class ResultSource(qm.extension.Extension):
-    """A 'ResultSource' provides access to stored test results.
-
-    For instance, a 'ResultSource' may load 'Result's from a pickle
-    file or an XML file.
-
-    This is an abstract class.
-
-    See also 'ResultStream'."""
-
-    kind = "result_source"
-
-    def GetResult(self):
-        """Return the next 'Result' from this source.
-
-        returns -- A 'Result', or 'None' if there are no more results.
-        """
-
-        raise NotImplementedError
-
-
-    def __iter__(self):
-        """A 'ResultSource' can be iterated over."""
-
-        return iter(self.GetResult, None)
-
-########################################################################
-# Local Variables:
-# mode: python
-# indent-tabs-mode: nil
-# fill-column: 72
-# End:
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/test/result_stream.py qm-ResultSource/qm/test/result_stream.py
--- qm-clean/qm/test/result_stream.py	2003-05-16 00:31:18.000000000 -0700
+++ qm-ResultSource/qm/test/result_stream.py	2003-07-02 00:40:38.000000000 -0700
@@ -52,9 +52,39 @@
            name = "suite_ids"),
         ]
     
+    def WriteAnnotation(self, key, value):
+        """Output an annotation for this run.
+
+        Subclasses should override this if they want to store/display
+        annotations; the default implementation simply discards them.
+
+        'key' -- the key value as a string.
+
+        'value' -- the value of this annotation as a string."""
+
+        pass
+    
+
+    def WriteAllAnnotations(self, annotations):
+        """Output all annotations in 'annotations' to this stream.
+
+        Currently this is the same as making repeated calls to
+        'WriteAnnotation', but in the future, as special annotation
+        types like timestamps are added, this will do the work of
+        dispatching to functions like 'WriteTimestamp'.
+
+        Should not be overridden by subclasses."""
+
+        for key, value in annotations.iteritems():
+            self.WriteAnnotation(key, value)
+
+
     def WriteResult(self, result):
         """Output a test result.
 
+        Subclasses must override this method; the default
+        implementation raises a 'NotImplementedError'.
+
         'result' -- A 'Result'."""
 
         raise NotImplementedError
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/qm/test/web/web.py qm-ResultSource/qm/test/web/web.py
--- qm-clean/qm/test/web/web.py	2003-06-25 02:03:35.000000000 -0700
+++ qm-ResultSource/qm/test/web/web.py	2003-07-02 19:50:05.000000000 -0700
@@ -1203,13 +1203,19 @@
     A 'StorageResultsStream' does not write any output.  It simply
     stores the results for future display."""
 
-    def __init__(self):
+    def __init__(self, arguments):
         """Construct a 'StorageResultsStream'."""
 
-        ResultStream.__init__(self, {})
+        super(StorageResultsStream, self).__init__(arguments)
         self.__test_results = {}
         self.__test_results_in_order = []
         self.__resource_results = {}
+        # Put in a note.  No software currently pays any attention to
+        # this key, but it's useful to mark runs that were done from
+        # the GUI, because they may be an amalgamation of multiple
+        # runs, and therefore cannot be trusted to describe a single
+        # version of the software under test.
+        self.__annotations = { "qmtest.created_from_gui": "true" }
         # The stream is not finished yet.
         self.__is_finished = 0
         
@@ -1220,6 +1226,17 @@
         self.__lock = Lock()
 
 
+    def GetAnnotations(self):
+        """Return the annotations for this run."""
+
+        return self.__annotations
+
+
+    def WriteAnnotation(self, key, value):
+
+        self.__annotations[key] = value
+
+
     def WriteResult(self, result):
         """Output a test result.
 
@@ -1573,7 +1590,7 @@
 
         self.__expected_outcomes = expectations
         # There are no results yet.        
-        self.__results_stream = StorageResultsStream()
+        self.__results_stream = StorageResultsStream({})
         self.__results_stream.Summarize()
         # There is no execution thread.
         self.__execution_thread = None
@@ -1646,7 +1663,7 @@
         # Eliminate the old results stream.
         del self.__results_stream
         # And create a new one.
-        self.__results_stream = StorageResultsStream()
+        self.__results_stream = StorageResultsStream({})
         self.__results_stream.Summarize()
 
         # Redirect to the main page.
@@ -1898,6 +1915,8 @@
         # Create a results stream for storing the results.
         rsc = qm.test.cmdline.get_qmtest().GetFileResultStreamClass()
         rs = rsc({ "file" : s })
+        # Write all the annotations.
+        rs.WriteAllAnnotations(self.__results_stream.GetAnnotations())
         # Write all the results.
         for r in self.__results_stream.GetTestResults().values():
             rs.WriteResult(r)
@@ -2331,7 +2350,9 @@
         # Read the results.
         results = qm.test.base.load_results(f, self.GetDatabase())
         # Enter them into a new results stream.
-        self.__results_stream = StorageResultsStream()
+        self.__results_stream = StorageResultsStream({})
+        annotations = results.GetAnnotations()
+        self.__results_stream.WriteAllAnnotations(annotations)
         for r in results:
             self.__results_stream.WriteResult(r)
         self.__results_stream.Summarize()
Binary files qm-clean/results.qmr and qm-ResultSource/results.qmr differ
diff -urN --exclude='*~' --exclude='.*' --exclude=CVS --exclude='*.pyo' --exclude='*.pyc' --exclude=build --exclude=GNUmakefile --exclude=config.log --exclude=config.status --exclude=setup_path.py --exclude=qm.sh --exclude=qm.spec qm-clean/scripts/create-results-database.py qm-ResultSource/scripts/create-results-database.py
--- qm-clean/scripts/create-results-database.py	1969-12-31 16:00:00.000000000 -0800
+++ qm-ResultSource/scripts/create-results-database.py	2003-07-02 18:29:53.000000000 -0700
@@ -0,0 +1,103 @@
+#!/usr/bin/env python
+
+########################################################################
+#
+# File:   create-results-database.py
+# Author: Nathaniel Smith
+# Date:   2003-07-02
+#
+# Contents:
+#   Script to set up a PostgreSQL results database.
+#
+# Copyright (c) 2003 by CodeSourcery, LLC.  All rights reserved. 
+#
+# For license terms see the file COPYING.
+#
+########################################################################
+
+########################################################################
+# Imports
+########################################################################
+
+import sys
+import pgdb
+
+########################################################################
+# Script
+########################################################################
+
+if len(sys.argv) != 2:
+    print "Usage: %s <database_name>" % (sys.argv[0],)
+    sys.exit(1)
+
+dbname = sys.argv[1]
+cxn = pgdb.connect(database=dbname)
+cursor = cxn.cursor()
+
+cursor.execute("""
+    CREATE TABLE db_schema_version (
+        version INT
+    )
+    """)
+cursor.execute("""
+    INSERT INTO db_schema_version (version) VALUES (1)
+    """)
+
+cursor.execute("""
+    CREATE SEQUENCE run_id_seq
+    """)
+
+cursor.execute("""
+    CREATE TABLE runs (
+        run_id INT PRIMARY KEY
+    )
+    """)
+
+cursor.execute("""
+    CREATE TABLE run_annotations (
+        run_id INT NOT NULL,
+        key TEXT NOT NULL,
+        value TEXT NOT NULL,
+        FOREIGN KEY (run_id) REFERENCES runs (run_id),
+        PRIMARY KEY (run_id, key)
+    )
+    """)
+        
+cursor.execute("""
+    CREATE TABLE results (
+        run_id INT NOT NULL,
+        result_id TEXT NOT NULL,
+        kind TEXT NOT NULL,
+        outcome TEXT NOT NULL,
+        FOREIGN KEY (run_id) REFERENCES runs (run_id),
+        PRIMARY KEY (run_id, result_id, kind)
+    )
+    """)
+cursor.execute("""
+    CREATE INDEX results_outcome_idx ON results (run_id, outcome)
+    """)
+cursor.execute("""
+    CREATE INDEX results_kind_idx ON results (run_id, kind)
+    """)
+
+cursor.execute("""
+    CREATE TABLE result_annotations (
+        run_id INT NOT NULL,
+        result_id TEXT NOT NULL,
+        result_kind TEXT NOT NULL,
+        key TEXT NOT NULL,
+        value TEXT NOT NULL,
+        FOREIGN KEY (run_id, result_id, result_kind)
+            REFERENCES results (run_id, result_id, kind),
+        PRIMARY KEY (run_id, result_id, result_kind, key)
+    )
+    """)
+
+cxn.commit()
+
+########################################################################
+# Local Variables:
+# mode: python
+# indent-tabs-mode: nil
+# fill-column: 72
+# End:


More information about the qmtest mailing list