PATCH: Generalize "qmtest create"
Mark Mitchell
mark at codesourcery.com
Thu Jul 21 08:09:43 UTC 2005
This patch adds support for generating new entries in a test database
from the command line. For example, you can say:
qmtest create --id=simple -a program=testprog test command.ExecTest
to create a new test named "simple" in your database, using the
"command.ExecTest" test class, with the "program" argument
"testprog". This will work independently of the storage format used
by your database.
The old "qmtest create" behavior (generating an XML file) still works;
if you don't use "--id" you get the old behavior.
Committed.
--
Mark Mitchell
CodeSourcery, LLC
mark at codesourcery.com
2005-07-21 Mark Mitchell <mark at codesourcery.com>
* qm/extension.py (Extension.GetClassName): New method.
(Extension.GetExplicitArguments): Likewise.
(Extension.MakeDomEelement): Use GetExplicitArguments.
(parse_descriptor): Add extension_loader.
* qm/test/cmdline.py (qm.test.runnable.Runnable): Import it.
(QMTest.extension_id_option_spec): New variable.
(QMTest.conflicting_option_specs): Update.
(QMTest.__ExecuteCreate): Support writing extensions to the
database.
* qm/test/database.py (Database.GetExtension): Fix typo.
* qm/test/doc/reference.xml: Document --id option to "qmtest
create".
* qm/test/share/messages/diagnostics.txt (db not modifiable): New
message.
(no db specified): Update message.
Index: qm/extension.py
===================================================================
RCS file: /home/qm/Repository/qm/qm/extension.py,v
retrieving revision 1.18
diff -c -5 -p -r1.18 extension.py
*** qm/extension.py 10 Jun 2005 20:49:40 -0000 1.18
--- qm/extension.py 21 Jul 2005 08:05:39 -0000
*************** class Extension(object):
*** 163,186 ****
if field is None:
raise AttributeError, name
return field.GetDefaultValue()
! def MakeDomElement(self, document, element = None):
! """Create a DOM node for 'self'.
- 'document' -- The DOM document that will contain the new
- element.
-
- 'element' -- If not 'None' the extension element to which items
- will be added. Otherwise, a new element will be created by this
- function.
! returns -- A new DOM element corresponding to an instance of the
! extension class. The caller is responsible for attaching it to
! the 'document'."""
# Get all of the arguments.
arguments = get_class_arguments_as_dictionary(self.__class__)
# Determine which subset of the 'arguments' have been set
# explicitly.
explicit_arguments = {}
--- 163,187 ----
if field is None:
raise AttributeError, name
return field.GetDefaultValue()
! def GetClassName(self):
! """Return the name of the extension class.
!
! returns -- A string giving the name of this etension class."""
!
! return get_extension_class_name(self.__class__)
! def GetExplicitArguments(self):
! """Return the arguments to this extension instance.
+ returns -- A dictionary mapping argument names to their
+ values. Computed arguments are ommitted from the
+ dictionary."""
+
# Get all of the arguments.
arguments = get_class_arguments_as_dictionary(self.__class__)
# Determine which subset of the 'arguments' have been set
# explicitly.
explicit_arguments = {}
*************** class Extension(object):
*** 189,199 ****
if field.IsComputed():
continue
if self.__dict__.has_key(name):
explicit_arguments[name] = self.__dict__[name]
! return make_dom_element(self.__class__, explicit_arguments,
document, element)
def MakeDomDocument(self):
"""Create a DOM document for 'self'.
--- 190,218 ----
if field.IsComputed():
continue
if self.__dict__.has_key(name):
explicit_arguments[name] = self.__dict__[name]
! return explicit_arguments
!
!
! def MakeDomElement(self, document, element = None):
! """Create a DOM node for 'self'.
!
! 'document' -- The DOM document that will contain the new
! element.
!
! 'element' -- If not 'None' the extension element to which items
! will be added. Otherwise, a new element will be created by this
! function.
!
! returns -- A new DOM element corresponding to an instance of the
! extension class. The caller is responsible for attaching it to
! the 'document'."""
!
! return make_dom_element(self.__class__,
! self.GetExplicitArguments(),
document, element)
def MakeDomDocument(self):
"""Create a DOM document for 'self'.
*************** def get_class_arguments(extension_class)
*** 236,245 ****
--- 255,265 ----
arguments in the class hierarchy."""
assert issubclass(extension_class, Extension)
return extension_class._argument_list
+
def get_class_arguments_as_dictionary(extension_class):
"""Return the arguments associated with 'extension_class'.
'extension_class' -- A class derived from 'Extension'.
*************** def read_extension_file(file, class_load
*** 487,511 ****
document = qm.xmlutil.load_xml(file)
return parse_dom_element(document.documentElement,
class_loader,
attachment_store)
!
! def parse_descriptor(descriptor, class_loader):
"""Parse a descriptor representing an instance of 'Extension'.
'descriptor' -- A string representing an instance of 'Extension'.
The 'descriptor' has the form 'class(arg1 = "val1", arg2 = "val2",
...)'. The arguments and the parentheses are optional.
! If 'class' names a file in the file system, it is assumed to be an
! extension file. Any attributes provided in the descriptor
! override those in the file.
! 'class_loader' -- A callable. The callable will be passed the
! name of the extension class and must return the actual class
! object.
returns -- A pair ('extension_class', 'arguments') containing the
extension class (a class derived from 'Extension') and the
arguments (a dictionary mapping names to values) stored in the
'element'. The 'arguments' will have already been processed by
--- 507,535 ----
document = qm.xmlutil.load_xml(file)
return parse_dom_element(document.documentElement,
class_loader,
attachment_store)
!
! def parse_descriptor(descriptor, class_loader, extension_loader = None):
"""Parse a descriptor representing an instance of 'Extension'.
'descriptor' -- A string representing an instance of 'Extension'.
The 'descriptor' has the form 'class(arg1 = "val1", arg2 = "val2",
...)'. The arguments and the parentheses are optional.
! 'class_loader' -- A callable that, when passed the name of the
! extension class, will return the actual Python class object.
! 'extension_loader' -- A callable that loads an existing extension
! given the name of that extension and returns a tuple '(class,
! arguments)' where 'class' is a class derived from 'Extension'. If
! 'extension_loader' is 'None', or if the 'class' returned is
! 'None', then if a file exists named 'class', the extension is read
! from 'class' as XML. Any arguments returned by the extension
! loader or read from the file system are overridden by the
! arguments explicitly provided in the descriptor.
returns -- A pair ('extension_class', 'arguments') containing the
extension class (a class derived from 'Extension') and the
arguments (a dictionary mapping names to values) stored in the
'element'. The 'arguments' will have already been processed by
*************** def parse_descriptor(descriptor, class_l
*** 520,536 ****
else:
# The class name is the part of the descriptor up to the
# parenthesis.
class_name = descriptor[:open_paren]
! # Load the extension class.
! if os.path.exists(class_name):
! extension_class, orig_arguments \
! = read_extension_file(open(class_name), class_loader)
! else:
! extension_class = class_loader(class_name)
! orig_arguments = {}
arguments = {}
# Parse the arguments.
if open_paren != -1:
--- 544,567 ----
else:
# The class name is the part of the descriptor up to the
# parenthesis.
class_name = descriptor[:open_paren]
! # Load the extension, if it already exists.
! extension_class = None
! if extension_loader:
! extension = extension_loader(class_name)
! if extension:
! extension_class = extension.__class__
! orig_arguments = extension.GetExplicitArguments()
! if not extension_class:
! if os.path.exists(class_name):
! extension_class, orig_arguments \
! = read_extension_file(open(filename), class_loader)
! else:
! extension_class = class_loader(class_name)
! orig_arguments = {}
arguments = {}
# Parse the arguments.
if open_paren != -1:
Index: qm/test/cmdline.py
===================================================================
RCS file: /home/qm/Repository/qm/qm/test/cmdline.py,v
retrieving revision 1.108
diff -c -5 -p -r1.108 cmdline.py
*** qm/test/cmdline.py 6 Jul 2005 06:24:17 -0000 1.108
--- qm/test/cmdline.py 21 Jul 2005 08:05:39 -0000
*************** import qm.platform
*** 27,36 ****
--- 27,37 ----
from qm.test import test
from qm.test.result import Result
from qm.test.context import *
from qm.test.execution_engine import *
from qm.test.result_stream import ResultStream
+ from qm.test.runnable import Runnable
from qm.test.report import ReportGenerator
from qm.trace import *
import qm.test.web.web
import qm.xmlutil
import Queue
*************** class QMTest:
*** 138,147 ****
--- 139,155 ----
"o",
"output",
"FILE",
"Write the extension to FILE.",
)
+
+ extension_id_option_spec = (
+ "i",
+ "id",
+ "NAME",
+ "Write the extension to the database as NAME.",
+ )
output_option_spec = (
"o",
"output",
"FILE",
*************** class QMTest:
*** 298,307 ****
--- 306,316 ----
# Groups of options that should not be used together.
conflicting_option_specs = (
( output_option_spec, no_output_option_spec ),
( concurrent_option_spec, targets_option_spec ),
+ ( extension_output_option_spec, extension_id_option_spec ),
)
global_options_spec = [
help_option_spec,
version_option_spec,
*************** class QMTest:
*** 315,342 ****
"""Create (or update) an extension.
The EXTENSION-KIND indicates what kind of extension to
create; it must be one of """ + __extension_kinds_string + """.
! The CLASS-NAME indicates the name of the extension class. It
! must have the form 'MODULE.CLASS'. For a list of available
! extension classes use "qmtest extensions". If the extension
! class takes arguments, those arguments can be specified after
! the CLASS-NAME as show above.
Any "--attribute" options are processed before the arguments
specified after the class name. Therefore, the "--attribute"
options can be overridden by the arguments provided after the
CLASS-NAME. If no attributes are specified, the parentheses
following the 'CLASS-NAME' can be omitted.
! The extension instance is written to the file given by the
! "--output" option, or to the standard output if no "--output"
! option is present.""",
( attribute_option_spec,
help_option_spec,
! extension_output_option_spec,
),
),
("create-target",
"Create (or update) a target specification.",
--- 324,358 ----
"""Create (or update) an extension.
The EXTENSION-KIND indicates what kind of extension to
create; it must be one of """ + __extension_kinds_string + """.
! The CLASS-NAME indicates the name of the extension class, or
! the name of an existing extension object. If the CLASS-NAME
! is the name of a extension in the test database, then the
!
! In the former case, it must have the form 'MODULE.CLASS'. For
! a list of available extension classes use "qmtest extensions".
! If the extension class takes arguments, those arguments can be
! specified after the CLASS-NAME as show above. In the latter
! case,
Any "--attribute" options are processed before the arguments
specified after the class name. Therefore, the "--attribute"
options can be overridden by the arguments provided after the
CLASS-NAME. If no attributes are specified, the parentheses
following the 'CLASS-NAME' can be omitted.
! If the "--id" option is given, the extension is written to the
! database. Otherwise, if the "--output" option is given, the
! extension is written as XML to the file indicated. If neither
! option is given, the extension is written as XML to the
! standard output.""",
( attribute_option_spec,
help_option_spec,
! extension_id_option_spec,
! extension_output_option_spec
),
),
("create-target",
"Create (or update) a target specification.",
*************** Valid formats are %s.
*** 882,917 ****
# Get the extension kind.
kind = self.__arguments[0]
self.__CheckExtensionKind(kind)
! # Get the --attribute options.
! arguments = self.__GetAttributeOptions()
# Process the descriptor.
(extension_class, more_arguments) \
= (qm.extension.parse_descriptor
! (self.__arguments[1],
! lambda n: \
! qm.test.base.get_extension_class(n, kind, database)))
# Validate the --attribute options.
arguments = qm.extension.validate_arguments(extension_class,
arguments)
# Override the --attribute options with the arguments provided
# as part of the descriptor.
arguments.update(more_arguments)
! # Figure out what file to use.
! filename = self.GetCommandOption("output")
! if filename is not None:
! file = open(filename, "w")
else:
! file = sys.stdout
!
! # Write out the file.
! qm.extension.write_extension_file(extension_class, arguments, file)
return 0
def __ExecuteCreateTdb(self, db_path):
--- 898,954 ----
# Get the extension kind.
kind = self.__arguments[0]
self.__CheckExtensionKind(kind)
! extension_id = self.GetCommandOption("id")
! if extension_id is not None:
! if not database:
! raise QMException, qm.error("no db specified")
! if not database.IsModifiable():
! raise QMException, qm.error("db not modifiable")
! extension_loader = database.GetExtension
! else:
! extension_loader = None
+ class_loader = lambda n: \
+ qm.test.base.get_extension_class(n, kind, database)
+
# Process the descriptor.
(extension_class, more_arguments) \
= (qm.extension.parse_descriptor
! (self.__arguments[1], class_loader, extension_loader))
# Validate the --attribute options.
+ arguments = self.__GetAttributeOptions()
arguments = qm.extension.validate_arguments(extension_class,
arguments)
# Override the --attribute options with the arguments provided
# as part of the descriptor.
arguments.update(more_arguments)
! if extension_id is not None:
! # Create the extension instance. Objects derived from
! # Runnable require magic additional arguments.
! if issubclass(extension_class, Runnable):
! extras = { Runnable.EXTRA_ID : extension_id,
! Runnable.EXTRA_DATABASE : database }
! else:
! extras = {}
! extension = extension_class(arguments, **extras)
! # Write the extension to the database.
! database.WriteExtension(extension_id, extension)
else:
! # Figure out what file to use.
! filename = self.GetCommandOption("output")
! if filename is not None:
! file = open(filename, "w")
! else:
! file = sys.stdout
! # Write out the file.
! qm.extension.write_extension_file(extension_class, arguments,
! file)
return 0
def __ExecuteCreateTdb(self, db_path):
Index: qm/test/database.py
===================================================================
RCS file: /home/qm/Repository/qm/qm/test/database.py,v
retrieving revision 1.43
diff -c -5 -p -r1.43 database.py
*** qm/test/database.py 21 Jul 2005 05:06:37 -0000 1.43
--- qm/test/database.py 21 Jul 2005 08:05:39 -0000
*************** class Database(qm.extension.Extension):
*** 620,630 ****
implements this generic method in terms of the special-purpose
methods."""
for kind in (Database.TEST, Database.RESOURCE):
try:
! item = self.GetItem(kind, id).GetItem()
except NoSuchItemError:
pass
try:
return self.GetSuite(id)
--- 620,630 ----
implements this generic method in terms of the special-purpose
methods."""
for kind in (Database.TEST, Database.RESOURCE):
try:
! return self.GetItem(kind, id).GetItem()
except NoSuchItemError:
pass
try:
return self.GetSuite(id)
Index: qm/test/doc/reference.xml
===================================================================
RCS file: /home/qm/Repository/qm/qm/test/doc/reference.xml,v
retrieving revision 1.41
diff -c -5 -p -r1.41 reference.xml
*** qm/test/doc/reference.xml 21 Jul 2005 05:06:38 -0000 1.41
--- qm/test/doc/reference.xml 21 Jul 2005 08:05:46 -0000
***************
*** 687,728 ****
create a new test or resource. For a list of the kinds of
extensions supported by &qmtest;, run <command>&qmtest-cmd;
extensions</command>. The <replaceable>kind</replaceable>
must be one of these extension kinds.</para>
<para>The descriptor specifies an extension class and (optionally)
attributes for that extension class. The form of the descriptor is
<command><replaceable>class</replaceable>(<replaceable>attributes</replaceable>)</command>,
where the attributes are of the form
<command><replaceable>attr</replaceable> =
"<replaceable>val</replaceable>"</command>. If there
are no attributes, the parentheses may be omitted.</para>
<para>The <replaceable>class</replaceable> may be either the path
! to an extension file or a QMTest class name in the form
<replaceable>module.class</replaceable>. If the
! <replaceable>class</replaceable> is the path to an extension file
! (such as an existing test or resource file), the class name used
! is the one provided in the file; otherwise the class named used is
! the name provided on the command line.</para>
<para>The attributes used to construct the extension instance come
! from three sources: the attributes in the extension file (if the
<replaceable>class</replaceable> is the path to an extension
file), the <option>&dashdash;attribute</option> options provided
on the command line, and the explicit attributes provided in the
descriptor. If multiple values for the the same attribute name
are provided, the value used is taken from the first source in the
following list for which there is a value: the rightmost attribute
provided in the descriptor, the extension file, or the rightmost
<option>&dashdash;attribute</option> present on the command
line.</para>
- <para>The new extension file is written to the file specified with
- the <option>&dashdash;output</option> option, or to the standard
- output if no <option>&dashdash;output</option> is specified.</para>
-
<para>The <command>create</command> command accepts these
options:</para>
<variablelist>
<varlistentry>
--- 687,736 ----
create a new test or resource. For a list of the kinds of
extensions supported by &qmtest;, run <command>&qmtest-cmd;
extensions</command>. The <replaceable>kind</replaceable>
must be one of these extension kinds.</para>
+ <para>If the <option>&dashdash;id</option> option is provided then
+ the new instance is created in the test database. The argument to
+ the <option>&dashdash;id</option> option gives the name of the
+ instance. Otherwise, the extension is written as XML to the
+ filename specified by <option>&dashdash;output</option> option, or
+ to the standard output if no <option>&dashdash;output</option> is
+ specified.</para>
+
<para>The descriptor specifies an extension class and (optionally)
attributes for that extension class. The form of the descriptor is
<command><replaceable>class</replaceable>(<replaceable>attributes</replaceable>)</command>,
where the attributes are of the form
<command><replaceable>attr</replaceable> =
"<replaceable>val</replaceable>"</command>. If there
are no attributes, the parentheses may be omitted.</para>
<para>The <replaceable>class</replaceable> may be either the path
! to an extensing extension or a QMTest class name in the form
<replaceable>module.class</replaceable>. If the
! <option>&dashdash;id</option> option has been provided, QMTest
! will look for an existing extension in the test database named
! <replaceable>class</replaceable>. If the
! <option>&dashdash;id</option> option has not been provided, QMTest
! will look for an XML file named <replaceable>class</replaceable>.
! In either case, if an existing extension cannot be found, the
! <replaceable>class</replaceable> is interepreted as the name of an
! extension class.</para>
<para>The attributes used to construct the extension instance come
! from three sources: the attributes in the extant extension (if the
<replaceable>class</replaceable> is the path to an extension
file), the <option>&dashdash;attribute</option> options provided
on the command line, and the explicit attributes provided in the
descriptor. If multiple values for the the same attribute name
are provided, the value used is taken from the first source in the
following list for which there is a value: the rightmost attribute
provided in the descriptor, the extension file, or the rightmost
<option>&dashdash;attribute</option> present on the command
line.</para>
<para>The <command>create</command> command accepts these
options:</para>
<variablelist>
<varlistentry>
***************
*** 738,754 ****
and valid values is dependent on the extension class in use.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-o</option> <replaceable>file</replaceable></term>
<term>
<option>&dashdash;output</option>
<replaceable>file</replaceable>
</term>
<listitem>
! <para>Write a description of the extension instance to
<replaceable>file</replaceable>.</para>
</listitem>
</varlistentry>
</variablelist>
</section>
--- 746,774 ----
and valid values is dependent on the extension class in use.</para>
</listitem>
</varlistentry>
<varlistentry>
+ <term><option>-i</option> <replaceable>id</replaceable></term>
+ <term>
+ <option>&dashdash;id</option>
+ <replaceable>id</replaceable>
+ </term>
+ <listitem>
+ <para>Add the extension instance to the database, using
+ <replaceable>id</replaceable> as the name of the instance.</para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><option>-o</option> <replaceable>file</replaceable></term>
<term>
<option>&dashdash;output</option>
<replaceable>file</replaceable>
</term>
<listitem>
! <para>Write the extension instance to
<replaceable>file</replaceable>.</para>
</listitem>
</varlistentry>
</variablelist>
</section>
***************
*** 762,771 ****
--- 782,809 ----
qmtest create -a format=stats -o rs
result_stream text_result_stream.TextResultStream(filename="rs")]]></userinput></screen>
creates a file called <filename>rs</filename> containing an
instance of <classname>TextResultStream</classname>.
</para>
+
+ <para>
+ This command:
+ <screen>
+ &prompt;<userinput>qmtest create --id=simple -a program=testprog test command.ExecTest</userinput>
+ </screen>
+ creates a test named "simple" that executes the program
+ <filename>testprog</filename>:
+ </para>
+
+ <para>
+ This command:
+ <screen>
+ &prompt;<userinput>qmtest create --id=copy test simple</userinput>
+ </screen>
+ creates a copy of the "simple" test, naming the new version
+ "copy".
+ </para>
</section>
</section>
<section id="sec-testcmd-create-target">
Index: qm/test/share/messages/diagnostics.txt
===================================================================
RCS file: /home/qm/Repository/qm/qm/test/share/messages/diagnostics.txt,v
retrieving revision 1.12
diff -c -5 -p -r1.12 diagnostics.txt
*** qm/test/share/messages/diagnostics.txt 20 Jul 2005 00:42:18 -0000 1.12
--- qm/test/share/messages/diagnostics.txt 21 Jul 2005 08:05:46 -0000
*************** The "%(class_name)s" extension class cou
*** 34,43 ****
--- 34,46 ----
The "%(file)s" target file could not be loaded.
@ db path doesn't exist
"%(path)s" does not exist.
+ @ db not modifiable
+ The test database is not modifiable.
+
@ dependency cycle
This test depends on itself, either directly or by way of other tests.
@ error loading xml resource
A problem occurred while loading the XML resource file "%(resource_id)s":
*************** QMTest has created a new test database a
*** 144,155 ****
@ no db specified
You must specify the location of the test database.
* Specify the '--db-path' option with the path to the database.
! * Or, set the '%(envvar)s' environment variable to the path to the
! database.
@ loading class
QMTest will load '%(class_name)s' from '%(file_name)s'.
@ no id for edit
--- 147,158 ----
@ no db specified
You must specify the location of the test database.
* Specify the '--db-path' option with the path to the database.
! * Or, set the 'QMTEST_DB_PATH' environment variable to the path
! to the database.
@ loading class
QMTest will load '%(class_name)s' from '%(file_name)s'.
@ no id for edit
More information about the qmtest
mailing list