Added the capability for the test driver to split the sys.stderr/sys.stdout into
[lldb.git] / lldb / test / dotest.py
1 #!/usr/bin/env python
2
3 """
4 A simple testing framework for lldb using python's unit testing framework.
5
6 Tests for lldb are written as python scripts which take advantage of the script
7 bridging provided by LLDB.framework to interact with lldb core.
8
9 A specific naming pattern is followed by the .py script to be recognized as
10 a module which implements a test scenario, namely, Test*.py.
11
12 To specify the directories where "Test*.py" python test scripts are located,
13 you need to pass in a list of directory names.  By default, the current
14 working directory is searched if nothing is specified on the command line.
15
16 Type:
17
18 ./dotest.py -h
19
20 for available options.
21 """
22
23 import os, signal, sys, time
24 import unittest2
25
26 class _WritelnDecorator(object):
27     """Used to decorate file-like objects with a handy 'writeln' method"""
28     def __init__(self,stream):
29         self.stream = stream
30
31     def __getattr__(self, attr):
32         if attr in ('stream', '__getstate__'):
33             raise AttributeError(attr)
34         return getattr(self.stream,attr)
35
36     def writeln(self, arg=None):
37         if arg:
38             self.write(arg)
39         self.write('\n') # text-mode streams translate to \r\n if needed
40
41 #
42 # Global variables:
43 #
44
45 # The test suite.
46 suite = unittest2.TestSuite()
47
48 # The config file is optional.
49 configFile = None
50
51 # The dictionary as a result of sourcing configFile.
52 config = {}
53
54 # Delay startup in order for the debugger to attach.
55 delay = False
56
57 # The filter (testcase.testmethod) used to admit tests into our test suite.
58 filterspec = None
59
60 # If '-g' is specified, the filterspec must be consulted for each test module, default to False.
61 fs4all = False
62
63 # Ignore the build search path relative to this script to locate the lldb.py module.
64 ignore = False
65
66 # By default, we skip long running test case.  Use '-l' option to override.
67 skipLongRunningTest = True
68
69 # The regular expression pattern to match against eligible filenames as our test cases.
70 regexp = None
71
72 # By default, tests are executed in place and cleanups are performed afterwards.
73 # Use '-r dir' option to relocate the tests and their intermediate files to a
74 # different directory and to forgo any cleanups.  The directory specified must
75 # not exist yet.
76 rdir = None
77
78 # Default verbosity is 0.
79 verbose = 0
80
81 # By default, search from the current working directory.
82 testdirs = [ os.getcwd() ]
83
84 # Separator string.
85 separator = '-' * 70
86
87
88 def usage():
89     print """
90 Usage: dotest.py [option] [args]
91 where options:
92 -h   : print this help message and exit (also --help)
93 -c   : read a config file specified after this option
94        (see also lldb-trunk/example/test/usage-config)
95 -d   : delay startup for 10 seconds (in order for the debugger to attach)
96 -f   : specify a filter, which consists of the test class name, a dot, followed by
97        the test method, to admit tests into the test suite
98        e.g., -f 'ClassTypesTestCase.test_with_dwarf_and_python_api'
99 -g   : if specified, only the modules with the corect filter will be run
100        it has no effect if no '-f' option is present
101        '-f filterspec -g' can be used with '-p filename-regexp' to select only
102        the testfile.testclass.testmethod to run
103 -i   : ignore (don't bailout) if 'lldb.py' module cannot be located in the build
104        tree relative to this script; use PYTHONPATH to locate the module
105 -l   : don't skip long running test
106 -p   : specify a regexp filename pattern for inclusion in the test suite
107 -r   : specify a dir to relocate the tests and their intermediate files to;
108        the directory must not exist before running this test driver;
109        no cleanup of intermediate test files is performed in this case
110 -t   : trace lldb command execution and result
111 -v   : do verbose mode of unittest framework
112 -w   : insert some wait time (currently 0.5 sec) between consecutive test cases
113
114 and:
115 args : specify a list of directory names to search for python Test*.py scripts
116        if empty, search from the curret working directory, instead
117
118 Running of this script also sets up the LLDB_TEST environment variable so that
119 individual test cases can locate their supporting files correctly.  The script
120 tries to set up Python's search paths for modules by looking at the build tree
121 relative to this script.  See also the '-i' option.
122
123 Environment variables related to loggings:
124
125 o LLDB_LOG: if defined, specifies the log file pathname for the 'lldb' subsystem
126   with a default option of 'event process' if LLDB_LOG_OPTION is not defined.
127
128 o GDB_REMOTE_LOG: if defined, specifies the log file pathname for the
129   'process.gdb-remote' subsystem with a default option of 'packets' if
130   GDB_REMOTE_LOG_OPTION is not defined.
131 """
132     sys.exit(0)
133
134
135 def parseOptionsAndInitTestdirs():
136     """Initialize the list of directories containing our unittest scripts.
137
138     '-h/--help as the first option prints out usage info and exit the program.
139     """
140
141     global configFile
142     global delay
143     global filterspec
144     global fs4all
145     global ignore
146     global skipLongRunningTest
147     global regexp
148     global rdir
149     global verbose
150     global testdirs
151
152     if len(sys.argv) == 1:
153         return
154
155     # Process possible trace and/or verbose flag, among other things.
156     index = 1
157     while index < len(sys.argv):
158         if not sys.argv[index].startswith('-'):
159             # End of option processing.
160             break
161
162         if sys.argv[index].find('-h') != -1:
163             usage()
164         elif sys.argv[index].startswith('-c'):
165             # Increment by 1 to fetch the config file name option argument.
166             index += 1
167             if index >= len(sys.argv) or sys.argv[index].startswith('-'):
168                 usage()
169             configFile = sys.argv[index]
170             if not os.path.isfile(configFile):
171                 print "Config file:", configFile, "does not exist!"
172                 usage()
173             index += 1
174         elif sys.argv[index].startswith('-d'):
175             delay = True
176             index += 1
177         elif sys.argv[index].startswith('-f'):
178             # Increment by 1 to fetch the filter spec.
179             index += 1
180             if index >= len(sys.argv) or sys.argv[index].startswith('-'):
181                 usage()
182             filterspec = sys.argv[index]
183             index += 1
184         elif sys.argv[index].startswith('-g'):
185             fs4all = True
186             index += 1
187         elif sys.argv[index].startswith('-i'):
188             ignore = True
189             index += 1
190         elif sys.argv[index].startswith('-l'):
191             skipLongRunningTest = False
192             index += 1
193         elif sys.argv[index].startswith('-p'):
194             # Increment by 1 to fetch the reg exp pattern argument.
195             index += 1
196             if index >= len(sys.argv) or sys.argv[index].startswith('-'):
197                 usage()
198             regexp = sys.argv[index]
199             index += 1
200         elif sys.argv[index].startswith('-r'):
201             # Increment by 1 to fetch the relocated directory argument.
202             index += 1
203             if index >= len(sys.argv) or sys.argv[index].startswith('-'):
204                 usage()
205             rdir = os.path.abspath(sys.argv[index])
206             if os.path.exists(rdir):
207                 print "Relocated directory:", rdir, "must not exist!"
208                 usage()
209             index += 1
210         elif sys.argv[index].startswith('-t'):
211             os.environ["LLDB_COMMAND_TRACE"] = "YES"
212             index += 1
213         elif sys.argv[index].startswith('-v'):
214             verbose = 2
215             index += 1
216         elif sys.argv[index].startswith('-w'):
217             os.environ["LLDB_WAIT_BETWEEN_TEST_CASES"] = 'YES'
218             index += 1
219         else:
220             print "Unknown option: ", sys.argv[index]
221             usage()
222
223     # Gather all the dirs passed on the command line.
224     if len(sys.argv) > index:
225         testdirs = map(os.path.abspath, sys.argv[index:])
226
227     # If '-r dir' is specified, the tests should be run under the relocated
228     # directory.  Let's copy the testdirs over.
229     if rdir:
230         from shutil import copytree, ignore_patterns
231
232         tmpdirs = []
233         for srcdir in testdirs:
234             dstdir = os.path.join(rdir, os.path.basename(srcdir))
235             # Don't copy the *.pyc and .svn stuffs.
236             copytree(srcdir, dstdir, ignore=ignore_patterns('*.pyc', '.svn'))
237             tmpdirs.append(dstdir)
238
239         # This will be our modified testdirs.
240         testdirs = tmpdirs
241
242         # With '-r dir' specified, there's no cleanup of intermediate test files.
243         os.environ["LLDB_DO_CLEANUP"] = 'NO'
244
245         # If testdirs is ['test'], the make directory has already been copied
246         # recursively and is contained within the rdir/test dir.  For anything
247         # else, we would need to copy over the make directory and its contents,
248         # so that, os.listdir(rdir) looks like, for example:
249         #
250         #     array_types conditional_break make
251         #
252         # where the make directory contains the Makefile.rules file.
253         if len(testdirs) != 1 or os.path.basename(testdirs[0]) != 'test':
254             # Don't copy the .svn stuffs.
255             copytree('make', os.path.join(rdir, 'make'),
256                      ignore=ignore_patterns('.svn'))
257
258     #print "testdirs:", testdirs
259
260     # Source the configFile if specified.
261     # The side effect, if any, will be felt from this point on.  An example
262     # config file may be these simple two lines:
263     #
264     # sys.stderr = open("/tmp/lldbtest-stderr", "w")
265     # sys.stdout = open("/tmp/lldbtest-stdout", "w")
266     #
267     # which will reassign the two file objects to sys.stderr and sys.stdout,
268     # respectively.
269     #
270     # See also lldb-trunk/example/test/usage-config.
271     global config
272     if configFile:
273         # Pass config (a dictionary) as the locals namespace for side-effect.
274         execfile(configFile, globals(), config)
275         #print "config:", config
276         #print "sys.stderr:", sys.stderr
277         #print "sys.stdout:", sys.stdout
278
279
280 def setupSysPath():
281     """Add LLDB.framework/Resources/Python to the search paths for modules."""
282
283     global rdir
284     global testdirs
285
286     # Get the directory containing the current script.
287     scriptPath = sys.path[0]
288     if not scriptPath.endswith('test'):
289         print "This script expects to reside in lldb's test directory."
290         sys.exit(-1)
291
292     if rdir:
293         # Set up the LLDB_TEST environment variable appropriately, so that the
294         # individual tests can be located relatively.
295         #
296         # See also lldbtest.TestBase.setUpClass(cls).
297         if len(testdirs) == 1 and os.path.basename(testdirs[0]) == 'test':
298             os.environ["LLDB_TEST"] = os.path.join(rdir, 'test')
299         else:
300             os.environ["LLDB_TEST"] = rdir
301     else:
302         os.environ["LLDB_TEST"] = scriptPath
303     pluginPath = os.path.join(scriptPath, 'plugins')
304
305     # Append script dir and plugin dir to the sys.path.
306     sys.path.append(scriptPath)
307     sys.path.append(pluginPath)
308     
309     global ignore
310
311     # The '-i' option is used to skip looking for lldb.py in the build tree.
312     if ignore:
313         return
314         
315     base = os.path.abspath(os.path.join(scriptPath, os.pardir))
316     dbgPath = os.path.join(base, 'build', 'Debug', 'LLDB.framework',
317                            'Resources', 'Python')
318     relPath = os.path.join(base, 'build', 'Release', 'LLDB.framework',
319                            'Resources', 'Python')
320     baiPath = os.path.join(base, 'build', 'BuildAndIntegration',
321                            'LLDB.framework', 'Resources', 'Python')
322
323     lldbPath = None
324     if os.path.isfile(os.path.join(dbgPath, 'lldb.py')):
325         lldbPath = dbgPath
326     elif os.path.isfile(os.path.join(relPath, 'lldb.py')):
327         lldbPath = relPath
328     elif os.path.isfile(os.path.join(baiPath, 'lldb.py')):
329         lldbPath = baiPath
330
331     if not lldbPath:
332         print 'This script requires lldb.py to be in either ' + dbgPath + ',',
333         print relPath + ', or ' + baiPath
334         sys.exit(-1)
335
336     # This is to locate the lldb.py module.  Insert it right after sys.path[0].
337     sys.path[1:1] = [lldbPath]
338
339
340 def doDelay(delta):
341     """Delaying startup for delta-seconds to facilitate debugger attachment."""
342     def alarm_handler(*args):
343         raise Exception("timeout")
344
345     signal.signal(signal.SIGALRM, alarm_handler)
346     signal.alarm(delta)
347     sys.stdout.write("pid=%d\n" % os.getpid())
348     sys.stdout.write("Enter RET to proceed (or timeout after %d seconds):" %
349                      delta)
350     sys.stdout.flush()
351     try:
352         text = sys.stdin.readline()
353     except:
354         text = ""
355     signal.alarm(0)
356     sys.stdout.write("proceeding...\n")
357     pass
358
359
360 def visit(prefix, dir, names):
361     """Visitor function for os.path.walk(path, visit, arg)."""
362
363     global suite
364     global regexp
365     global filterspec
366     global fs4all
367
368     for name in names:
369         if os.path.isdir(os.path.join(dir, name)):
370             continue
371
372         if '.py' == os.path.splitext(name)[1] and name.startswith(prefix):
373             # Try to match the regexp pattern, if specified.
374             if regexp:
375                 import re
376                 if re.search(regexp, name):
377                     #print "Filename: '%s' matches pattern: '%s'" % (name, regexp)
378                     pass
379                 else:
380                     #print "Filename: '%s' does not match pattern: '%s'" % (name, regexp)
381                     continue
382
383             # We found a match for our test.  Add it to the suite.
384
385             # Update the sys.path first.
386             if not sys.path.count(dir):
387                 sys.path.insert(0, dir)
388             base = os.path.splitext(name)[0]
389
390             # Thoroughly check the filterspec against the base module and admit
391             # the (base, filterspec) combination only when it makes sense.
392             if filterspec:
393                 # Optimistically set the flag to True.
394                 filtered = True
395                 module = __import__(base)
396                 parts = filterspec.split('.')
397                 obj = module
398                 for part in parts:
399                     try:
400                         parent, obj = obj, getattr(obj, part)
401                     except AttributeError:
402                         # The filterspec has failed.
403                         filtered = False
404                         break
405                 # Forgo this module if the (base, filterspec) combo is invalid
406                 # and the '-g' option is present.
407                 if fs4all and not filtered:
408                     continue
409                 
410             # Add either the filtered test case or the entire test class.
411             if filterspec and filtered:
412                 suite.addTests(
413                     unittest2.defaultTestLoader.loadTestsFromName(filterspec, module))
414             else:
415                 # A simple case of just the module name.  Also the failover case
416                 # from the filterspec branch when the (base, filterspec) combo
417                 # doesn't make sense.
418                 suite.addTests(unittest2.defaultTestLoader.loadTestsFromName(base))
419
420
421 def lldbLoggings():
422     """Check and do lldb loggings if necessary."""
423
424     # Turn on logging for debugging purposes if ${LLDB_LOG} environment variable is
425     # defined.  Use ${LLDB_LOG} to specify the log file.
426     ci = lldb.DBG.GetCommandInterpreter()
427     res = lldb.SBCommandReturnObject()
428     if ("LLDB_LOG" in os.environ):
429         if ("LLDB_LOG_OPTION" in os.environ):
430             lldb_log_option = os.environ["LLDB_LOG_OPTION"]
431         else:
432             lldb_log_option = "event process"
433         ci.HandleCommand(
434             "log enable -f " + os.environ["LLDB_LOG"] + " lldb " + lldb_log_option,
435             res)
436         if not res.Succeeded():
437             raise Exception('log enable failed (check LLDB_LOG env variable.')
438     # Ditto for gdb-remote logging if ${GDB_REMOTE_LOG} environment variable is defined.
439     # Use ${GDB_REMOTE_LOG} to specify the log file.
440     if ("GDB_REMOTE_LOG" in os.environ):
441         if ("GDB_REMOTE_LOG_OPTION" in os.environ):
442             gdb_remote_log_option = os.environ["GDB_REMOTE_LOG_OPTION"]
443         else:
444             gdb_remote_log_option = "packets"
445         ci.HandleCommand(
446             "log enable -f " + os.environ["GDB_REMOTE_LOG"] + " process.gdb-remote "
447             + gdb_remote_log_option,
448             res)
449         if not res.Succeeded():
450             raise Exception('log enable failed (check GDB_REMOTE_LOG env variable.')
451
452
453 ############################################
454 #                                          #
455 # Execution of the test driver starts here #
456 #                                          #
457 ############################################
458
459 #
460 # Start the actions by first parsing the options while setting up the test
461 # directories, followed by setting up the search paths for lldb utilities;
462 # then, we walk the directory trees and collect the tests into our test suite.
463 #
464 parseOptionsAndInitTestdirs()
465 setupSysPath()
466
467 #
468 # If '-d' is specified, do a delay of 10 seconds for the debugger to attach.
469 #
470 if delay:
471     doDelay(10)
472
473 #
474 # If '-l' is specified, do not skip the long running tests.
475 if not skipLongRunningTest:
476     os.environ["LLDB_SKIP_LONG_RUNNING_TEST"] = "NO"
477
478 #
479 # Walk through the testdirs while collecting tests.
480 #
481 for testdir in testdirs:
482     os.path.walk(testdir, visit, 'Test')
483
484 #
485 # Now that we have loaded all the test cases, run the whole test suite.
486 #
487
488 # For the time being, let's bracket the test runner within the
489 # lldb.SBDebugger.Initialize()/Terminate() pair.
490 import lldb, atexit
491 lldb.SBDebugger.Initialize()
492 atexit.register(lambda: lldb.SBDebugger.Terminate())
493
494 # Create a singleton SBDebugger in the lldb namespace.
495 lldb.DBG = lldb.SBDebugger.Create()
496
497 # Turn on lldb loggings if necessary.
498 lldbLoggings()
499
500 # Install the control-c handler.
501 unittest2.signals.installHandler()
502
503 #
504 # Invoke the default TextTestRunner to run the test suite, possibly iterating
505 # over different configurations.
506 #
507
508 iterArchs = False
509 iterCompilers = False
510
511 from types import *
512 if "archs" in config:
513     archs = config["archs"]
514     if type(archs) is ListType and len(archs) >= 1:
515         iterArchs = True
516 if "compilers" in config:
517     compilers = config["compilers"]
518     if type(compilers) is ListType and len(compilers) >= 1:
519         iterCompilers = True
520
521 # Make a shallow copy of sys.path, we need to manipulate the search paths later.
522 # This is only necessary if we are relocated and with different configurations.
523 if rdir and (iterArchs or iterCompilers):
524     old_sys_path = sys.path[:]
525     old_stderr = sys.stderr
526     old_stdout = sys.stdout
527     new_stderr = None
528     new_stdout = None
529
530 for ia in range(len(archs) if iterArchs else 1):
531     archConfig = ""
532     if iterArchs:
533         os.environ["ARCH"] = archs[ia]
534         archConfig = "arch=%s" % archs[ia]
535     for ic in range(len(compilers) if iterCompilers else 1):
536         if iterCompilers:
537             os.environ["CC"] = compilers[ic]
538             configString = "%s compiler=%s" % (archConfig, compilers[ic])
539         else:
540             configString = archConfig
541
542         if iterArchs or iterCompilers:
543             # If we specified a relocated directory to run the test suite, do
544             # the extra housekeeping to copy the testdirs to a configStringified
545             # directory and to update sys.path before invoking the test runner.
546             # The purpose is to separate the configuration-specific directories
547             # from each other.
548             if rdir:
549                 from string import maketrans
550                 from shutil import copytree, ignore_patterns
551
552                 # Translate ' ' to '-' for dir name.
553                 tbl = maketrans(' ', '-')
554                 configPostfix = configString.translate(tbl)
555                 newrdir = "%s.%s" % (rdir, configPostfix)
556
557                 # Copy the tree to a new directory with postfix name configPostfix.
558                 copytree(rdir, newrdir, ignore=ignore_patterns('*.pyc', '*.o', '*.d'))
559
560                 # Check whether we need to split stderr/stdout into configuration
561                 # specific files.
562                 if old_stderr.name != '<stderr>' and config.get('split_stderr'):
563                     if new_stderr:
564                         new_stderr.close()
565                     new_stderr = open("%s.%s" % (old_stderr.name, configPostfix), "w")
566                     sys.stderr = new_stderr
567                 if old_stdout.name != '<stderr>' and config.get('split_stderr'):
568                     if new_stdout:
569                         new_stdout.close()
570                     new_stdout = open("%s.%s" % (old_stdout.name, configPostfix), "w")
571                     sys.stdout = new_stdout
572
573                 # Update the LLDB_TEST environment variable to reflect new top
574                 # level test directory.
575                 #
576                 # See also lldbtest.TestBase.setUpClass(cls).
577                 if len(testdirs) == 1 and os.path.basename(testdirs[0]) == 'test':
578                     os.environ["LLDB_TEST"] = os.path.join(newrdir, 'test')
579                 else:
580                     os.environ["LLDB_TEST"] = newrdir
581
582                 # And update the Python search paths for modules.
583                 sys.path = [x.replace(rdir, newrdir, 1) for x in old_sys_path]
584
585             # Output the configuration.
586             sys.stderr.write("\nConfiguration: " + configString + "\n")
587
588         #print "sys.stderr name is", sys.stderr.name
589         #print "sys.stdout name is", sys.stdout.name
590
591         # First, write out the number of collected test cases.
592         sys.stderr.write(separator + "\n")
593         sys.stderr.write("Collected %d test%s\n\n"
594                          % (suite.countTestCases(),
595                             suite.countTestCases() != 1 and "s" or ""))
596
597         # Invoke the test runner.
598         result = unittest2.TextTestRunner(stream=sys.stderr, verbosity=verbose).run(suite)
599         
600
601 # Terminate the test suite if ${LLDB_TESTSUITE_FORCE_FINISH} is defined.
602 # This should not be necessary now.
603 if ("LLDB_TESTSUITE_FORCE_FINISH" in os.environ):
604     import subprocess
605     print "Terminating Test suite..."
606     subprocess.Popen(["/bin/sh", "-c", "kill %s; exit 0" % (os.getpid())])
607
608 # Exiting.
609 sys.exit(not result.wasSuccessful)