[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