1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 import base
21 import database
22 import os
23 import qm
24 import qm.attachment
25 import qm.cmdline
26 import qm.platform
27 from qm.extension import get_extension_class_name, get_class_description
28 from qm.test import test
29 from qm.test.result import Result
30 from qm.test.context import *
31 from qm.test.execution_engine import *
32 from qm.test.result_stream import ResultStream
33 from qm.test.runnable import Runnable
34 from qm.test.suite import Suite
35 from qm.test.report import ReportGenerator
36 from qm.test.classes.dir_run_database import *
37 from qm.test.expectation_database import ExpectationDatabase
38 from qm.test.classes.previous_testrun import PreviousTestRun
39 from qm.trace import *
40 from qm.test.web.web import QMTestServer
41 import qm.structured_text
42 import qm.xmlutil
43 import Queue
44 import random
45 from result import *
46 import signal
47 import string
48 import sys
49 import xml.sax
50
51
52
53
54
55 _the_qmtest = None
56 """The global 'QMTest' object."""
57
58
59
60
61
63 """Return a string consisting of the 'items', separated by commas.
64
65 'items' -- A list of strings giving the items in the list.
66
67 'conjunction' -- A string to use before the final item, if there is
68 more than one.
69
70 returns -- A string consisting all of the 'items', separated by
71 commas, and with the 'conjunction' before the final item."""
72
73 s = ""
74 need_comma = 0
75
76
77 for i in items[:-1]:
78
79 if need_comma:
80 s += ", "
81 else:
82 need_comma = 1
83
84 s += "'%s'" % i
85
86 if items:
87 i = items[-1]
88 if need_comma:
89 s += ", %s " % conjunction
90 s += "'%s'" % i
91
92 return s
93
94
95
96
97
99 """An instance of QMTest."""
100
101 __extension_kinds_string \
102 = _make_comma_separated_string(base.extension_kinds, "or")
103 """A string listing the available extension kinds."""
104
105 db_path_environment_variable = "QMTEST_DB_PATH"
106 """The environment variable specifying the test database path."""
107
108 summary_formats = ("brief", "full", "stats", "batch", "none")
109 """Valid formats for result summaries."""
110
111 context_file_name = "context"
112 """The default name of a context file."""
113
114 expectations_file_name = "expectations.qmr"
115 """The default name of a file containing expectations."""
116
117 results_file_name = "results.qmr"
118 """The default name of a file containing results."""
119
120 target_file_name = "targets"
121 """The default name of a file containing targets."""
122
123 help_option_spec = (
124 "h",
125 "help",
126 None,
127 "Display usage summary."
128 )
129
130 version_option_spec = (
131 None,
132 "version",
133 None,
134 "Display version information."
135 )
136
137 db_path_option_spec = (
138 "D",
139 "tdb",
140 "PATH",
141 "Path to the test database."
142 )
143
144 extension_output_option_spec = (
145 "o",
146 "output",
147 "FILE",
148 "Write the extension to FILE.",
149 )
150
151 extension_id_option_spec = (
152 "i",
153 "id",
154 "NAME",
155 "Write the extension to the database as NAME.",
156 )
157
158 output_option_spec = (
159 "o",
160 "output",
161 "FILE",
162 "Write test results to FILE (- for stdout)."
163 )
164
165 no_output_option_spec = (
166 None,
167 "no-output",
168 None,
169 "Don't generate test results."
170 )
171
172 outcomes_option_spec = (
173 "O",
174 "outcomes",
175 "FILE",
176 "Use expected outcomes in FILE."
177 )
178
179 expectations_option_spec = (
180 "e",
181 "expectations",
182 "FILE",
183 "Use expectations in FILE."
184 )
185
186 context_option_spec = (
187 "c",
188 "context",
189 "KEY=VALUE",
190 "Add or override a context property."
191 )
192
193 context_file_spec = (
194 "C",
195 "load-context",
196 "FILE",
197 "Read context from a file (- for stdin)."
198 )
199
200 daemon_option_spec = (
201 None,
202 "daemon",
203 None,
204 "Run as a daemon."
205 )
206
207 port_option_spec = (
208 "P",
209 "port",
210 "PORT",
211 "Server port number."
212 )
213
214 address_option_spec = (
215 "A",
216 "address",
217 "ADDRESS",
218 "Local address."
219 )
220
221 log_file_option_spec = (
222 None,
223 "log-file",
224 "PATH",
225 "Log file name."
226 )
227
228 no_browser_option_spec = (
229 None,
230 "no-browser",
231 None,
232 "Do not open a new browser window."
233 )
234
235 pid_file_option_spec = (
236 None,
237 "pid-file",
238 "PATH",
239 "Process ID file name."
240 )
241
242 concurrent_option_spec = (
243 "j",
244 "concurrency",
245 "COUNT",
246 "Execute tests in COUNT concurrent threads."
247 )
248
249 targets_option_spec = (
250 "T",
251 "targets",
252 "FILE",
253 "Use FILE as the target specification file."
254 )
255
256 random_option_spec = (
257 None,
258 "random",
259 None,
260 "Run the tests in a random order."
261 )
262
263 rerun_option_spec = (
264 None,
265 "rerun",
266 "FILE",
267 "Rerun the tests that failed."
268 )
269
270 seed_option_spec = (
271 None,
272 "seed",
273 "INTEGER",
274 "Seed the random number generator."
275 )
276
277 format_option_spec = (
278 "f",
279 "format",
280 "FORMAT",
281 "Specify the summary format."
282 )
283
284 result_stream_spec = (
285 None,
286 "result-stream",
287 "CLASS-NAME",
288 "Specify the results file format."
289 )
290
291 annotation_option_spec = (
292 "a",
293 "annotate",
294 "NAME=VALUE",
295 "Set an additional annotation to be written to the result stream(s)."
296 )
297
298 tdb_class_option_spec = (
299 "c",
300 "class",
301 "CLASS-NAME",
302 "Specify the test database class.",
303 )
304
305 attribute_option_spec = (
306 "a",
307 "attribute",
308 "NAME",
309 "Get an attribute of the extension class."
310 )
311
312 set_attribute_option_spec = (
313 "a",
314 "attribute",
315 "KEY=VALUE",
316 "Set an attribute of the extension class."
317 )
318
319 extension_kind_option_spec = (
320 "k",
321 "kind",
322 "EXTENSION-KIND",
323 "Specify the kind of extension class."
324 )
325
326 report_output_option_spec = (
327 "o",
328 "output",
329 "FILE",
330 "Write test report to FILE (- for stdout)."
331 )
332
333 report_flat_option_spec = (
334 "f",
335 "flat",
336 None,
337 """Generate a flat listing of test results, instead of reproducing the
338 database directory tree in the report."""
339 )
340
341 results_option_spec = (
342 "R",
343 "results",
344 "DIRECTORY",
345 "Read in all results (*.qmr) files from DIRECTORY."
346 )
347
348 list_long_option_spec = (
349 "l",
350 "long",
351 None,
352 "Use a detailed output format."
353 )
354
355 list_details_option_spec = (
356 "d",
357 "details",
358 None,
359 "Display details for individual items."
360 )
361
362 list_recursive_option_spec = (
363 "R",
364 "recursive",
365 None,
366 "Recursively list the contents of directories."
367 )
368
369
370 conflicting_option_specs = (
371 ( output_option_spec, no_output_option_spec ),
372 ( concurrent_option_spec, targets_option_spec ),
373 ( extension_output_option_spec, extension_id_option_spec ),
374 ( expectations_option_spec, outcomes_option_spec ),
375 )
376
377 global_options_spec = [
378 help_option_spec,
379 version_option_spec,
380 db_path_option_spec,
381 ]
382
383 commands_spec = [
384 ("create",
385 "Create (or update) an extension.",
386 "EXTENSION-KIND CLASS-NAME(ATTR1 = 'VAL1', ATTR2 = 'VAL2', ...)",
387 """Create (or update) an extension.
388
389 The EXTENSION-KIND indicates what kind of extension to
390 create; it must be one of """ + __extension_kinds_string + """.
391
392 The CLASS-NAME indicates the name of the extension class, or
393 the name of an existing extension object. If the CLASS-NAME
394 is the name of a extension in the test database, then the
395
396 In the former case, it must have the form 'MODULE.CLASS'. For
397 a list of available extension classes use "qmtest extensions".
398 If the extension class takes arguments, those arguments can be
399 specified after the CLASS-NAME as show above. In the latter
400 case,
401
402 Any "--attribute" options are processed before the arguments
403 specified after the class name. Therefore, the "--attribute"
404 options can be overridden by the arguments provided after the
405 CLASS-NAME. If no attributes are specified, the parentheses
406 following the 'CLASS-NAME' can be omitted.
407
408 If the "--id" option is given, the extension is written to the
409 database. Otherwise, if the "--output" option is given, the
410 extension is written as XML to the file indicated. If neither
411 option is given, the extension is written as XML to the
412 standard output.""",
413 ( set_attribute_option_spec,
414 help_option_spec,
415 extension_id_option_spec,
416 extension_output_option_spec
417 ),
418 ),
419
420 ("create-target",
421 "Create (or update) a target specification.",
422 "NAME CLASS [ GROUP ]",
423 "Create (or update) a target specification.",
424 ( set_attribute_option_spec,
425 help_option_spec,
426 targets_option_spec
427 )
428 ),
429
430 ("create-tdb",
431 "Create a new test database.",
432 "",
433 "Create a new test database.",
434 ( help_option_spec,
435 tdb_class_option_spec,
436 set_attribute_option_spec)
437 ),
438
439 ("gui",
440 "Start the QMTest GUI.",
441 "",
442 "Start the QMTest graphical user interface.",
443 (
444 address_option_spec,
445 concurrent_option_spec,
446 context_file_spec,
447 context_option_spec,
448 daemon_option_spec,
449 help_option_spec,
450 log_file_option_spec,
451 no_browser_option_spec,
452 pid_file_option_spec,
453 port_option_spec,
454 outcomes_option_spec,
455 targets_option_spec,
456 results_option_spec
457 )
458 ),
459
460 ("extensions",
461 "List extension classes.",
462 "",
463 """
464 List the available extension classes.
465
466 Use the '--kind' option to limit the classes displayed to test classes,
467 resource classes, etc. The parameter to '--kind' can be one of """ + \
468 __extension_kinds_string + "\n",
469 (
470 extension_kind_option_spec,
471 help_option_spec,
472 )
473 ),
474
475 ("describe",
476 "Describe an extension.",
477 "EXTENSION-KIND NAME",
478 """Display details for the specified extension.""",
479 (
480 attribute_option_spec,
481 list_long_option_spec,
482 help_option_spec,
483 )
484 ),
485
486 ("help",
487 "Display usage summary.",
488 "",
489 "Display usage summary.",
490 ()
491 ),
492
493 ("ls",
494 "List database contents.",
495 "[ NAME ... ]",
496 """
497 List items stored in the database.
498
499 If no arguments are provided, the contents of the root
500 directory of the database are displayed. Otherwise, each of
501 the database is searched for each of the NAMEs. If the item
502 found is a directory then the contents of the directory are
503 displayed.
504 """,
505 (
506 help_option_spec,
507 list_long_option_spec,
508 list_details_option_spec,
509 list_recursive_option_spec,
510 ),
511 ),
512
513 ("register",
514 "Register an extension class.",
515 "KIND CLASS",
516 """
517 Register an extension class with QMTest. KIND is the kind of extension
518 class to register; it must be one of """ + __extension_kinds_string + """
519
520 The CLASS gives the name of the class in the form 'module.class'.
521
522 QMTest will search the available extension class directories to find the
523 new CLASS. QMTest looks for files whose basename is the module name and
524 whose extension is either '.py', '.pyc', or '.pyo'.
525
526 QMTest will then attempt to load the extension class. If the extension
527 class cannot be loaded, QMTest will issue an error message to help you
528 debug the problem. Otherwise, QMTest will update the 'classes.qmc' file
529 in the directory containing the module to mention your new extension class.
530 """,
531 (help_option_spec,)
532 ),
533
534 ("remote",
535 "Run QMTest as a remote server.",
536 "",
537 """
538 Runs QMTest as a remote server. This mode is only used by QMTest
539 itself when distributing tests across multiple machines. Users
540 should not directly invoke QMTest with this option.
541 """,
542 (help_option_spec,)
543 ),
544
545 ("report",
546 "Generate report from one or more test results.",
547 "[ result [-e expected] ]+",
548 """
549 Generates a test report. The arguments are result files each optionally
550 followed by '-e' and an expectation file. This command attempts to reproduce
551 the test database structure, and thus requires the '--tdb' option. To generate
552 a flat test report specify the '--flat' option.
553 """,
554 (help_option_spec,
555 report_output_option_spec,
556 report_flat_option_spec)
557 ),
558
559 ("run",
560 "Run one or more tests.",
561 "[ ID ... ]",
562 """
563 Runs tests. Optionally, generates a summary of the test run and a
564 record of complete test results. You may specify test IDs and test
565 suite IDs to run; omit arguments to run the entire test database.
566
567 Test results are written to "results.qmr". Use the '--output' option to
568 specify a different output file, or '--no-output' to supress results.
569
570 Use the '--format' option to specify the output format for the summary.
571 Valid formats are %s.
572 """ % _make_comma_separated_string(summary_formats, "and"),
573 (
574 annotation_option_spec,
575 concurrent_option_spec,
576 context_file_spec,
577 context_option_spec,
578 format_option_spec,
579 help_option_spec,
580 no_output_option_spec,
581 outcomes_option_spec,
582 expectations_option_spec,
583 output_option_spec,
584 random_option_spec,
585 rerun_option_spec,
586 result_stream_spec,
587 seed_option_spec,
588 targets_option_spec,
589 )
590 ),
591
592 ("summarize",
593 "Summarize results from a test run.",
594 "[FILE [ ID ... ]]",
595 """
596 Loads a test results file and summarizes the results. FILE is the path
597 to the results file. Optionally, specify one or more test or suite IDs
598 whose results are shown. If none are specified, shows all tests that
599 did not pass.
600
601 Use the '--format' option to specify the output format for the summary.
602 Valid formats are %s.
603 """ % _make_comma_separated_string(summary_formats, "and"),
604 ( help_option_spec,
605 format_option_spec,
606 outcomes_option_spec,
607 expectations_option_spec,
608 output_option_spec,
609 result_stream_spec)
610 ),
611
612 ]
613
614 __version_output = \
615 ("QMTest %s\n"
616 "Copyright (C) 2002 - 2007 CodeSourcery, Inc.\n"
617 "QMTest comes with ABSOLUTELY NO WARRANTY\n"
618 "For more information about QMTest visit http://www.qmtest.com\n")
619 """The string printed when the --version option is used.
620
621 There is one fill-in, for a string, which should contain the version
622 number."""
623
624 - def __init__(self, argument_list, path):
625 """Construct a new QMTest.
626
627 Parses the argument list but does not execute the command.
628
629 'argument_list' -- The arguments to QMTest, not including the
630 initial argv[0].
631
632 'path' -- The path to the QMTest executable."""
633
634 global _the_qmtest
635
636 _the_qmtest = self
637
638
639 self._stdout = sys.stdout
640 self._stderr = sys.stderr
641
642
643 self.__tracer = Tracer()
644
645
646 self.__parser = qm.cmdline.CommandParser(
647 "qmtest",
648 self.global_options_spec,
649 self.commands_spec,
650 self.conflicting_option_specs)
651
652 components = self.__parser.ParseCommandLine(argument_list)
653
654 ( self.__global_options,
655 self.__command,
656 self.__command_options,
657 self.__arguments
658 ) = components
659
660
661 self.__qmtest_path = path
662
663
664 self.targets = None
665
666
667
668 self.__file_result_stream_class_name \
669 = "pickle_result_stream.PickleResultStream"
670
671 self.__text_result_stream_class_name \
672 = "text_result_stream.TextResultStream"
673
674 self.__expected_outcomes = None
675
676
681
682
684 """Return true if 'option' was specified as a global command.
685
686 'command' -- The long name of the option, but without the
687 preceding "--".
688
689 returns -- True if the option is present."""
690
691 return option in map(lambda x: x[0], self.__global_options)
692
693
695 """Return the value of global 'option', or 'default' if omitted."""
696
697 for opt, opt_arg in self.__global_options:
698 if opt == option:
699 return opt_arg
700 return default
701
702
704 """Return true if command 'option' was specified."""
705
706 for opt, opt_arg in self.__command_options:
707 if opt == option:
708 return 1
709 return 0
710
711
713 """Return the value of command 'option'.
714
715 'option' -- The long form of an command-specific option.
716
717 'default' -- The default value to be returned if the 'option'
718 was not specified. This option should be the kind of an option
719 that takes an argument.
720
721 returns -- The value specified by the option, or 'default' if
722 the option was not specified."""
723
724 for opt, opt_arg in self.__command_options:
725 if opt == option:
726 return opt_arg
727 return default
728
729
731 """Execute the command.
732
733 returns -- 0 if the command was executed successfully. 1 if
734 there was a problem or if any tests run had unexpected outcomes."""
735
736
737
738
739 if self.HasGlobalOption("version"):
740 self._stdout.write(self.__version_output % qm.version)
741 return 0
742
743 if (self.GetGlobalOption("help") is not None
744 or self.__command == "help"):
745 self._stdout.write(self.__parser.GetBasicHelp())
746 return 0
747
748 if self.GetCommandOption("help") is not None:
749 self.__WriteCommandHelp(self.__command)
750 return 0
751
752
753 if self.__command == "":
754 raise qm.cmdline.CommandError, qm.error("missing command")
755
756
757
758
759
760
761 db_path = self.GetGlobalOption("tdb")
762 if not db_path:
763 if os.environ.has_key(self.db_path_environment_variable):
764 db_path = os.environ[self.db_path_environment_variable]
765 else:
766 db_path = "."
767
768
769 if not os.path.isabs(db_path):
770 db_path = os.path.join(os.getcwd(), db_path)
771
772
773 self.__db_path = os.path.normpath(db_path)
774 database.set_path(self.__db_path)
775
776 error_occurred = 0
777
778
779 if self.__command == "create-tdb":
780 return self.__ExecuteCreateTdb(db_path)
781
782 method = {
783 "create" : self.__ExecuteCreate,
784 "create-target" : self.__ExecuteCreateTarget,
785 "describe" : self.__ExecuteDescribe,
786 "extensions" : self.__ExecuteExtensions,
787 "gui" : self.__ExecuteServer,
788 "ls" : self.__ExecuteList,
789 "register" : self.__ExecuteRegister,
790 "remote" : self.__ExecuteRemote,
791 "run" : self.__ExecuteRun,
792 "report" : self.__ExecuteReport,
793 "summarize": self.__ExecuteSummarize,
794 }[self.__command]
795
796 return method()
797
798
800 """Return the test database to use.
801
802 returns -- The 'Database' to use for this execution. Raises an
803 exception if no 'Database' is available."""
804
805 return database.get_database()
806
807
809 """Return the test database to use.
810
811 returns -- The 'Database' to use for this execution, or 'None'
812 if no 'Database' is available."""
813
814 try:
815 return self.GetDatabase()
816 except:
817 return None
818
819
833
834
866
867
868
906
907
909 """Return the 'Tracer' associated with this instance of QMTest.
910
911 returns -- The 'Tracer' associated with this instance of QMTest."""
912
913 return self.__tracer
914
915
916 - def MakeContext(self):
917 """Construct a 'Context' object for running tests."""
918
919 context = Context()
920
921
922
923 use_implicit_context_file = 1
924 for option, argument in self.__command_options:
925 if option == "load-context":
926 use_implicit_context_file = 0
927 break
928
929
930 if (use_implicit_context_file
931 and os.path.isfile(self.context_file_name)):
932 context.Read(self.context_file_name)
933
934 for option, argument in self.__command_options:
935
936 if option == "load-context":
937 context.Read(argument)
938
939 elif option == "context":
940
941 name, value = qm.common.parse_assignment(argument)
942
943 try:
944
945 context[name] = value
946 except ValueError, msg:
947
948
949 raise qm.cmdline.CommandError, msg
950
951 return context
952
953
955 """Return the path to the QMTest executable.
956
957 returns -- A string giving the path to the QMTest executable.
958 This is the path that should be used to invoke QMTest
959 recursively. Returns 'None' if the path to the QMTest
960 executable is uknown."""
961
962 return self.__qmtest_path
963
964
966 """Return the 'ResultStream' class used for results files.
967
968 returns -- The 'ResultStream' class used for results files."""
969
970 return get_extension_class(self.__file_result_stream_class_name,
971 "result_stream",
972 self.GetDatabaseIfAvailable())
973
975 """Return the 'ResultStream' class used for textual feedback.
976
977 returns -- the 'ResultStream' class used for textual
978 feedback."""
979
980 return get_extension_class(self.__text_result_stream_class_name,
981 "result_stream",
982 self.GetDatabaseIfAvailable())
983
984
986 """Return the attributes specified on the command line.
987
988 'expect_value' -- True if the attribute is to be parsed as
989 an assignment.
990
991 returns -- A dictionary. If expect_value is True, it
992 maps attribute names (strings) to values (strings).
993 Else it contains the raw attribute strings, mapping to None.
994 There is an entry for each attribute specified with
995 '--attribute' on the command line."""
996
997
998 attributes = {}
999
1000
1001 for option, argument in self.__command_options:
1002 if option == "attribute":
1003 if expect_value:
1004 name, value = qm.common.parse_assignment(argument)
1005 attributes[name] = value
1006 else:
1007 attributes[argument] = None
1008 return attributes
1009
1010
1012 """Return all annotate options.
1013
1014 returns -- A dictionary containing the annotation name / value pairs."""
1015
1016 annotations = {}
1017 for option, argument in self.__command_options:
1018 if option == "annotate":
1019 name, value = qm.common.parse_assignment(argument)
1020 annotations[name] = value
1021 return annotations
1022
1023
1025 """Create a new extension file."""
1026
1027
1028 if len(self.__arguments) != 2:
1029 self.__WriteCommandHelp("create")
1030 return 2
1031
1032
1033 database = self.GetDatabaseIfAvailable()
1034
1035
1036 kind = self.__arguments[0]
1037 self.__CheckExtensionKind(kind)
1038
1039 extension_id = self.GetCommandOption("id")
1040 if extension_id is not None:
1041 if not database:
1042 raise QMException, qm.error("no db specified")
1043 if not database.IsModifiable():
1044 raise QMException, qm.error("db not modifiable")
1045 extension_loader = database.GetExtension
1046 else:
1047 extension_loader = None
1048
1049 class_loader = lambda n: get_extension_class(n, kind, database)
1050
1051
1052 (extension_class, more_arguments) \
1053 = (qm.extension.parse_descriptor
1054 (self.__arguments[1], class_loader, extension_loader))
1055
1056
1057 arguments = self.__GetAttributeOptions()
1058 arguments = qm.extension.validate_arguments(extension_class,
1059 arguments)
1060
1061
1062 arguments.update(more_arguments)
1063
1064 if extension_id is not None:
1065
1066
1067 if issubclass(extension_class, (Runnable, Suite)):
1068 extras = { extension_class.EXTRA_ID : extension_id,
1069 extension_class.EXTRA_DATABASE : database }
1070 else:
1071 extras = {}
1072 extension = extension_class(arguments, **extras)
1073
1074 database.WriteExtension(extension_id, extension)
1075 else:
1076
1077 filename = self.GetCommandOption("output")
1078 if filename is not None:
1079 file = open(filename, "w")
1080 else:
1081 file = sys.stdout
1082
1083 qm.extension.write_extension_file(extension_class, arguments,
1084 file)
1085
1086 return 0
1087
1088
1090 """Handle the command for creating a new test database.
1091
1092 'db_path' -- The path at which to create the new test database."""
1093
1094 if len(self.__arguments) != 0:
1095 self.__WriteCommandHelp("create-tdb")
1096 return 2
1097
1098
1099 if not os.path.isdir(db_path):
1100 os.mkdir(db_path)
1101
1102 config_dir = database.get_configuration_directory(db_path)
1103 if not os.path.isdir(config_dir):
1104 os.mkdir(config_dir)
1105
1106
1107
1108 self.__command_options.append(("output",
1109 database.get_configuration_file(db_path)))
1110
1111 class_name \
1112 = self.GetCommandOption("class", "xml_database.XMLDatabase")
1113
1114 self.__arguments.append("database")
1115 self.__arguments.append(class_name)
1116
1117 self.__ExecuteCreate()
1118
1119 self._stdout.write(qm.message("new db message", path=db_path) + "\n")
1120
1121 return 0
1122
1123
1125 """Create a new target file."""
1126
1127
1128 if (len(self.__arguments) < 2 or len(self.__arguments) > 3):
1129 self.__WriteCommandHelp("create-target")
1130 return 2
1131
1132
1133 target_name = self.__arguments[0]
1134 class_name = self.__arguments[1]
1135 if (len(self.__arguments) > 2):
1136 target_group = self.__arguments[2]
1137 else:
1138 target_group = ""
1139
1140
1141 database = self.GetDatabase()
1142
1143
1144 target_class = get_extension_class(class_name, "target", database)
1145
1146
1147 field_dictionary \
1148 = qm.extension.get_class_arguments_as_dictionary(target_class)
1149
1150
1151 file_name = self.GetTargetFileName()
1152
1153 if os.path.exists(file_name):
1154
1155 document = qm.xmlutil.load_xml_file(file_name)
1156
1157 targets_element = document.documentElement
1158 duplicates = []
1159 for target_element \
1160 in targets_element.getElementsByTagName("extension"):
1161 for attribute \
1162 in target_element.getElementsByTagName("argument"):
1163 if attribute.getAttribute("name") == "name":
1164 name = field_dictionary["name"].\
1165 GetValueFromDomNode(attribute.childNodes[0],
1166 None)
1167 if name == target_name:
1168 duplicates.append(target_element)
1169 break
1170 for duplicate in duplicates:
1171 targets_element.removeChild(duplicate)
1172 duplicate.unlink()
1173 else:
1174 document = (qm.xmlutil.create_dom_document
1175 (public_id = "QMTest/Target",
1176 document_element_tag = "targets"))
1177 targets_element = document.documentElement
1178
1179
1180 attributes = self.__GetAttributeOptions()
1181 attributes["name"] = target_name
1182 attributes["group"] = target_group
1183 attributes = qm.extension.validate_arguments(target_class,
1184 attributes)
1185
1186
1187 target_element = qm.extension.make_dom_element(target_class,
1188 attributes,
1189 document)
1190 targets_element.appendChild(target_element)
1191
1192
1193 document.writexml(open(self.GetTargetFileName(), "w"))
1194
1195 return 0
1196
1197
1239
1240
1274
1275
1277 """List the contents of the database."""
1278
1279 database = self.GetDatabase()
1280
1281 long_format = self.HasCommandOption("long")
1282 details_format = self.HasCommandOption("details")
1283 recursive = self.HasCommandOption("recursive")
1284
1285
1286 args = self.__arguments or ("",)
1287
1288
1289 extensions = {}
1290 for arg in args:
1291 extension = database.GetExtension(arg)
1292 if not extension:
1293 raise QMException, qm.error("no such ID", id = arg)
1294 if isinstance(extension, qm.test.suite.Suite):
1295 if recursive:
1296 test_ids, suite_ids = extension.GetAllTestAndSuiteIds()
1297 extensions.update([(i, database.GetExtension(i))
1298 for i in test_ids + suite_ids])
1299 else:
1300 ids = extension.GetTestIds() + extension.GetSuiteIds()
1301 extensions.update([(i, database.GetExtension(i))
1302 for i in ids])
1303 else:
1304 extensions[arg] = extension
1305
1306
1307 ids = extensions.keys()
1308 ids.sort()
1309
1310
1311 if not long_format:
1312 for id in ids:
1313 print >> sys.stdout, id
1314 return 0
1315
1316
1317
1318
1319
1320
1321 longest_kind = 0
1322 longest_class = 0
1323 for i in (0, 1):
1324 for id in ids:
1325 extension = extensions[id]
1326 if isinstance(extension,
1327 qm.test.directory_suite.DirectorySuite):
1328 kind = "directory"
1329 class_name = ""
1330 else:
1331 kind = extension.__class__.kind
1332 class_name = extension.GetClassName()
1333
1334 if i == 0:
1335 kind_len = len(kind) + 1
1336 if kind_len > longest_kind:
1337 longest_kind = kind_len
1338 class_len = len(class_name) + 1
1339 if class_len > longest_class:
1340 longest_class = class_len
1341 else:
1342 print >> sys.stdout, \
1343 "%-*s%-*s%s" % (longest_kind, kind,
1344 longest_class, class_name, id)
1345 if details_format:
1346 tab = max([len(name)
1347 for name in extension._argument_dictionary])
1348 for name in extension._argument_dictionary:
1349 value = str(getattr(extension, name))
1350 print " %-*s %s"%(tab, name, value)
1351
1352 return 0
1353
1354
1356 """Register a new extension class."""
1357
1358
1359 if (len(self.__arguments) != 2):
1360 self.__WriteCommandHelp("register")
1361 return 2
1362 kind = self.__arguments[0]
1363 class_name = self.__arguments[1]
1364
1365
1366 self.__CheckExtensionKind(kind)
1367
1368
1369 if class_name.count('.') != 1:
1370 raise qm.cmdline.CommandError, \
1371 qm.error("invalid class name",
1372 class_name = class_name)
1373 module, name = class_name.split('.')
1374
1375
1376
1377 database = self.GetDatabaseIfAvailable()
1378
1379
1380 found = None
1381 directories = get_extension_directories(kind, database,
1382 self.__db_path)
1383 for directory in directories:
1384 for ext in (".py", ".pyc", ".pyo"):
1385 file_name = os.path.join(directory, module + ext)
1386 if os.path.exists(file_name):
1387 found = file_name
1388 break
1389 if found:
1390 break
1391
1392
1393 if not found:
1394 raise qm.QMException, \
1395 qm.error("module does not exist",
1396 module = module)
1397
1398
1399
1400
1401
1402 self._stdout.write(qm.structured_text.to_text
1403 (qm.message("loading class",
1404 class_name = name,
1405 file_name = found)))
1406
1407
1408 extension_class = get_extension_class_from_directory(class_name,
1409 kind,
1410 directory,
1411 directories)
1412
1413
1414 classes_file_name = os.path.join(directory, "classes.qmc")
1415
1416
1417 document = (qm.xmlutil.create_dom_document
1418 (public_id = "Class-Directory",
1419 document_element_tag="class-directory"))
1420
1421
1422 extensions = get_extension_class_names_in_directory(directory)
1423 for k, ns in extensions.iteritems():
1424 for n in ns:
1425
1426 if k == kind and n == class_name:
1427 continue
1428 element = document.createElement("class")
1429 element.setAttribute("kind", k)
1430 element.setAttribute("name", n)
1431 document.documentElement.appendChild(element)
1432
1433
1434 element = document.createElement("class")
1435 element.setAttribute("kind", kind)
1436 element.setAttribute("name", class_name)
1437 document.documentElement.appendChild(element)
1438
1439
1440 document.writexml(open(classes_file_name, "w"),
1441 addindent = " ", newl = "\n")
1442
1443 return 0
1444
1445
1447 """Read in test run results and summarize."""
1448
1449
1450 if len(self.__arguments) == 0:
1451 results_path = self.results_file_name
1452 else:
1453 results_path = self.__arguments[0]
1454
1455 database = self.GetDatabaseIfAvailable()
1456
1457
1458 id_arguments = self.__arguments[1:]
1459
1460
1461 if len(id_arguments) > 0 and not '.' in id_arguments:
1462 ids = set()
1463
1464 if database:
1465 for id in id_arguments:
1466 extension = database.GetExtension(id)
1467 if not extension:
1468 raise qm.cmdline.CommandError, \
1469 qm.error("no such ID", id = id)
1470 if extension.kind == database.SUITE:
1471 ids.update(extension.GetAllTestAndSuiteIds()[0])
1472 else:
1473 ids.add(id)
1474 else:
1475 ids = set(id_arguments)
1476 else:
1477
1478
1479 ids = None
1480
1481
1482 try:
1483 results = base.load_results(results_path, database)
1484 except Exception, exception:
1485 raise QMException, \
1486 qm.error("invalid results file",
1487 path=results_path,
1488 problem=str(exception))
1489
1490 any_unexpected_outcomes = 0
1491
1492
1493 expectations = (self.GetCommandOption('expectations') or
1494 self.GetCommandOption('outcomes'))
1495 expectations = base.load_expectations(expectations,
1496 database,
1497 results.GetAnnotations())
1498
1499
1500 streams = self.__CreateResultStreams(self.GetCommandOption("output"),
1501 results.GetAnnotations(),
1502 expectations)
1503
1504 resource_results = {}
1505 for r in results:
1506 if r.GetKind() != Result.TEST:
1507 if ids is None or r.GetId() in ids:
1508 for s in streams:
1509 s.WriteResult(r)
1510 elif r.GetKind() == Result.RESOURCE_SETUP:
1511 resource_results[r.GetId()] = r
1512 continue
1513
1514
1515 if ids is not None and r.GetId() not in ids:
1516 continue
1517
1518
1519
1520 if (ids is not None
1521 and r.GetOutcome() == Result.UNTESTED
1522 and r.has_key(Result.RESOURCE)):
1523 rid = r[Result.RESOURCE]
1524 rres = resource_results.get(rid)
1525 if rres:
1526 del resource_results[rid]
1527 for s in streams:
1528 s.WriteResult(rres)
1529
1530 for s in streams:
1531 s.WriteResult(r)
1532 if (not any_unexpected_outcomes
1533 and r.GetOutcome() != expectations.Lookup(r.GetId())):
1534 any_unexpected_outcomes = 1
1535
1536 for s in streams:
1537 s.Summarize()
1538
1539 return any_unexpected_outcomes
1540
1541
1543 """Execute the 'remote' command."""
1544
1545 database = self.GetDatabase()
1546
1547
1548
1549 target_class = get_extension_class("serial_target.SerialTarget",
1550 'target', database)
1551
1552 target = target_class(database, { "name" : "child" })
1553
1554
1555 response_queue = Queue.Queue(0)
1556 target.Start(response_queue)
1557
1558
1559
1560 while 1:
1561
1562 command = cPickle.load(sys.stdin)
1563
1564
1565
1566 if isinstance(command, types.StringType):
1567 assert command == "Stop"
1568 target.Stop()
1569 break
1570
1571
1572 method, id, context = command
1573
1574 descriptor = database.GetTest(id)
1575
1576 target.RunTest(descriptor, context)
1577
1578 results = []
1579
1580 while 1:
1581 try:
1582 result = response_queue.get(0)
1583 results.append(result)
1584 except Queue.Empty:
1585
1586 break
1587
1588 cPickle.dump(results, sys.stdout)
1589
1590
1591
1592 sys.stdout.flush()
1593
1594 return 0
1595
1596
1617
1618
1620 """Execute a 'run' command."""
1621
1622 database = self.GetDatabase()
1623
1624
1625
1626 seed = self.GetCommandOption("seed")
1627 if seed:
1628
1629 try:
1630 seed = int(seed)
1631 except ValueError:
1632 raise qm.cmdline.CommandError, \
1633 qm.error("seed not integer", seed=seed)
1634
1635 random.seed(seed)
1636
1637
1638 if len(self.__arguments) == 0:
1639
1640 self.__arguments.append("")
1641 elif '.' in self.__arguments:
1642
1643 self.__arguments = [""]
1644
1645
1646 try:
1647 test_ids, test_suites \
1648 = self.GetDatabase().ExpandIds(self.__arguments)
1649 except (qm.test.database.NoSuchTestError,
1650 qm.test.database.NoSuchSuiteError), exception:
1651 raise qm.cmdline.CommandError, str(exception)
1652 except ValueError, exception:
1653 raise qm.cmdline.CommandError, \
1654 qm.error("no such ID", id=str(exception))
1655
1656
1657 annotations = self.__GetAnnotateOptions()
1658
1659
1660 expectations = (self.GetCommandOption('expectations') or
1661 self.GetCommandOption('outcomes'))
1662 expectations = base.load_expectations(expectations,
1663 database,
1664 annotations)
1665
1666
1667 test_ids = self.__FilterTestsToRun(test_ids, expectations)
1668
1669
1670 targets = self.GetTargets()
1671
1672 context = self.MakeContext()
1673
1674
1675 if self.HasCommandOption("no-output"):
1676
1677 result_file_name = None
1678 else:
1679 result_file_name = self.GetCommandOption("output")
1680 if result_file_name is None:
1681
1682 result_file_name = self.results_file_name
1683
1684
1685
1686 result_streams = self.__CreateResultStreams(result_file_name,
1687 annotations,
1688 expectations)
1689
1690 if self.HasCommandOption("random"):
1691
1692 random.shuffle(test_ids)
1693 else:
1694 test_ids.sort()
1695
1696
1697 engine = ExecutionEngine(database, test_ids, context, targets,
1698 result_streams,
1699 expectations)
1700 if engine.Run():
1701 return 1
1702
1703 return 0
1704
1705
1707 """Process the server command."""
1708
1709 database = self.GetDatabase()
1710
1711
1712
1713 port_number = self.GetCommandOption("port", default=0)
1714 try:
1715 port_number = int(port_number)
1716 except ValueError:
1717 raise qm.cmdline.CommandError, qm.error("bad port number")
1718
1719
1720
1721
1722 address = self.GetCommandOption("address", default="127.0.0.1")
1723
1724
1725 log_file_path = self.GetCommandOption("log-file")
1726 if log_file_path == "-":
1727
1728 log_file = sys.stdout
1729 elif log_file_path is None:
1730
1731 log_file = None
1732 else:
1733
1734 log_file = open(log_file_path, "a+")
1735
1736
1737 pid_file_path = self.GetCommandOption("pid-file")
1738 if pid_file_path is not None:
1739
1740
1741 if not pid_file_path:
1742 pid_file_path = qm.common.rc.Get("pid-file",
1743 "/var/run/qmtest.pid",
1744 "qmtest")
1745 try:
1746 pid_file = open(pid_file_path, "w")
1747 except IOError, e:
1748 raise qm.cmdline.CommandError, str(e)
1749 else:
1750 pid_file = None
1751
1752
1753 run_db = None
1754 directory = self.GetCommandOption("results", default="")
1755 if directory:
1756 directory = os.path.normpath(directory)
1757 run_db = DirRunDatabase(directory, database)
1758
1759
1760
1761 expectations = self.GetCommandOption('outcomes')
1762 expectations = base.load_expectations(expectations, database)
1763
1764
1765 if not type(expectations) in (ExpectationDatabase, PreviousTestRun):
1766 raise qm.cmdline.CommandError, 'not a valid results file'
1767
1768 targets = self.GetTargets()
1769
1770 context = self.MakeContext()
1771
1772 server = QMTestServer(database,
1773 port_number, address,
1774 log_file, targets, context,
1775 expectations,
1776 run_db)
1777 port_number = server.GetServerAddress()[1]
1778
1779
1780 if address == "":
1781 url_address = qm.platform.get_host_name()
1782 else:
1783 url_address = address
1784 if run_db:
1785 url = "http://%s:%d/report/dir" % (url_address, port_number)
1786 else:
1787 url = "http://%s:%d/test/dir" % (url_address, port_number)
1788
1789 if not self.HasCommandOption("no-browser"):
1790
1791
1792 qm.platform.open_in_browser(url)
1793
1794 message = qm.message("server url", url=url)
1795 sys.stderr.write(message + "\n")
1796
1797
1798 if self.GetCommandOption("daemon") is not None:
1799
1800 if os.fork() != 0:
1801 os._exit(0)
1802 if os.fork() != 0:
1803 os._exit(0)
1804
1805
1806
1807
1808
1809 try:
1810 if pid_file:
1811 pid_file.write(str(os.getpid()))
1812 pid_file.close()
1813
1814
1815 try:
1816 server.Run()
1817 except qm.platform.SignalException, se:
1818 if se.GetSignalNumber() == signal.SIGTERM:
1819
1820 pass
1821 else:
1822
1823 raise
1824 except KeyboardInterrupt:
1825
1826 pass
1827 finally:
1828 if pid_file:
1829 os.remove(pid_file_path)
1830
1831 return 0
1832
1833
1835 """Write out help information about 'command'.
1836
1837 'command' -- The name of the command for which help information
1838 is required."""
1839
1840 self._stderr.write(self.__parser.GetCommandHelp(command))
1841
1843 """Return those tests from 'test_ids' that should be run.
1844
1845 'test_ids' -- A sequence of test ids.
1846
1847 'expectations' -- An ExpectationDatabase.
1848
1849 returns -- Those elements of 'test_names' that are not to be
1850 skipped. If 'a' precedes 'b' in 'test_ids', and both 'a' and
1851 'b' are present in the result, 'a' will precede 'b' in the
1852 result."""
1853
1854
1855
1856 rerun_file_name = self.GetCommandOption("rerun")
1857 if rerun_file_name:
1858
1859 outcomes = base.load_outcomes(rerun_file_name,
1860 self.GetDatabase())
1861
1862 test_ids = [t for t in test_ids
1863 if outcomes.get(t, Result.PASS)
1864 != expectations.Lookup(t).GetOutcome()]
1865
1866 return test_ids
1867
1868
1870 """Check that 'kind' is a valid extension kind.
1871
1872 'kind' -- A string giving the name of an extension kind. If the
1873 'kind' does not name a valid extension kind, an appropriate
1874 exception is raised."""
1875
1876 if kind not in base.extension_kinds:
1877 raise qm.cmdline.CommandError, \
1878 qm.error("invalid extension kind",
1879 kind = kind)
1880
1881
1883 """Return the result streams to use.
1884
1885 'output_file' -- If not 'None', the name of a file to which
1886 the standard results file format should be written.
1887
1888 'annotations' -- A dictionary with annotations for this test run.
1889
1890 'expectations' -- An ExpectationDatabase.
1891
1892 returns -- A list of 'ResultStream' objects, as indicated by the
1893 user."""
1894
1895 database = self.GetDatabaseIfAvailable()
1896
1897 result_streams = []
1898
1899 arguments = {}
1900 arguments['expected_outcomes'] = expectations.GetExpectedOutcomes()
1901
1902
1903 format = self.GetCommandOption("format", "")
1904 if format and format not in self.summary_formats:
1905
1906 valid_format_string = string.join(
1907 map(lambda f: '"%s"' % f, self.summary_formats), ", ")
1908 raise qm.cmdline.CommandError, \
1909 qm.error("invalid results format",
1910 format=format,
1911 valid_formats=valid_format_string)
1912 if format != "none":
1913 args = { "format" : format }
1914 args.update(arguments)
1915 stream = self.GetTextResultStreamClass()(args)
1916 result_streams.append(stream)
1917
1918 f = lambda n: get_extension_class(n, "result_stream", database)
1919
1920
1921 for opt, opt_arg in self.__command_options:
1922 if opt == "result-stream":
1923 ec, args = qm.extension.parse_descriptor(opt_arg, f)
1924 args.update(arguments)
1925 result_streams.append(ec(args))
1926
1927
1928
1929 if output_file is not None:
1930 rs = (self.GetFileResultStreamClass()
1931 ({ "filename" : output_file}))
1932 result_streams.append(rs)
1933
1934 for name, value in annotations.iteritems():
1935 for rs in result_streams:
1936 rs.WriteAnnotation(name, value)
1937
1938 return result_streams
1939
1940
1941
1942
1943
1945 """Returns the global QMTest object.
1946
1947 returns -- The 'QMTest' object that corresponds to the currently
1948 executing thread.
1949
1950 At present, there is only one QMTest object per process. In the
1951 future, however, there may be more than one. Then, this function
1952 will return different values in different threads."""
1953
1954 return _the_qmtest
1955
1956
1957
1958
1959
1960
1961
1962