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