1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 from calendar import timegm
21 import ConfigParser
22 import imp
23 import lock
24 import os
25 import os.path
26 import qm
27 import re
28 import string
29 import sys
30 import tempfile
31 import time
32 import traceback
33 import types
34 import getpass
35 import StringIO
36 import htmllib
37 import formatter
38 if sys.platform != "win32":
39 import fcntl
40
41
42
43
44
45 program_name = None
46 """The name of the application program."""
47
48
49
50
51
53 """An exception generated directly by QM.
54
55 All exceptions thrown by QM should be derived from this class."""
56
58 """Construct a new 'QMException'.
59
60 'message' -- A string describing the cause of the message as
61 structured text. If this exception is not handled, the
62 'message' will be displayed as an error message."""
63
64 Exception.__init__(self, message)
65
66
67
69
70 pass
71
72
73
75 """A 'PythonException' is a wrapper around a Python exception.
76
77 A 'PythonException' is a 'QMException' and, as such, can be
78 processed by the QM error-handling routines. However, the raw
79 Python exception which triggered this exception can be obtained by
80 using the 'exc_type' and 'exc_value' attributes of this
81 exception."""
82
83 - def __init__(self, message, exc_type, exc_value):
84 """Construct a new 'PythonException'.
85
86 'message' -- A string describing the cause of the message as
87 structured text. If this exception is not handled, the
88 'message' will be displayed as an error message.
89
90 'exc_type' -- The type of the Python exception.
91
92 'exc_value' -- The value of the Python exception."""
93
94 QMException.__init__(self, message)
95
96 self.exc_type = exc_type
97 self.exc_value = exc_value
98
99
100
101
102
104 """Interface object to QM configuration files.
105
106 Configuration files are in the format parsed by the standard
107 'ConfigParser' module, namely 'win.ini'--style files."""
108
109 user_rc_file_name = ".qmrc"
110 """The name of the user configuration file."""
111
112
114 """Create a new configuration instance."""
115
116 ConfigParser.ConfigParser.__init__(self)
117 if os.environ.has_key("HOME"):
118 home_directory = os.environ["HOME"]
119 rc_file = os.path.join(home_directory, self.user_rc_file_name)
120
121
122
123 self.read(rc_file)
124
125
126 - def Load(self, section):
127 """Load configuration.
128
129 'section' -- The configuration section from which subsequent
130 variables are loaded."""
131
132 self.__section = section
133
134
135 - def Get(self, option, default, section=None):
136 """Retrieve a configuration variable.
137
138 'option' -- The name of the option to retrieve.
139
140 'default' -- The default value to return if the option is not
141 found.
142
143 'section' -- The section from which to retrieve the option.
144 'None' indicates the section specified to the 'Load' method for
145 this instance.
146
147 precondition -- The RC configuration must be loaded."""
148
149
150
151 if section is None:
152 section = self.__section
153
154 try:
155
156 return self.get(section, option)
157 except ConfigParser.NoSectionError:
158
159 return default
160 except ConfigParser.NoOptionError:
161
162 return default
163
164
166 """Return a sequence of options.
167
168 'section' -- The section for which to list options, or 'None'
169 for the section specified to 'Load'.
170
171 precondition -- The RC configuration must be loaded."""
172
173
174
175 if section is None:
176 section = self.__section
177 try:
178 options = self.options(section)
179 except ConfigParser.NoSectionError:
180
181 return []
182 else:
183
184
185 if "__name__" in options:
186 options.remove("__name__")
187 return options
188
189
190
191
192
193
195 """Return the path to a file in the QM library directory."""
196
197
198
199 return os.path.join(os.path.dirname(__file__), *components)
200
201
203 """Return the path to a file in the QM data file directory."""
204
205 return os.path.join(qm.prefix, qm.data_dir, *components)
206
207
209 """Return a path to a file in the QM documentation file directory."""
210
211 return os.path.join(qm.prefix, qm.doc_dir, *components)
212
213
227
228
238
239
241 """Replace CRLF with LF in 'text'."""
242
243 return string.replace(text, "\r\n", "\n")
244
245
246 __load_module_lock = lock.RLock()
247 """A lock used by load_module."""
248
249 -def load_module(name, search_path=sys.path, load_path=sys.path):
250 """Load a Python module.
251
252 'name' -- The fully-qualified name of the module to load, for
253 instance 'package.subpackage.module'.
254
255 'search_path' -- A sequence of directories. These directories are
256 searched to find the module.
257
258 'load_path' -- The setting of 'sys.path' when the module is loaded.
259
260 returns -- A module object.
261
262 raises -- 'ImportError' if the module cannot be found."""
263
264
265
266
267
268
269
270
271
272 __load_module_lock.acquire()
273 try:
274
275 module = sys.modules.get(name)
276 if module:
277 return module
278
279
280
281 components = string.split(name, ".")
282 if len(components) > 1:
283
284
285 parent_package = string.join(components[:-1], ".")
286
287 package = load_module(parent_package, search_path, load_path)
288
289 search_path = package.__path__
290 else:
291
292 package = None
293
294
295 module_name = components[-1]
296
297 file, file_name, description = imp.find_module(module_name,
298 search_path)
299
300 try:
301
302
303
304
305 old_python_path = sys.path[:]
306 sys.path = load_path + sys.path
307
308 try:
309 module = imp.load_module(name, file, file_name, description)
310 except:
311
312 if sys.modules.has_key(name):
313 del sys.modules[name]
314 raise
315
316 sys.path = old_python_path
317
318
319 if package is not None:
320 setattr(package, module_name, module)
321 return module
322 finally:
323
324 if file is not None:
325 file.close()
326 finally:
327
328 __load_module_lock.release()
329
330
331 -def load_class(name, search_path = sys.path, load_path = sys.path):
332 """Load a Python class.
333
334 'name' -- The fully-qualified (including package and module names)
335 class name, for instance 'package.subpackage.module.MyClass'. The
336 class must be at the top level of the module's namespace, i.e. not
337 nested in another class.
338
339 'search_path' -- A sequence of directories. These directories are
340 searched to find the module.
341
342 'load_path' -- The setting of 'sys.path' when the module is loaded.
343
344 returns -- A class object.
345
346 raises -- 'ImportError' if the module containing the class can't be
347 imported, or if there is no class with the specified name in that
348 module, or if 'name' doesn't correspond to a class."""
349
350
351
352
353 if not "." in name:
354 raise QMException, \
355 "%s is not a fully-qualified class name" % name
356
357 components = string.split(name, ".")
358
359 module_name = string.join(components[:-1], ".")
360
361 class_name = components[-1]
362
363 module = load_module(module_name, search_path, load_path)
364
365 try:
366 klass = module.__dict__[class_name]
367
368
369
370
371 if (not isinstance(klass, types.ClassType)
372 and not issubclass(klass, object)):
373
374 raise QMException, "%s is not a class" % name
375 return klass
376 except KeyError:
377
378 raise QMException, \
379 "no class named %s in module %s" % (class_name, module_name)
380
381
383 """Split 'path' into components.
384
385 Uses 'os.path.split' recursively on the directory components of
386 'path' to separate all path components.
387
388 'path' -- The path to split.
389
390 returns -- A list of path componets."""
391
392 dir, entry = os.path.split(path)
393 if dir == "" or dir == os.sep:
394 return [ entry ]
395 else:
396 return split_path_fully(dir) + [ entry ]
397
398
400 """Create and open a temporary file.
401
402 'suffix' -- The last part of the temporary file name, as for
403 Python's 'mktemp' function.
404
405 The file is open for reading and writing. The caller is responsible
406 for deleting the file when finished with it.
407
408 returns -- A pair '(file_name, file_descriptor)' for the temporary
409 file."""
410
411 file_name = tempfile.mktemp(suffix)
412
413 try:
414
415 fd = os.open(file_name,
416 os.O_CREAT | os.O_EXCL | os.O_RDWR,
417 0600)
418 except:
419 exc_info = sys.exc_info()
420 raise QMException, \
421 qm.error("temp file error",
422 file_name=file_name,
423 exc_class=str(exc_info[0]),
424 exc_arg=str(exc_info[1]))
425 return (file_name, fd)
426
427
429 """Create and open a temporary file.
430
431 'mode' -- The mode argument to pass to 'fopen'.
432
433 'suffix' -- The last part of the temporary file name, as for
434 Python's 'mktemp' function.
435
436 Like 'open_temporary_file_fd', except that the second element of the
437 return value is a file object."""
438
439
440
441 file_name, fd = open_temporary_file_fd(suffix)
442 return (file_name, os.fdopen(fd, mode))
443
444
446 """Prevent 'fd' from being inherited across 'exec'.
447
448 'fd' -- A file descriptor, or object providing a 'fileno()'
449 method.
450
451 This function has no effect on Windows."""
452
453 if sys.platform != "win32":
454 flags = fcntl.fcntl(fd, fcntl.F_GETFD)
455 try:
456 flags |= fcntl.FD_CLOEXEC
457 except AttributeError:
458
459
460
461 flags |= 1
462 fcntl.fcntl(fd, fcntl.F_SETFD, flags)
463
464
466 """Make a best-effort attempt to copy 'object'.
467
468 returns -- A copy of 'object', if feasible, or otherwise
469 'object'."""
470
471 if type(object) is types.ListType:
472
473 return object[:]
474 elif type(object) is types.DictionaryType:
475
476 return object.copy()
477 elif type(object) is types.InstanceType:
478
479
480 copy_function = getattr(object, "copy", None)
481 if callable(copy_function):
482 return object.copy()
483 else:
484 return object
485 else:
486
487 return object
488
489
490 -def wrap_lines(text, columns=72, break_delimiter="\\", indent=""):
491 """Wrap lines in 'text' to 'columns' columns.
492
493 'text' -- The text to wrap.
494
495 'columns' -- The maximum number of columns of text.
496
497 'break_delimiter' -- Text to place at the end of each broken line
498 (may be an empty string).
499
500 'indent' -- Text to place at the start of each line. The length of
501 'indent' does not count towards 'columns'.
502
503 returns -- The wrapped text."""
504
505
506 lines = string.split(text, "\n")
507
508
509 new_length = columns - len(break_delimiter)
510
511 for index in range(0, len(lines)):
512 line = lines[index]
513
514 if len(line) > columns:
515
516 breaks = len(line) / new_length
517 new_line = ""
518
519 while breaks > 0:
520 new_line = new_line \
521 + line[:new_length] \
522 + break_delimiter \
523 + "\n" + indent
524 line = line[new_length:]
525 breaks = breaks - 1
526 new_line = new_line + line
527
528 lines[index] = new_line
529
530 lines = map(lambda l, i=indent: i + l, lines)
531
532 return string.join(lines, "\n")
533
534
559
560
578
579
581 """Parse a ISO8601-compliant formatted date and time.
582
583 See also 'format_time_iso'.
584
585 'time_string' -- The string to be parsed, as returned by
586 e.g. 'format_time_iso'.
587
588 returns -- The time as a float, like that returned by
589 'time.time'."""
590
591 return time.mktime(time.strptime(time_string, "%Y-%m-%dT%H:%M:%SZ"))
592
593
602
603
605 """Split a command into an argument list.
606
607 'command' -- A string containing a shell or similar command.
608
609 returns -- An argument list obtained by splitting the command."""
610
611
612 command = string.strip(command)
613
614 argument_list = re.split(" +", command)
615 return argument_list
616
617
619 """Parse a boolean string.
620
621 'value' -- A string.
622
623 returns -- True if 'value' is a true string, false if 'value' is a
624 false string.
625
626 raises -- 'ValueError' if 'value' is neither a true string, nor a
627 false string."""
628
629 value = value.lower()
630 if value in ("1", "true", "yes", "on"):
631 return 1
632 elif value in ("0", "false", "no", "off"):
633 return 0
634 else:
635 raise ValueError, value
636
637
639 """Parse a string list.
640
641 'value' -- A string.
642
643 returns -- A list of strings.
644
645 raises -- 'ValueError' if 'value' contains unbalanced quotes."""
646
647
648 if "'" not in value and '"' not in value:
649 return value.split()
650
651 breaks = []
652 esc = False
653 quoted_1 = False
654 quoted_2 = False
655 value.strip()
656
657 for i, c in enumerate(value):
658 if c == '\\':
659 esc = not esc
660 continue
661 elif c == "'":
662 if not esc and not quoted_2:
663 quoted_1 = not quoted_1
664 elif c == '"':
665 if not esc and not quoted_1:
666 quoted_2 = not quoted_2
667 elif c in [' ', '\t']:
668
669 if not (quoted_1 or quoted_2 or esc):
670 breaks.append(i)
671 esc = False
672
673 if quoted_1 or quoted_2 or esc:
674 raise ValueError, value
675 string_list = []
676 start = 0
677 for end in breaks:
678 string_list.append(value[start:end])
679 start = end
680 string_list.append(value[start:])
681 return [s.strip() for s in string_list if s not in [' ', '\t']]
682
683
684
685
686
687
688 -def parse_time(time_string, default_local_time_zone=1):
689 """Parse a date and/or time string.
690
691 'time_string' -- A string representing a date and time in the format
692 returned by 'format_time'. This function makes a best-effort
693 attempt to parse incomplete strings as well.
694
695 'default_local_time_zone' -- If the time zone is not specified in
696 'time_string' and this parameter is true, assume the time is in the
697 local time zone. If this parameter is false, assume the time is
698 UTC.
699
700 returns -- An integer number of seconds since the start of the UNIX
701 epoch, UTC.
702
703 Only UTC and the current local time zone may be specified explicitly
704 in 'time_string'."""
705
706
707 time_string = string.strip(time_string)
708 time_string = re.sub(" +", " ", time_string)
709 time_string = re.sub("/", "-", time_string)
710
711
712
713 time_string = re.sub("GMT Standard Time", "UTC", time_string)
714
715 components = string.split(time_string, " ")
716
717
718 if components[-1] == "UTC":
719
720 utc = 1
721 dst = 0
722 components.pop()
723 elif components[-1] == time.tzname[0]:
724
725 utc = 0
726 dst = 0
727 components.pop()
728 elif time.daylight and components[-1] == time.tzname[1]:
729
730 utc = 0
731 dst = 1
732 components.pop()
733 else:
734
735 if default_local_time_zone:
736 utc = 0
737 dst = -1
738 else:
739 utc = 1
740 dst = 0
741
742
743 if utc:
744 time_tuple = time.gmtime(time.time())
745 else:
746 time_tuple = time.localtime(time.time())
747
748 year, month, day = time_tuple[:3]
749
750 hour = 0
751 minute = 0
752
753
754 for component in components:
755 if string.count(component, "-") == 2:
756
757 year, month, day = map(int, string.split(component, "-"))
758 elif string.count(component, ":") in [1, 2]:
759
760 hour, minute = map(int, string.split(component, ":")[:2])
761 else:
762
763 raise ValueError
764
765
766 time_tuple = (year, month, day, hour, minute, 0, 0, 0, dst)
767
768 if utc:
769 return int(timegm(time_tuple))
770 else:
771 return int(time.mktime(time_tuple))
772
773
775 """Parse an 'assignment' of the form 'name=value'.
776
777 'aassignment' -- A string. The string should have the form
778 'name=value'.
779
780 returns -- A pair '(name, value)'."""
781
782
783 try:
784 (name, value) = string.split(assignment, "=", 1)
785 return (name, value)
786 except:
787 raise QMException, \
788 qm.error("invalid keyword assignment",
789 argument=assignment)
790
791
793 """Read assignments from a 'file'.
794
795 'file' -- A file object containing the context. When the file is
796 read, leading and trailing whitespace is discarded from each line
797 in the file. Then, lines that begin with a '#' and lines that
798 contain no characters are discarded. All other lines must be of
799 the form 'NAME=VALUE' and indicate an assignment to the context
800 variable 'NAME' of the indicated 'VALUE'.
801
802 returns -- A dictionary mapping each of the indicated 'NAME's to its
803 corresponding 'VALUE'. If multiple assignments to the same 'NAME'
804 are present, only the 'VALUE' from the last assignment is stored."""
805
806
807 assignments = {}
808
809
810 lines = file.readlines()
811
812 lines = map(string.strip, lines)
813
814
815 lines = filter(lambda x: x != "" and not x.startswith("#"),
816 lines)
817
818 for line in lines:
819
820 (name, value) = parse_assignment(line)
821
822 assignments[name] = value
823
824 return assignments
825
826
828 """Returns the current username as a string.
829
830 This is our best guess as to the username of the user who is
831 actually logged in, as opposed to the effective user id used for
832 running tests.
833
834 If the username cannot be found, raises a 'QMException'."""
835
836
837 try:
838 return getpass.getuser()
839 except:
840 pass
841
842
843
844 try:
845 import win32api
846 except ImportError:
847 pass
848 else:
849 try:
850 return win32api.GetUserName()
851 except:
852 raise PythonException("Error accessing win32 user database",
853 *sys.exc_info()[:2])
854
855
856 raise QMException, "Cannot determine user name."
857
858
860 """Returns the current user id as an integer.
861
862 This is the real user id, not the effective user id, to better track
863 who is actually running the tests.
864
865 If the user id cannot be found or is not defined, raises a
866 'QMException'."""
867
868 try:
869 uid = os.getuid()
870 except AttributeError:
871 raise QMException, "User ids not supported on this system."
872 return uid
873
874
875 -def html_to_text(html, width=72):
876 """Renders HTML to text in a simple way.
877
878 'html' -- A string containing the HTML code to be rendered.
879
880 'width' -- Column at which to word-wrap. Default 72.
881
882 returns -- A string containing a plain text rendering of the
883 HTML."""
884
885 s = StringIO.StringIO()
886 w = formatter.DumbWriter(s, width)
887 f = formatter.AbstractFormatter(w)
888 p = htmllib.HTMLParser(f)
889 p.feed(html)
890 p.close()
891 return s.getvalue()
892
893
894
895
896
897
898 rc = RcConfiguration()
899 """The configuration stored in system and user rc files."""
900
901
902 _unique_tag = 0
903
904
905
906
907
908
909
910