1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 import cPickle
21 import cStringIO
22 import os
23 import qm
24 import qm.attachment
25 from qm.common import *
26 from qm.test.file_result_reader import FileResultReader
27 from qm.test.expectation_database import ExpectationDatabase
28 import qm.platform
29 import qm.structured_text
30 from qm.test.context import *
31 from qm.test.result import *
32 import qm.xmlutil
33 import string
34 import sys
35 import tempfile
36 import types
37
38
39
40
41
43 """An exception indicating that an extension class could not be loaded."""
44
45 - def __init__(self, class_name, exc_info):
46 """Construct a new 'CouldNotLoadExtensionError'.
47
48 'class_name' -- The name of the class.
49
50 'exc_info' -- An exception tuple, as returned by 'sys.exc_info'."""
51
52 self.exc_info = exc_info
53 message = qm.common.format_exception(exc_info)
54 message += "\n" + qm.error("could not load extension class",
55 class_name = class_name)
56 QMException.__init__(self, message)
57
58
59
60
61
63 """Return the directories to search for QMTest extensions.
64
65 'kind' -- A string giving kind of extension for which we are looking.
66 This must be of the elements of 'extension_kinds'.
67
68 'database' -- The 'Database' with which the extension class will be
69 used, or 'None'.
70
71 'database_path' -- The path from which the database will be loaded.
72 If 'None', 'database.GetPath()' is used.
73
74 returns -- A sequence of strings. Each string is the path to a
75 directory that should be searched for QMTest extensions. The
76 directories must be searched in order; the first directory
77 containing the desired module is the one from which the module is
78 loaded.
79
80 The directories that are returned are, in order:
81
82 1. Those directories present in the 'QMTEST_CLASS_PATH' environment
83 variable.
84
85 2. Those directories specified by the 'GetClassPaths' method on the
86 test database -- unless 'kind' is 'database'.
87
88 3. The directory specified by config.extension_path.
89
90 4. The directories containing classes that come with QMTest.
91
92 By placing the 'QMTEST_CLASS_PATH' directories first, users can
93 override test classes with standard names."""
94
95 global extension_kinds
96
97
98 assert kind in extension_kinds
99
100
101
102 if os.environ.has_key('QMTEST_CLASS_PATH'):
103 dirs = string.split(os.environ['QMTEST_CLASS_PATH'],
104 os.pathsep)
105 else:
106 dirs = []
107
108
109 if database:
110 dirs = dirs + database.GetClassPaths()
111
112
113 if database:
114 dirs.append(database.GetConfigurationDirectory())
115 elif database_path:
116 dirs.append(qm.test.database.get_configuration_directory
117 (database_path))
118
119
120 dirs.append(os.path.join(qm.prefix, qm.extension_path))
121
122 dirs.append(qm.common.get_lib_directory('test', 'classes'))
123
124 return dirs
125
126
128 """Return the names of QMTest extension classes in 'directory'.
129
130 'directory' -- A string giving the path to a directory in the file
131 system.
132
133 returns -- A dictionary mapping the strings in 'extension_kinds' to
134 sequences of strings. Each element in the sequence names an
135 extension class, using the form 'module.class'"""
136
137 global extension_kinds
138
139
140 extensions = {}
141 for kind in extension_kinds:
142 extensions[kind] = []
143
144
145 file = os.path.join(directory, 'classes.qmc')
146
147
148 if not os.path.isfile(file):
149 return extensions
150
151 try:
152
153 document = qm.xmlutil.load_xml_file(file)
154
155 root = document.documentElement
156
157
158 classes = root.getElementsByTagName("class")
159
160 for c in classes:
161 kind = c.getAttribute('kind')
162
163
164 if kind not in extension_kinds:
165 continue
166 if c.hasAttribute("name"):
167 name = c.getAttribute("name")
168 else:
169
170
171 name = qm.xmlutil.get_dom_text(c)
172
173 name = name.strip()
174 extensions[kind].append(name)
175 except:
176 raise
177
178 return extensions
179
180
182 """Return the names of extension classes.
183
184 'kind' -- The kind of extension class. This value must be one
185 of the 'extension_kinds'.
186
187 'database' -- The 'Database' with which the extension class will be
188 used, or 'None' if 'kind' is 'database'.
189
190 'database_path' -- The path from which the database will be loaded.
191 If 'None', 'database.GetPath()' is used.
192
193 returns -- A sequence of strings giving the names of the extension
194 classes with the indicated 'kind', in the form 'module.class'."""
195
196 dirs = get_extension_directories(kind, database, database_path)
197 names = []
198 for d in dirs:
199 names.extend(get_extension_class_names_in_directory(d)[kind])
200 return names
201
202
204 """Load an extension class from 'directory'.
205
206 'class_name' -- The name of the extension class, in the form
207 'module.class'.
208
209 'kind' -- The kind of class to load. This value must be one
210 of the 'extension_kinds'.
211
212 'directory' -- The directory from which to load the class.
213
214 'path' -- The directories to search for modules imported by the new
215 module.
216
217 returns -- The class loaded."""
218
219 global __class_caches
220 global __extension_bases
221
222
223 cache = __class_caches[kind]
224 if cache.has_key(class_name):
225 return cache[class_name]
226
227
228 try:
229 klass = qm.common.load_class(class_name, [directory],
230 path + sys.path)
231 except:
232 raise CouldNotLoadExtensionError(class_name, sys.exc_info())
233
234
235 if not issubclass(klass, __extension_bases[kind]):
236 raise QMException, \
237 qm.error("extension class not subclass",
238 kind = kind,
239 class_name = class_name,
240 base_name = __extension_bases[kind].__name__)
241
242
243 cache[class_name] = klass
244
245 return klass
246
247
249 """Return the extension class named 'class_name'.
250
251 'class_name' -- The name of the class, in the form 'module.class'.
252
253 'kind' -- The kind of class to load. This value must be one
254 of the 'extension_kinds'.
255
256 'database' -- The 'Database' with which the extension class will be
257 used, or 'None' if 'kind' is 'database'.
258
259 'database_path' -- The path from which the database will be loaded.
260 If 'None', 'database.GetPath()' is used.
261
262 returns -- The class object with the indicated 'class_name'."""
263
264 global __class_caches
265
266
267 cache = __class_caches[kind]
268 if cache.has_key(class_name):
269 return cache[class_name]
270
271
272
273
274 if kind == "database" and class_name in ("xmldb.Database",
275 "qm.test.xmldb.Database"):
276 class_name = "xml_database.XMLDatabase"
277
278
279 directories = get_extension_directories(kind, database, database_path)
280 directory = None
281 for d in directories:
282 if class_name in get_extension_class_names_in_directory(d)[kind]:
283 directory = d
284 break
285
286
287 if not directory:
288 raise QMException, qm.error("extension class not found",
289 klass=class_name)
290
291
292 return get_extension_class_from_directory(class_name, kind,
293 directory, directories)
294
295
297 """Return the test class named 'class_name'.
298
299 'class_name' -- The name of the test class, in the form
300 'module.class'.
301
302 returns -- The test class object with the indicated 'class_name'."""
303
304 return get_extension_class(class_name, 'test', database)
305
306
308 """Return the resource class named 'class_name'.
309
310 'class_name' -- The name of the resource class, in the form
311 'module.class'.
312
313 returns -- The resource class object with the indicated
314 'class_name'."""
315
316 return get_extension_class(class_name, 'resource', database)
317
318
340
341
343 """Read test results from a file.
344
345 'file' -- The filename or file object from which to read the
346 results. If 'file' is not a string, then it is must be a seekable
347 file object, and this function will look for a 'FileResultReader'
348 that accepts the file. If 'file' is a string, then it is treated as
349 either a filename or as an extension descriptor.
350
351 'database' -- The current database.
352
353 returns -- A 'ResultReader' object, or raises an exception if no
354 appropriate reader is available."""
355
356 f = None
357 if isinstance(file, types.StringTypes):
358 if os.path.exists(file):
359 f = open(file, "rb")
360 else:
361 f = file
362 if f:
363
364 for c in get_extension_classes("result_reader", database):
365 if issubclass(c, FileResultReader):
366 try:
367 return c({"file" : f})
368 except FileResultReader.InvalidFile:
369
370 f.seek(0)
371 if not isinstance(file, types.StringTypes):
372 raise FileResultReader.InvalidFile, \
373 "not a valid results file"
374 if database:
375 extension_loader = database.GetExtension
376 else:
377 extension_loader = None
378 class_loader = lambda n: get_extension_class(n,
379 "result_reader",
380 database)
381 cl, args = qm.extension.parse_descriptor(file,
382 class_loader,
383 extension_loader)
384 return cl(args)
385
386
388 """Read expectations from a file.
389
390 'file' -- The filename or file object from which to read the
391 expectations. If 'file' is not a string, then it is must be a seekable
392 file object, and this function will look for an 'ExpectationDatabase'
393 that accepts the file. If 'file' is a string, then it is treated as
394 either a filename or as an extension descriptor.
395
396 'database' -- The current database.
397
398 'annotations' -- Annotations for the current test run.
399
400 returns -- An 'ExpectationDatabase' object, or raises an exception if no
401 appropriate reader is available."""
402
403 if not file:
404 return ExpectationDatabase()
405 f = None
406 if isinstance(file, (str, unicode)):
407 if os.path.exists(file):
408 f = open(file, "rb")
409 else:
410 f = file
411 if f:
412 return PreviousTestRun(test_database = database, results_file = f)
413 if not isinstance(file, (str, unicode)):
414 raise QMException, "not a valid expectation database"
415 if database:
416 extension_loader = database.GetExtension
417 else:
418 extension_loader = None
419 class_loader = lambda n: get_extension_class(n,
420 "expectation_database",
421 database)
422 cl, args = qm.extension.parse_descriptor(file,
423 class_loader,
424 extension_loader)
425 args['test_database'] = database
426 args['testrun_parameters'] = annotations or {}
427 return cl(**args)
428
429
431 """Load test outcomes from a file.
432
433 'file' -- The file object from which to read the results. See
434 'load_results' for details.
435
436 'database' -- The current database.
437
438 returns -- A map from test IDs to outcomes."""
439
440 results = load_results(file, database)
441 outcomes = {}
442 for r in results:
443
444 if r.GetKind() == Result.TEST:
445 outcomes[r.GetId()] = r.GetOutcome()
446 return outcomes
447
448
450 """Extract a result from a DOM node.
451
452 'node' -- A DOM node corresponding to a "result" element.
453
454 returns -- A 'Result' object. The context for the result is 'None',
455 since context is not represented in a result DOM node."""
456
457 assert node.tagName == "result"
458
459 outcome = qm.xmlutil.get_child_text(node, "outcome")
460
461 test_id = node.getAttribute("id")
462 kind = node.getAttribute("kind")
463
464 result = Result(kind, test_id, outcome)
465
466 for property_node in node.getElementsByTagName("property"):
467
468 name = property_node.getAttribute("name")
469
470 value = qm.xmlutil.get_dom_text(property_node)
471
472 result[name] = value
473
474 return result
475
476
477
478
479
480
481 import qm.test.database
482 import qm.label
483 import qm.host
484 import qm.test.resource
485 import qm.test.result_reader
486 import qm.test.result_stream
487 import qm.test.run_database
488 import qm.test.expectation_database
489 import qm.test.suite
490 import qm.test.target
491 import qm.test.test
492 from qm.test.classes.previous_testrun import PreviousTestRun
493
494 __extension_bases = {
495 'database' : qm.test.database.Database,
496 'host' : qm.host.Host,
497 'label' : qm.label.Label,
498 'resource' : qm.test.resource.Resource,
499 'result_reader' : qm.test.result_reader.ResultReader,
500 'result_stream' : qm.test.result_stream.ResultStream,
501 'run_database' : qm.test.run_database.RunDatabase,
502 'expectation_database' : qm.test.expectation_database.ExpectationDatabase,
503 'suite' : qm.test.suite.Suite,
504 'target' : qm.test.target.Target,
505 'test' : qm.test.test.Test
506 }
507 """A map from extension class kinds to base classes.
508
509 An extension class of a particular 'kind' must be derived from
510 'extension_bases[kind]'."""
511
512 extension_kinds = __extension_bases.keys()
513 """Names of different kinds of QMTest extension classes."""
514 extension_kinds.sort()
515
516 __class_caches = {}
517 """A dictionary of loaded class caches.
518
519 The keys are the kinds in 'extension_kinds'. The associated value
520 is itself a dictionary mapping class names to class objects."""
521
522
523 for kind in extension_kinds:
524 __class_caches[kind] = {}
525
526
527
528
529
530
531
532