Testsuite: add/fix cleanup for certain tests
[ghc.git] / testsuite / driver / testlib.py
index d804e2f..dbae8d7 100644 (file)
@@ -2,8 +2,7 @@
 # (c) Simon Marlow 2002
 #
 
-# This allows us to use the "with X:" syntax with python 2.5:
-from __future__ import with_statement
+from __future__ import print_function
 
 import shutil
 import sys
@@ -12,25 +11,23 @@ import errno
 import string
 import re
 import traceback
+import time
+import datetime
 import copy
 import glob
-import types
 from math import ceil, trunc
+import collections
+import subprocess
 
-have_subprocess = False
-try:
-    import subprocess
-    have_subprocess = True
-except:
-    print "Warning: subprocess not found, will fall back to spawnv"
-
-from string import join
 from testglobals import *
 from testutil import *
 
 if config.use_threads:
     import threading
-    import thread
+    try:
+        import thread
+    except ImportError: # Python 3
+        import _thread as thread
 
 global wantToStop
 wantToStop = False
@@ -58,6 +55,11 @@ def setLocalTestOpts(opts):
     global testopts_local
     testopts_local.x=opts
 
+def isStatsTest():
+    opts = getTestOpts()
+    return len(opts.compiler_stats_range_fields) > 0 or len(opts.stats_range_fields) > 0
+
+
 # This can be called at the top of a file of tests, to set default test options
 # for the following tests.
 def setTestOpts( f ):
@@ -92,28 +94,27 @@ def reqlib( lib ):
 have_lib = {}
 
 def _reqlib( name, opts, lib ):
-    if have_lib.has_key(lib):
+    if lib in have_lib:
         got_it = have_lib[lib]
     else:
-        if have_subprocess:
-            # By preference we use subprocess, as the alternative uses
-            # /dev/null which mingw doesn't have.
-            p = subprocess.Popen([config.ghc_pkg, '--no-user-package-db', 'describe', lib],
-                                 stdout=subprocess.PIPE,
-                                 stderr=subprocess.PIPE)
-            # read from stdout and stderr to avoid blocking due to
-            # buffers filling
-            p.communicate()
-            r = p.wait()
-        else:
-            r = os.system(config.ghc_pkg + ' describe ' + lib
-                                         + ' > /dev/null 2> /dev/null')
+        cmd = strip_quotes(config.ghc_pkg)
+        p = subprocess.Popen([cmd, '--no-user-package-db', 'describe', lib],
+                             stdout=subprocess.PIPE,
+                             stderr=subprocess.PIPE)
+        # read from stdout and stderr to avoid blocking due to
+        # buffers filling
+        p.communicate()
+        r = p.wait()
         got_it = r == 0
         have_lib[lib] = got_it
 
     if not got_it:
         opts.expect = 'missing-lib'
 
+def req_haddock( name, opts ):
+    if not config.haddock:
+        opts.expect = 'missing-lib'
+
 def req_profiling( name, opts ):
     if not config.have_profiling:
         opts.expect = 'fail'
@@ -225,6 +226,17 @@ def exit_code( val ):
 def _exit_code( name, opts, v ):
     opts.exit_code = v
 
+def signal_exit_code( val ):
+    if opsys('solaris2'):
+        return exit_code( val );
+    else:
+        # When application running on Linux receives fatal error
+        # signal, then its exit code is encoded as 128 + signal
+        # value. See http://www.tldp.org/LDP/abs/html/exitcodes.html
+        # I assume that Mac OS X behaves in the same way at least Mac
+        # OS X builder behavior suggests this.
+        return exit_code( val+128 );
+
 # -----
 
 def timeout_multiplier( val ):
@@ -252,6 +264,7 @@ def _extra_hc_opts( name, opts, v ):
 # -----
 
 def extra_clean( files ):
+    assert not isinstance(files, str), files
     return lambda name, opts, v=files: _extra_clean(name, opts, v);
 
 def _extra_clean( name, opts, v ):
@@ -266,7 +279,7 @@ def _stats_num_field( name, opts, field, expecteds ):
     if field in opts.stats_range_fields:
         framework_fail(name, 'duplicate-numfield', 'Duplicate ' + field + ' num_field check')
 
-    if type(expecteds) is types.ListType:
+    if type(expecteds) is list:
         for (b, expected, dev) in expecteds:
             if b:
                 opts.stats_range_fields[field] = (expected, dev)
@@ -284,6 +297,11 @@ def _compiler_stats_num_field( name, opts, field, expecteds ):
     if field in opts.compiler_stats_range_fields:
         framework_fail(name, 'duplicate-numfield', 'Duplicate ' + field + ' num_field check')
 
+    # Compiler performance numbers change when debugging is on, making the results
+    # useless and confusing. Therefore, skip if debugging is on.
+    if compiler_debugged():
+        skip(name, opts)
+
     for (b, expected, dev) in expecteds:
         if b:
             opts.compiler_stats_range_fields[field] = (expected, dev)
@@ -456,34 +474,82 @@ def _compile_cmd_prefix( name, opts, prefix ):
 
 # ----
 
+def check_stdout( f ):
+    return lambda name, opts, f=f: _check_stdout(name, opts, f)
+
+def _check_stdout( name, opts, f ):
+    opts.check_stdout = f
+
+# ----
+
 def normalise_slashes( name, opts ):
-    opts.extra_normaliser = normalise_slashes_
+    _normalise_fun(name, opts, normalise_slashes_)
 
 def normalise_exe( name, opts ):
-    opts.extra_normaliser = normalise_exe_
+    _normalise_fun(name, opts, normalise_exe_)
+
+def normalise_fun( *fs ):
+    return lambda name, opts: _normalise_fun(name, opts, fs)
+
+def _normalise_fun( name, opts, *fs ):
+    opts.extra_normaliser = join_normalisers(opts.extra_normaliser, fs)
+
+def normalise_errmsg_fun( *fs ):
+    return lambda name, opts: _normalise_errmsg_fun(name, opts, fs)
+
+def _normalise_errmsg_fun( name, opts, *fs ):
+    opts.extra_errmsg_normaliser =  join_normalisers(opts.extra_errmsg_normaliser, fs)
+
+def normalise_version_( *pkgs ):
+    def normalise_version__( str ):
+        return re.sub('(' + '|'.join(map(re.escape,pkgs)) + ')-[0-9.]+',
+                      '\\1-<VERSION>', str)
+    return normalise_version__
+
+def normalise_version( *pkgs ):
+    def normalise_version__( name, opts ):
+        _normalise_fun(name, opts, normalise_version_(*pkgs))
+        _normalise_errmsg_fun(name, opts, normalise_version_(*pkgs))
+    return normalise_version__
+
+def join_normalisers(*a):
+    """
+    Compose functions, flattening sequences.
 
-def normalise_fun( fun ):
-    return lambda name, opts, f=fun: _normalise_fun(name, opts, f)
+       join_normalisers(f1,[f2,f3],f4)
 
-def _normalise_fun( name, opts, f ):
-    opts.extra_normaliser = f
+    is the same as
 
-def normalise_errmsg_fun( fun ):
-    return lambda name, opts, f=fun: _normalise_errmsg_fun(name, opts, f)
+       lambda x: f1(f2(f3(f4(x))))
+    """
 
-def _normalise_errmsg_fun( name, opts, f ):
-    opts.extra_errmsg_normaliser = f
+    def flatten(l):
+        """
+        Taken from http://stackoverflow.com/a/2158532/946226
+        """
+        for el in l:
+            if isinstance(el, collections.Iterable) and not isinstance(el, basestring):
+                for sub in flatten(el):
+                    yield sub
+            else:
+                yield el
+
+    a = flatten(a)
 
-def two_normalisers(f, g):
-    return lambda x, f=f, g=g: f(g(x))
+    fn = lambda x:x # identity function
+    for f in a:
+        assert callable(f)
+        fn = lambda x,f=f,fn=fn: fn(f(x))
+    return fn
 
 # ----
 # Function for composing two opt-fns together
 
 def executeSetups(fs, name, opts):
-    if type(fs) is types.ListType:
+    if type(fs) is list:
         # If we have a list of setups, then execute each one
-        map (lambda f : executeSetups(f, name, opts), fs)
+        for f in fs:
+            executeSetups(f, name, opts)
     else:
         # fs is a single function, so just apply it
         fs(name, opts)
@@ -594,8 +660,7 @@ def test_common_work (name, opts, func, args):
             all_ways = ['normal']
 
         # A test itself can request extra ways by setting opts.extra_ways
-        all_ways = all_ways + filter(lambda way: way not in all_ways,
-                                     opts.extra_ways)
+        all_ways = all_ways + [way for way in opts.extra_ways if way not in all_ways]
 
         t.total_test_cases = t.total_test_cases + len(all_ways)
 
@@ -604,10 +669,11 @@ def test_common_work (name, opts, func, args):
             and (config.only == [] or name in config.only) \
             and (getTestOpts().only_ways == None or way in getTestOpts().only_ways) \
             and (config.cmdline_ways == [] or way in config.cmdline_ways) \
+            and (not (config.skip_perf_tests and isStatsTest())) \
             and way not in getTestOpts().omit_ways
 
         # Which ways we are asked to skip
-        do_ways = filter (ok_way,all_ways)
+        do_ways = list(filter (ok_way,all_ways))
 
         # In fast mode, we skip all but one way
         if config.fast and len(do_ways) > 0:
@@ -625,24 +691,23 @@ def test_common_work (name, opts, func, args):
                     skiptest (name,way)
 
         if getTestOpts().cleanup != '' and (config.clean_only or do_ways != []):
-            clean(map (lambda suff: name + suff,
-                      ['', '.exe', '.exe.manifest', '.genscript',
+            pretest_cleanup(name)
+            clean([name + suff for suff in [
+                       '', '.exe', '.exe.manifest', '.genscript',
                        '.stderr.normalised',        '.stdout.normalised',
-                       '.run.stderr',               '.run.stdout',
                        '.run.stderr.normalised',    '.run.stdout.normalised',
-                       '.comp.stderr',              '.comp.stdout',
                        '.comp.stderr.normalised',   '.comp.stdout.normalised',
-                       '.interp.stderr',            '.interp.stdout',
                        '.interp.stderr.normalised', '.interp.stdout.normalised',
                        '.stats', '.comp.stats',
                        '.hi', '.o', '.prof', '.exe.prof', '.hc',
                        '_stub.h', '_stub.c', '_stub.o',
-                       '.hp', '.exe.hp', '.ps', '.aux', '.hcr', '.eventlog']))
+                       '.hp', '.exe.hp', '.ps', '.aux', '.hcr', '.eventlog']])
 
             if func == multi_compile or func == multi_compile_fail:
                     extra_mods = args[1]
-                    clean(map (lambda (f,x): replace_suffix(f, 'o'), extra_mods))
-                    clean(map (lambda (f,x): replace_suffix(f, 'hi'), extra_mods))
+                    clean([replace_suffix(fx[0],'o') for fx in extra_mods])
+                    clean([replace_suffix(fx[0], 'hi') for fx in extra_mods])
+
 
             clean(getTestOpts().clean_files)
 
@@ -664,7 +729,7 @@ def test_common_work (name, opts, func, args):
                     result = runCmdFor(name, 'cd ' + getTestOpts().testdir + ' && ' + cleanCmd)
                     if result != 0:
                         framework_fail(name, 'cleaning', 'clean-command failed: ' + str(result))
-            except e:
+            except:
                 framework_fail(name, 'cleaning', 'clean-command exception')
 
         package_conf_cache_file_end_timestamp = get_package_cache_timestamp();
@@ -682,11 +747,16 @@ def test_common_work (name, opts, func, args):
                         files_written_not_removed[name] = [f]
         except:
             pass
-    except Exception, e:
+    except Exception as e:
         framework_fail(name, 'runTest', 'Unhandled exception: ' + str(e))
 
 def clean(strs):
     for str in strs:
+        if (str.endswith('.package.conf') or
+            str.startswith('package.conf.') and not str.endswith('/*')):
+            # Package confs are directories now.
+            str += '/*'
+
         for name in glob.glob(in_testdir(str)):
             clean_full_path(name)
 
@@ -694,28 +764,29 @@ def clean_full_path(name):
         try:
             # Remove files...
             os.remove(name)
-        except OSError, e1:
+        except OSError as e1:
             try:
                 # ... and empty directories
                 os.rmdir(name)
-            except OSError, e2:
+            except OSError as e2:
                 # We don't want to fail here, but we do want to know
                 # what went wrong, so print out the exceptions.
                 # ENOENT isn't a problem, though, as we clean files
                 # that don't necessarily exist.
                 if e1.errno != errno.ENOENT:
-                    print e1
+                    print(e1)
                 if e2.errno != errno.ENOENT:
-                    print e2
+                    print(e2)
 
 def do_test(name, way, func, args):
     full_name = name + '(' + way + ')'
 
     try:
-        print '=====>', full_name, t.total_tests, 'of', len(allTestNames), \
-                        str([t.n_unexpected_passes,   \
-                             t.n_unexpected_failures, \
-                             t.n_framework_failures])
+        if_verbose(2, "=====> %s %d of %d %s " % \
+                    (full_name, t.total_tests, len(allTestNames), \
+                    [t.n_unexpected_passes, \
+                     t.n_unexpected_failures, \
+                     t.n_framework_failures]))
 
         if config.use_threads:
             t.lock.release()
@@ -726,11 +797,11 @@ def do_test(name, way, func, args):
                 result = runCmdFor(name, 'cd ' + getTestOpts().testdir + ' && ' + preCmd)
                 if result != 0:
                     framework_fail(name, way, 'pre-command failed: ' + str(result))
-        except e:
+        except:
             framework_fail(name, way, 'pre-command exception')
 
         try:
-            result = apply(func, [name,way] + args)
+            result = func(*[name,way] + args)
         finally:
             if config.use_threads:
                 t.lock.acquire()
@@ -754,16 +825,22 @@ def do_test(name, way, func, args):
                 else:
                     t.expected_passes[name] = [way]
             else:
-                print '*** unexpected pass for', full_name
+                if_verbose(1, '*** unexpected pass for %s' % full_name)
                 t.n_unexpected_passes = t.n_unexpected_passes + 1
                 addPassingTestInfo(t.unexpected_passes, getTestOpts().testdir, name, way)
         elif passFail == 'fail':
             if getTestOpts().expect == 'pass' \
                and way not in getTestOpts().expect_fail_for:
-                print '*** unexpected failure for', full_name
-                t.n_unexpected_failures = t.n_unexpected_failures + 1
                 reason = result['reason']
-                addFailingTestInfo(t.unexpected_failures, getTestOpts().testdir, name, reason, way)
+                tag = result.get('tag')
+                if tag == 'stat':
+                    if_verbose(1, '*** unexpected stat test failure for %s' % full_name)
+                    t.n_unexpected_stat_failures = t.n_unexpected_stat_failures + 1
+                    addFailingTestInfo(t.unexpected_stat_failures, getTestOpts().testdir, name, reason, way)
+                else:
+                    if_verbose(1, '*** unexpected failure for %s' % full_name)
+                    t.n_unexpected_failures = t.n_unexpected_failures + 1
+                    addFailingTestInfo(t.unexpected_failures, getTestOpts().testdir, name, reason, way)
             else:
                 if getTestOpts().expect == 'missing-lib':
                     t.n_missing_libs = t.n_missing_libs + 1
@@ -820,7 +897,7 @@ def skiptest (name, way):
 
 def framework_fail( name, way, reason ):
     full_name = name + '(' + way + ')'
-    print '*** framework failure for', full_name, reason
+    if_verbose(1, '*** framework failure for %s %s ' % (full_name, reason))
     t.n_framework_failures = t.n_framework_failures + 1
     if name in t.framework_failures:
         t.framework_failures[name].append(way)
@@ -838,8 +915,8 @@ def badResult(result):
 def passed():
     return {'passFail': 'pass'}
 
-def failBecause(reason):
-    return {'passFail': 'fail', 'reason': reason}
+def failBecause(reason, tag=None):
+    return {'passFail': 'fail', 'reason': reason, 'tag': tag}
 
 # -----------------------------------------------------------------------------
 # Generic command tests
@@ -858,21 +935,30 @@ def run_command( name, way, cmd ):
 # -----------------------------------------------------------------------------
 # GHCi tests
 
-def ghci_script( name, way, script ):
+def ghci_script_without_flag(flag):
+    def apply(name, way, script):
+        overrides = [f for f in getTestOpts().compiler_always_flags if f != flag]
+        return ghci_script_override_default_flags(overrides)(name, way, script)
+
+    return apply
+
+def ghci_script_override_default_flags(overrides):
+    def apply(name, way, script):
+        return ghci_script(name, way, script, overrides)
+
+    return apply
+
+def ghci_script( name, way, script, override_flags = None ):
     # filter out -fforce-recomp from compiler_always_flags, because we're
     # actually testing the recompilation behaviour in the GHCi tests.
-    flags = filter(lambda f: f != '-fforce-recomp', getTestOpts().compiler_always_flags)
-    flags.append(getTestOpts().extra_hc_opts)
-    if getTestOpts().outputdir != None:
-        flags.extend(["-outputdir", getTestOpts().outputdir])
+    flags = ' '.join(get_compiler_flags(override_flags, noforce=True))
+
+    way_flags = ' '.join(config.way_flags(name)['ghci'])
 
     # We pass HC and HC_OPTS as environment variables, so that the
     # script can invoke the correct compiler by using ':! $HC $HC_OPTS'
-    cmd = "HC='" + config.compiler + "' " + \
-          "HC_OPTS='" + join(flags,' ') + "' " + \
-          "'" + config.compiler + "'" + \
-          ' --interactive -v0 -ignore-dot-ghci ' + \
-          join(flags,' ')
+    cmd = ('HC={{compiler}} HC_OPTS="{flags}" {{compiler}} {flags} {way_flags}'
+          ).format(flags=flags, way_flags=way_flags)
 
     getTestOpts().stdin = script
     return simple_run( name, way, cmd, getTestOpts().extra_run_opts )
@@ -880,6 +966,32 @@ def ghci_script( name, way, script ):
 # -----------------------------------------------------------------------------
 # Compile-only tests
 
+def compile_override_default_flags(overrides):
+    def apply(name, way, extra_opts):
+        return do_compile(name, way, 0, '', [], extra_opts, overrides)
+
+    return apply
+
+def compile_fail_override_default_flags(overrides):
+    def apply(name, way, extra_opts):
+        return do_compile(name, way, 1, '', [], extra_opts, overrides)
+
+    return apply
+
+def compile_without_flag(flag):
+    def apply(name, way, extra_opts):
+        overrides = [f for f in getTestOpts().compiler_always_flags if f != flag]
+        return compile_override_default_flags(overrides)(name, way, extra_opts)
+
+    return apply
+
+def compile_fail_without_flag(flag):
+    def apply(name, way, extra_opts):
+        overrides = [f for f in getTestOpts.compiler_always_flags if f != flag]
+        return compile_fail_override_default_flags(overrides)(name, way, extra_opts)
+
+    return apply
+
 def compile( name, way, extra_hc_opts ):
     return do_compile( name, way, 0, '', [], extra_hc_opts )
 
@@ -898,7 +1010,7 @@ def multi_compile( name, way, top_mod, extra_mods, extra_hc_opts ):
 def multi_compile_fail( name, way, top_mod, extra_mods, extra_hc_opts ):
     return do_compile( name, way, 1, top_mod, extra_mods, extra_hc_opts)
 
-def do_compile( name, way, should_fail, top_mod, extra_mods, extra_hc_opts ):
+def do_compile( name, way, should_fail, top_mod, extra_mods, extra_hc_opts, override_flags = None ):
     # print 'Compile only, extra args = ', extra_hc_opts
     pretest_cleanup(name)
 
@@ -910,7 +1022,7 @@ def do_compile( name, way, should_fail, top_mod, extra_mods, extra_hc_opts ):
     force = 0
     if extra_mods:
        force = 1
-    result = simple_build( name, way, extra_hc_opts, should_fail, top_mod, 0, 1, force)
+    result = simple_build( name, way, extra_hc_opts, should_fail, top_mod, 0, 1, force, override_flags )
 
     if badResult(result):
         return result
@@ -927,8 +1039,10 @@ def do_compile( name, way, should_fail, top_mod, extra_mods, extra_hc_opts ):
     (platform_specific, expected_stderr_file) = platform_wordsize_qualify(namebase, 'stderr')
     actual_stderr_file = qualify(name, 'comp.stderr')
 
-    if not compare_outputs('stderr', \
-                           two_normalisers(two_normalisers(getTestOpts().extra_errmsg_normaliser, normalise_errmsg), normalise_whitespace), \
+    if not compare_outputs(way, 'stderr',
+                           join_normalisers(getTestOpts().extra_errmsg_normaliser,
+                                            normalise_errmsg,
+                                            normalise_whitespace),
                            expected_stderr_file, actual_stderr_file):
         return failBecause('stderr mismatch')
 
@@ -936,7 +1050,7 @@ def do_compile( name, way, should_fail, top_mod, extra_mods, extra_hc_opts ):
     return passed()
 
 def compile_cmp_asm( name, way, extra_hc_opts ):
-    print 'Compile only, extra args = ', extra_hc_opts
+    print('Compile only, extra args = ', extra_hc_opts)
     pretest_cleanup(name)
     result = simple_build( name + '.cmm', way, '-keep-s-files -O ' + extra_hc_opts, 0, '', 0, 0, 0)
 
@@ -955,7 +1069,8 @@ def compile_cmp_asm( name, way, extra_hc_opts ):
     (platform_specific, expected_asm_file) = platform_wordsize_qualify(namebase, 'asm')
     actual_asm_file = qualify(name, 's')
 
-    if not compare_outputs('asm', two_normalisers(normalise_errmsg, normalise_asm), \
+    if not compare_outputs(way, 'asm',
+                           join_normalisers(normalise_errmsg, normalise_asm),
                            expected_asm_file, actual_asm_file):
         return failBecause('asm mismatch')
 
@@ -976,8 +1091,6 @@ def compile_and_run__( name, way, top_mod, extra_mods, extra_hc_opts ):
 
     if way == 'ghci': # interpreted...
         return interpreter_run( name, way, extra_hc_opts, 0, top_mod )
-    elif way == 'extcore' or way == 'optextcore' :
-        return extcore_run( name, way, extra_hc_opts, 0, top_mod )
     else: # compiled...
         force = 0
         if extra_mods:
@@ -1003,49 +1116,60 @@ def multi_compile_and_run( name, way, top_mod, extra_mods, extra_hc_opts ):
 
 def stats( name, way, stats_file ):
     opts = getTestOpts()
-    return checkStats(stats_file, opts.stats_range_fields)
+    return checkStats(name, way, stats_file, opts.stats_range_fields)
 
 # -----------------------------------------------------------------------------
 # Check -t stats info
 
-def checkStats(stats_file, range_fields):
+def checkStats(name, way, stats_file, range_fields):
+    full_name = name + '(' + way + ')'
+
     result = passed()
     if len(range_fields) > 0:
-        f = open(in_testdir(stats_file))
+        try:
+            f = open(in_testdir(stats_file))
+        except IOError as e:
+            return failBecause(str(e))
         contents = f.read()
         f.close()
 
         for (field, (expected, dev)) in range_fields.items():
             m = re.search('\("' + field + '", "([0-9]+)"\)', contents)
             if m == None:
-                print 'Failed to find field: ', field
+                print('Failed to find field: ', field)
                 result = failBecause('no such stats field')
             val = int(m.group(1))
 
-            lowerBound = trunc(           expected * ((100 - float(dev))/100));
-            upperBound = trunc(0.5 + ceil(expected * ((100 + float(dev))/100)));
+            lowerBound = trunc(           expected * ((100 - float(dev))/100))
+            upperBound = trunc(0.5 + ceil(expected * ((100 + float(dev))/100)))
+
+            deviation = round(((float(val) * 100)/ expected) - 100, 1)
 
             if val < lowerBound:
-                print field, 'value is too low:'
-                print '(If this is because you have improved GHC, please'
-                print 'update the test so that GHC doesn\'t regress again)'
-                result = failBecause('stat too good')
+                print(field, 'value is too low:')
+                print('(If this is because you have improved GHC, please')
+                print('update the test so that GHC doesn\'t regress again)')
+                result = failBecause('stat too good', tag='stat')
             if val > upperBound:
-                print field, 'value is too high:'
-                result = failBecause('stat not good enough')
+                print(field, 'value is too high:')
+                result = failBecause('stat not good enough', tag='stat')
 
-            if val < lowerBound or val > upperBound:
+            if val < lowerBound or val > upperBound or config.verbose >= 4:
                 valStr = str(val)
                 valLen = len(valStr)
                 expectedStr = str(expected)
                 expectedLen = len(expectedStr)
-                length = max(map (lambda x : len(str(x)), [expected, lowerBound, upperBound, val]))
+                length = max(len(str(x)) for x in [expected, lowerBound, upperBound, val])
+
                 def display(descr, val, extra):
-                    print descr, string.rjust(str(val), length), extra
-                display('    Expected    ' + field + ':', expected, '+/-' + str(dev) + '%')
-                display('    Lower bound ' + field + ':', lowerBound, '')
-                display('    Upper bound ' + field + ':', upperBound, '')
-                display('    Actual      ' + field + ':', val, '')
+                    print(descr, str(val).rjust(length), extra)
+
+                display('    Expected    ' + full_name + ' ' + field + ':', expected, '+/-' + str(dev) + '%')
+                display('    Lower bound ' + full_name + ' ' + field + ':', lowerBound, '')
+                display('    Upper bound ' + full_name + ' ' + field + ':', upperBound, '')
+                display('    Actual      ' + full_name + ' ' + field + ':', val, '')
+                if val != expected:
+                    display('    Deviation   ' + full_name + ' ' + field + ':', deviation, '%')
                 
     return result
 
@@ -1064,7 +1188,7 @@ def extras_build( way, extra_mods, extra_hc_opts ):
     return {'passFail' : 'pass', 'hc_opts' : extra_hc_opts}
 
 
-def simple_build( name, way, extra_hc_opts, should_fail, top_mod, link, addsuf, noforce ):
+def simple_build( name, way, extra_hc_opts, should_fail, top_mod, link, addsuf, noforce, override_flags = None ):
     opts = getTestOpts()
     errname = add_suffix(name, 'comp.stderr')
     rm_no_fail( qualify(errname, '') )
@@ -1112,31 +1236,24 @@ def simple_build( name, way, extra_hc_opts, should_fail, top_mod, link, addsuf,
     else:
         cmd_prefix = getTestOpts().compile_cmd_prefix + ' '
 
-    comp_flags = copy.copy(getTestOpts().compiler_always_flags)
-    if noforce:
-        comp_flags = filter(lambda f: f != '-fforce-recomp', comp_flags)
-    if getTestOpts().outputdir != None:
-        comp_flags.extend(["-outputdir", getTestOpts().outputdir])
-
-    cmd = 'cd ' + getTestOpts().testdir + " && " + cmd_prefix + "'" \
-          + config.compiler + "' " \
-          + join(comp_flags,' ') + ' ' \
-          + to_do + ' ' + srcname + ' ' \
-          + join(config.way_flags(name)[way],' ') + ' ' \
-          + extra_hc_opts + ' ' \
-          + opts.extra_hc_opts + ' ' \
-          + '>' + errname + ' 2>&1'
+    flags = ' '.join(get_compiler_flags(override_flags, noforce) +
+                     config.way_flags(name)[way])
+
+    cmd = ('cd {opts.testdir} && {cmd_prefix} '
+           '{{compiler}} {to_do} {srcname} {flags} {extra_hc_opts} '
+           '> {errname} 2>&1'
+          ).format(**locals())
 
     result = runCmdFor(name, cmd)
 
     if result != 0 and not should_fail:
         actual_stderr = qualify(name, 'comp.stderr')
-        if_verbose(1,'Compile failed (status ' + `result` + ') errors were:')
+        if_verbose(1,'Compile failed (status ' + repr(result) + ') errors were:')
         if_verbose_dump(1,actual_stderr)
 
     # ToDo: if the sub-shell was killed by ^C, then exit
 
-    statsResult = checkStats(stats_file, opts.compiler_stats_range_fields)
+    statsResult = checkStats(name, way, stats_file, opts.compiler_stats_range_fields)
 
     if badResult(statsResult):
         return statsResult
@@ -1191,11 +1308,11 @@ def simple_run( name, way, prog, args ):
         stdin_comes_from = ' <' + use_stdin
 
     if opts.combined_output:
-        redirection = ' >' + run_stdout \
-                    + ' 2>&1'
+        redirection        = ' > {0} 2>&1'.format(run_stdout)
+        redirection_append = ' >> {0} 2>&1'.format(run_stdout)
     else:
-        redirection = ' >' + run_stdout \
-                    + ' 2>' + run_stderr
+        redirection        = ' > {0} 2> {1}'.format(run_stdout, run_stderr)
+        redirection_append = ' >> {0} 2>> {1}'.format(run_stdout, run_stderr)
 
     cmd = prog + ' ' + args + ' '  \
         + my_rts_flags + ' '       \
@@ -1203,7 +1320,7 @@ def simple_run( name, way, prog, args ):
         + redirection
 
     if opts.cmd_wrapper != None:
-        cmd = opts.cmd_wrapper(cmd);
+        cmd = opts.cmd_wrapper(cmd) + redirection_append
 
     cmd = 'cd ' + opts.testdir + ' && ' + cmd
 
@@ -1215,7 +1332,7 @@ def simple_run( name, way, prog, args ):
 
     # check the exit code
     if exit_code != opts.exit_code:
-        print 'Wrong exit code (expected', opts.exit_code, ', actual', exit_code, ')'
+        print('Wrong exit code (expected', opts.exit_code, ', actual', exit_code, ')')
         dump_stdout(name)
         dump_stderr(name)
         return failBecause('bad exit code')
@@ -1224,8 +1341,8 @@ def simple_run( name, way, prog, args ):
     check_prof = my_rts_flags.find("-p") != -1
 
     if not opts.ignore_output:
-        bad_stderr = not opts.combined_output and not check_stderr_ok(name)
-        bad_stdout = not check_stdout_ok(name)
+        bad_stderr = not opts.combined_output and not check_stderr_ok(name, way)
+        bad_stdout = not check_stdout_ok(name, way)
         if bad_stderr:
             return failBecause('bad stderr')
         if bad_stdout:
@@ -1233,10 +1350,10 @@ def simple_run( name, way, prog, args ):
         # exit_code > 127 probably indicates a crash, so don't try to run hp2ps.
         if check_hp and (exit_code <= 127 or exit_code == 251) and not check_hp_ok(name):
             return failBecause('bad heap profile')
-        if check_prof and not check_prof_ok(name):
+        if check_prof and not check_prof_ok(name, way):
             return failBecause('bad profile')
 
-    return checkStats(stats_file, opts.stats_range_fields)
+    return checkStats(name, way, stats_file, opts.stats_range_fields)
 
 def rts_flags(way):
     if (way == ''):
@@ -1247,7 +1364,7 @@ def rts_flags(way):
     if args == []:
         return ''
     else:
-        return '+RTS ' + join(args,' ') + ' -RTS'
+        return '+RTS ' + ' '.join(args) + ' -RTS'
 
 # -----------------------------------------------------------------------------
 # Run a program in the interpreter and check its output
@@ -1299,20 +1416,22 @@ def interpreter_run( name, way, extra_hc_opts, compile_only, top_mod ):
 
     script.close()
 
-    flags = copy.copy(getTestOpts().compiler_always_flags)
-    if getTestOpts().outputdir != None:
-        flags.extend(["-outputdir", getTestOpts().outputdir])
+    flags = ' '.join(get_compiler_flags(override_flags=None, noforce=False) +
+                     config.way_flags(name)[way])
+
+    if getTestOpts().combined_output:
+        redirection        = ' > {0} 2>&1'.format(outname)
+        redirection_append = ' >> {0} 2>&1'.format(outname)
+    else:
+        redirection        = ' > {0} 2> {1}'.format(outname, errname)
+        redirection_append = ' >> {0} 2>> {1}'.format(outname, errname)
 
-    cmd = "'" + config.compiler + "' " \
-          + join(flags,' ') + ' ' \
-          + srcname + ' ' \
-          + join(config.way_flags(name)[way],' ') + ' ' \
-          + extra_hc_opts + ' ' \
-          + getTestOpts().extra_hc_opts + ' ' \
-          + '<' + scriptname +  ' 1>' + outname + ' 2>' + errname
+    cmd = ('{{compiler}} {srcname} {flags} {extra_hc_opts} '
+           '< {scriptname} {redirection}'
+          ).format(**locals())
 
     if getTestOpts().cmd_wrapper != None:
-        cmd = getTestOpts().cmd_wrapper(cmd);
+        cmd = getTestOpts().cmd_wrapper(cmd) + redirection_append;
 
     cmd = 'cd ' + getTestOpts().testdir + " && " + cmd
 
@@ -1331,15 +1450,15 @@ def interpreter_run( name, way, extra_hc_opts, compile_only, top_mod ):
 
     # check the exit code
     if exit_code != getTestOpts().exit_code:
-        print 'Wrong exit code (expected', getTestOpts().exit_code, ', actual', exit_code, ')'
+        print('Wrong exit code (expected', getTestOpts().exit_code, ', actual', exit_code, ')')
         dump_stdout(name)
         dump_stderr(name)
         return failBecause('bad exit code')
 
     # ToDo: if the sub-shell was killed by ^C, then exit
 
-    if getTestOpts().ignore_output or (check_stderr_ok(name) and
-                                       check_stdout_ok(name)):
+    if getTestOpts().ignore_output or (check_stderr_ok(name, way) and
+                                       check_stdout_ok(name, way)):
         return passed()
     else:
         return failBecause('bad stdout or stderr')
@@ -1365,102 +1484,26 @@ def split_file(in_fn, delimiter, out1_fn, out2_fn):
     out2.close()
 
 # -----------------------------------------------------------------------------
-# Generate External Core for the given program, then compile the resulting Core
-# and compare its output to the expected output
-
-def extcore_run( name, way, extra_hc_opts, compile_only, top_mod ):
-
-    depsfilename = qualify(name, 'deps')
-    errname = add_suffix(name, 'comp.stderr')
-    qerrname = qualify(errname,'')
-
-    hcname = qualify(name, 'hc')
-    oname = qualify(name, 'o')
-
-    rm_no_fail( qerrname )
-    rm_no_fail( qualify(name, '') )
-
-    if (top_mod == ''):
-        srcname = add_hs_lhs_suffix(name)
-    else:
-        srcname = top_mod
-
-    qcorefilename = qualify(name, 'hcr')
-    corefilename = add_suffix(name, 'hcr')
-    rm_no_fail(qcorefilename)
-
-    # Generate External Core
+# Utils
+def get_compiler_flags(override_flags, noforce):
+    opts = getTestOpts()
 
-    if (top_mod == ''):
-        to_do = ' ' + srcname + ' '
+    if override_flags is not None:
+        flags = copy.copy(override_flags)
     else:
-        to_do = ' --make ' + top_mod + ' '
-
-    flags = copy.copy(getTestOpts().compiler_always_flags)
-    if getTestOpts().outputdir != None:
-        flags.extend(["-outputdir", getTestOpts().outputdir])
-    cmd = 'cd ' + getTestOpts().testdir + " && '" \
-          + config.compiler + "' " \
-          + join(flags,' ') + ' ' \
-          + join(config.way_flags(name)[way],' ') + ' ' \
-          + extra_hc_opts + ' ' \
-          + getTestOpts().extra_hc_opts \
-          + to_do \
-          + '>' + errname + ' 2>&1'
-    result = runCmdFor(name, cmd)
-
-    exit_code = result >> 8
+        flags = copy.copy(opts.compiler_always_flags)
 
-    if exit_code != 0:
-         if_verbose(1,'Compiling to External Core failed (status ' + `result` + ') errors were:')
-         if_verbose_dump(1,qerrname)
-         return failBecause('ext core exit code non-0')
-
-     # Compile the resulting files -- if there's more than one module, we need to read the output
-     # of the previous compilation in order to find the dependencies
-    if (top_mod == ''):
-        to_compile = corefilename
-    else:
-        result = runCmdFor(name, 'grep Compiling ' + qerrname + ' |  awk \'{print $4}\' > ' + depsfilename)
-        deps = open(depsfilename).read()
-        deplist = string.replace(deps, '\n',' ');
-        deplist2 = string.replace(deplist,'.lhs,', '.hcr');
-        to_compile = string.replace(deplist2,'.hs,', '.hcr');
-
-    flags = join(filter(lambda f: f != '-fext-core',config.way_flags(name)[way]),' ')
-    if getTestOpts().outputdir != None:
-        flags.extend(["-outputdir", getTestOpts().outputdir])
-
-    cmd = 'cd ' + getTestOpts().testdir + " && '" \
-          + config.compiler + "' " \
-          + join(getTestOpts().compiler_always_flags,' ') + ' ' \
-          + to_compile + ' ' \
-          + extra_hc_opts + ' ' \
-          + getTestOpts().extra_hc_opts + ' ' \
-          + flags                   \
-          + ' -fglasgow-exts -o ' + name \
-          + '>' + errname + ' 2>&1'
-
-    result = runCmdFor(name, cmd)
-    exit_code = result >> 8
-
-    if exit_code != 0:
-        if_verbose(1,'Compiling External Core file(s) failed (status ' + `result` + ') errors were:')
-        if_verbose_dump(1,qerrname)
-        return failBecause('ext core exit code non-0')
+    if noforce:
+        flags = [f for f in flags if f != '-fforce-recomp']
 
-    # Clean up
-    rm_no_fail ( oname )
-    rm_no_fail ( hcname )
-    rm_no_fail ( qcorefilename )
-    rm_no_fail ( depsfilename )
+    flags.append(opts.extra_hc_opts)
 
-    return simple_run ( name, way, './'+name, getTestOpts().extra_run_opts )
+    if opts.outputdir != None:
+        flags.extend(["-outputdir", opts.outputdir])
 
-# -----------------------------------------------------------------------------
-# Utils
+    return flags
 
-def check_stdout_ok( name ):
+def check_stdout_ok(name, way):
    if getTestOpts().with_namebase == None:
        namebase = name
    else:
@@ -1475,15 +1518,20 @@ def check_stdout_ok( name ):
       else:
          return normalise_output(str)
 
-   return compare_outputs('stdout', \
-                          two_normalisers(norm, getTestOpts().extra_normaliser), \
+   extra_norm = join_normalisers(norm, getTestOpts().extra_normaliser)
+
+   check_stdout = getTestOpts().check_stdout
+   if check_stdout:
+      return check_stdout(actual_stdout_file, extra_norm)
+
+   return compare_outputs(way, 'stdout', extra_norm,
                           expected_stdout_file, actual_stdout_file)
 
 def dump_stdout( name ):
-   print 'Stdout:'
-   print read_no_crs(qualify(name, 'run.stdout'))
+   print('Stdout:')
+   print(read_no_crs(qualify(name, 'run.stdout')))
 
-def check_stderr_ok( name ):
+def check_stderr_ok(name, way):
    if getTestOpts().with_namebase == None:
        namebase = name
    else:
@@ -1498,13 +1546,13 @@ def check_stderr_ok( name ):
       else:
          return normalise_errmsg(str)
 
-   return compare_outputs('stderr', \
-                          two_normalisers(norm, getTestOpts().extra_errmsg_normaliser), \
+   return compare_outputs(way, 'stderr',
+                          join_normalisers(norm, getTestOpts().extra_errmsg_normaliser), \
                           expected_stderr_file, actual_stderr_file)
 
 def dump_stderr( name ):
-   print "Stderr:"
-   print read_no_crs(qualify(name, 'run.stderr'))
+   print("Stderr:")
+   print(read_no_crs(qualify(name, 'run.stderr')))
 
 def read_no_crs(file):
     str = ''
@@ -1526,7 +1574,7 @@ def write_file(file, str):
 def check_hp_ok(name):
 
     # do not qualify for hp2ps because we should be in the right directory
-    hp2psCmd = "cd " + getTestOpts().testdir + " && '" + config.hp2ps + "' " + name
+    hp2psCmd = "cd " + getTestOpts().testdir + " && {hp2ps} " + name
 
     hp2psResult = runCmdExitCode(hp2psCmd)
 
@@ -1539,25 +1587,25 @@ def check_hp_ok(name):
                 if (gsResult == 0):
                     return (True)
                 else:
-                    print "hp2ps output for " + name + "is not valid PostScript"
+                    print("hp2ps output for " + name + "is not valid PostScript")
             else: return (True) # assume postscript is valid without ghostscript
         else:
-            print "hp2ps did not generate PostScript for " + name
+            print("hp2ps did not generate PostScript for " + name)
             return (False)
     else:
-        print "hp2ps error when processing heap profile for " + name
+        print("hp2ps error when processing heap profile for " + name)
         return(False)
 
-def check_prof_ok(name):
+def check_prof_ok(name, way):
 
     prof_file = qualify(name,'prof')
 
     if not os.path.exists(prof_file):
-        print prof_file + " does not exist"
+        print(prof_file + " does not exist")
         return(False)
 
     if os.path.getsize(qualify(name,'prof')) == 0:
-        print prof_file + " is empty"
+        print(prof_file + " is empty")
         return(False)
 
     if getTestOpts().with_namebase == None:
@@ -1572,14 +1620,14 @@ def check_prof_ok(name):
     if not os.path.exists(expected_prof_file):
         return True
     else:
-        return compare_outputs('prof', \
-                               two_normalisers(normalise_whitespace,normalise_prof), \
+        return compare_outputs(way, 'prof',
+                               join_normalisers(normalise_whitespace,normalise_prof), \
                                expected_prof_file, prof_file)
 
 # Compare expected output to actual output, and optionally accept the
 # new output. Returns true if output matched or was accepted, false
 # otherwise.
-def compare_outputs( kind, normaliser, expected_file, actual_file ):
+def compare_outputs(way, kind, normaliser, expected_file, actual_file):
     if os.path.exists(expected_file):
         expected_raw = read_no_crs(expected_file)
         # print "norm:", normaliser(expected_raw)
@@ -1595,7 +1643,7 @@ def compare_outputs( kind, normaliser, expected_file, actual_file ):
     if expected_str == actual_str:
         return 1
     else:
-        print 'Actual ' + kind + ' output differs from expected:'
+        if_verbose(1, 'Actual ' + kind + ' output differs from expected:')
 
         if expected_file_for_diff == '/dev/null':
             expected_normalised_file = '/dev/null'
@@ -1614,17 +1662,22 @@ def compare_outputs( kind, normaliser, expected_file, actual_file ):
         # (including newlines) so the diff would be hard to read.
         # This does mean that the diff might contain changes that
         # would be normalised away.
-        r = os.system( 'diff -uw ' + expected_file_for_diff + \
-                               ' ' + actual_file )
-
-        # If for some reason there were no non-whitespace differences,
-        # then do a full diff
-        if r == 0:
-            r = os.system( 'diff -u ' + expected_file_for_diff + \
-                                  ' ' + actual_file )
-
-        if config.accept:
-            print 'Accepting new output.'
+        if (config.verbose >= 1):
+            r = os.system( 'diff -uw ' + expected_file_for_diff + \
+                                   ' ' + actual_file )
+
+            # If for some reason there were no non-whitespace differences,
+            # then do a full diff
+            if r == 0:
+                r = os.system( 'diff -u ' + expected_file_for_diff + \
+                                      ' ' + actual_file )
+
+        if config.accept and (getTestOpts().expect == 'fail' or
+                              way in getTestOpts().expect_fail_for):
+            if_verbose(1, 'Test is expected to fail. Not accepting new output.')
+            return 0
+        elif config.accept:
+            if_verbose(1, 'Accepting new output.')
             write_file(expected_file, actual_raw)
             return 1
         else:
@@ -1637,6 +1690,11 @@ def normalise_whitespace( str ):
     return str
 
 def normalise_errmsg( str ):
+    # remove " error:" and lower-case " Warning:" to make patch for
+    # trac issue #10021 smaller
+    str = modify_lines(str, lambda l: re.sub(' error:', '', l))
+    str = modify_lines(str, lambda l: re.sub(' Warning:', ' warning:', l))
+
     # If somefile ends in ".exe" or ".exe:", zap ".exe" (for Windows)
     #    the colon is there because it appears in error messages; this
     #    hacky solution is used in place of more sophisticated filename
@@ -1647,10 +1705,8 @@ def normalise_errmsg( str ):
     # The inplace ghc's are called ghc-stage[123] to avoid filename
     # collisions, so we need to normalise that to just "ghc"
     str = re.sub('ghc-stage[123]', 'ghc', str)
-    # We sometimes see the name of the integer-gmp package on stderr,
-    # but this can change (either the implementation name or the
-    # version number), so we canonicalise it here
-    str = re.sub('integer-[a-z]+', 'integer-impl', str)
+    # Error messages simetimes contain integer implementation package
+    str = re.sub('integer-(gmp|simple)-[0-9.]+', 'integer-<IMPL>-<VERSION>', str)
     return str
 
 # normalise a .prof file, so that we can reasonably compare it against
@@ -1694,6 +1750,10 @@ def normalise_exe_( str ):
     return str
 
 def normalise_output( str ):
+    # remove " error:" and lower-case " Warning:" to make patch for
+    # trac issue #10021 smaller
+    str = modify_lines(str, lambda l: re.sub(' error:', '', l))
+    str = modify_lines(str, lambda l: re.sub(' Warning:', ' warning:', l))
     # Remove a .exe extension (for Windows)
     # This can occur in error messages generated by the program.
     str = re.sub('([^\\s])\\.exe', '\\1', str)
@@ -1720,16 +1780,16 @@ def normalise_asm( str ):
     out = '\n'.join(out)
     return out
 
-def if_verbose( n, str ):
+def if_verbose( n, s ):
     if config.verbose >= n:
-        print str
+        print(s)
 
 def if_verbose_dump( n, f ):
     if config.verbose >= n:
         try:
-            print open(f).read()
+            print(open(f).read())
         except:
-            print ''
+            print('')
 
 def rawSystem(cmd_and_args):
     # We prefer subprocess.call to os.spawnv as the latter
@@ -1737,17 +1797,28 @@ def rawSystem(cmd_and_args):
     # with the Windows (non-cygwin) python. An argument "a b c"
     # turns into three arguments ["a", "b", "c"].
 
-    # However, subprocess is new in python 2.4, so fall back to
-    # using spawnv if we don't have it
-
-    if have_subprocess:
-        return subprocess.call(cmd_and_args)
-    else:
-        return os.spawnv(os.P_WAIT, cmd_and_args[0], cmd_and_args)
+    cmd = cmd_and_args[0]
+    return subprocess.call([strip_quotes(cmd)] + cmd_and_args[1:])
+
+# When running under native msys Python, any invocations of non-msys binaries,
+# including timeout.exe, will have their arguments munged according to some
+# heuristics, which leads to malformed command lines (#9626).  The easiest way
+# to avoid problems is to invoke through /usr/bin/cmd which sidesteps argument
+# munging because it is a native msys application.
+def passThroughCmd(cmd_and_args):
+    args = []
+    # cmd needs a Windows-style path for its first argument.
+    args.append(cmd_and_args[0].replace('/', '\\'))
+    # Other arguments need to be quoted to deal with spaces.
+    args.extend(['"%s"' % arg for arg in cmd_and_args[1:]])
+    return ["cmd", "/c", " ".join(args)]
 
 # Note that this doesn't handle the timeout itself; it is just used for
 # commands that have timeout handling built-in.
 def rawSystemWithTimeout(cmd_and_args):
+    if config.os == 'mingw32' and sys.executable.startswith('/usr'):
+        # This is only needed when running under msys python.
+        cmd_and_args = passThroughCmd(cmd_and_args)
     r = rawSystem(cmd_and_args)
     if r == 98:
         # The python timeout program uses 98 to signal that ^C was pressed
@@ -1766,7 +1837,10 @@ def rawSystemWithTimeout(cmd_and_args):
 # Then, when using the native Python, os.system will invoke the cmd shell
 
 def runCmd( cmd ):
-    if_verbose( 1, cmd )
+    # Format cmd using config. Example: cmd='{hpc} report A.tix'
+    cmd = cmd.format(**config.__dict__)
+
+    if_verbose( 3, cmd )
     r = 0
     if config.os == 'mingw32':
         # On MinGW, we will always have timeout
@@ -1779,7 +1853,10 @@ def runCmd( cmd ):
     return r << 8
 
 def runCmdFor( name, cmd, timeout_multiplier=1.0 ):
-    if_verbose( 1, cmd )
+    # Format cmd using config. Example: cmd='{hpc} report A.tix'
+    cmd = cmd.format(**config.__dict__)
+
+    if_verbose( 3, cmd )
     r = 0
     if config.os == 'mingw32':
         # On MinGW, we will always have timeout
@@ -1957,7 +2034,7 @@ def checkForFilesWrittenProblems(file):
     if len(files_written_not_removed) > 0:
         file.write("\n")
         file.write("\nSome files written but not removed:\n")
-        tests = files_written_not_removed.keys()
+        tests = list(files_written_not_removed.keys())
         tests.sort()
         for t in tests:
             for f in files_written_not_removed[t]:
@@ -1969,7 +2046,7 @@ def checkForFilesWrittenProblems(file):
     if len(bad_file_usages) > 0:
         file.write("\n")
         file.write("\nSome bad file usages:\n")
-        tests = bad_file_usages.keys()
+        tests = list(bad_file_usages.keys())
         tests.sort()
         for t in tests:
             for f in bad_file_usages[t]:
@@ -1984,7 +2061,7 @@ def genGSCmd(psfile):
 
 def gsNotWorking():
     global gs_working
-    print "GhostScript not available for hp2ps tests"
+    print("GhostScript not available for hp2ps tests")
 
 global gs_working
 gs_working = 0
@@ -1992,9 +2069,10 @@ if config.have_profiling:
   if config.gs != '':
     resultGood = runCmdExitCode(genGSCmd(config.confdir + '/good.ps'));
     if resultGood == 0:
-        resultBad = runCmdExitCode(genGSCmd(config.confdir + '/bad.ps'));
+        resultBad = runCmdExitCode(genGSCmd(config.confdir + '/bad.ps') +
+                                   ' >/dev/null 2>&1')
         if resultBad != 0:
-            print "GhostScript available for hp2ps tests"
+            print("GhostScript available for hp2ps tests")
             gs_working = 1;
         else:
             gsNotWorking();
@@ -2061,7 +2139,7 @@ def platform_wordsize_qualify( name, suff ):
              for vers in ['-' + config.compiler_maj_version, '']]
 
     dir = glob.glob(basepath + '*')
-    dir = map (lambda d: normalise_slashes_(d), dir)
+    dir = [normalise_slashes_(d) for d in dir]
 
     for (platformSpecific, f) in paths:
        if f in dir:
@@ -2080,24 +2158,31 @@ def pretest_cleanup(name):
            pass
        os.mkdir(odir)
 
+   rm_no_fail(qualify(name,'interp.stderr'))
+   rm_no_fail(qualify(name,'interp.stdout'))
    rm_no_fail(qualify(name,'comp.stderr'))
+   rm_no_fail(qualify(name,'comp.stdout'))
    rm_no_fail(qualify(name,'run.stderr'))
    rm_no_fail(qualify(name,'run.stdout'))
-   rm_no_fail(qualify(name,'tix'))  # remove the old tix file
+   rm_no_fail(qualify(name,'tix'))
+   rm_no_fail(qualify(name,'exe.tix'))
    # simple_build zaps the following:
    # rm_nofail(qualify("o"))
    # rm_nofail(qualify(""))
    # not interested in the return code
 
 # -----------------------------------------------------------------------------
-# Return a list of all the files ending in '.T' below the directory dir.
+# Return a list of all the files ending in '.T' below directories roots.
 
 def findTFiles(roots):
-    return concat(map(findTFiles_,roots))
+    # It would be better to use os.walk, but that
+    # gives backslashes on Windows, which trip the
+    # testsuite later :-(
+    return [filename for root in roots for filename in findTFiles_(root)]
 
 def findTFiles_(path):
     if os.path.isdir(path):
-        paths = map(lambda x, p=path: p + '/' + x, os.listdir(path))
+        paths = [path + '/' + x for x in os.listdir(path)]
         return findTFiles(paths)
     elif path[-2:] == '.T':
         return [path]
@@ -2107,32 +2192,42 @@ def findTFiles_(path):
 # -----------------------------------------------------------------------------
 # Output a test summary to the specified file object
 
-def summary(t, file):
+def summary(t, file, short=False):
 
     file.write('\n')
-    printUnexpectedTests(file, [t.unexpected_passes, t.unexpected_failures])
+    printUnexpectedTests(file, [t.unexpected_passes, t.unexpected_failures, t.unexpected_stat_failures])
+
+    if short:
+        # Only print the list of unexpected tests above.
+        return
+
     file.write('OVERALL SUMMARY for test run started at '
-               + t.start_time + '\n'
-               + string.rjust(`t.total_tests`, 8)
+               + time.strftime("%c %Z", t.start_time) + '\n'
+               + str(datetime.timedelta(seconds=
+                    round(time.time() - time.mktime(t.start_time)))).rjust(8)
+               + ' spent to go through\n'
+               + repr(t.total_tests).rjust(8)
                + ' total tests, which gave rise to\n'
-               + string.rjust(`t.total_test_cases`, 8)
+               + repr(t.total_test_cases).rjust(8)
                + ' test cases, of which\n'
-               + string.rjust(`t.n_tests_skipped`, 8)
+               + repr(t.n_tests_skipped).rjust(8)
                + ' were skipped\n'
                + '\n'
-               + string.rjust(`t.n_missing_libs`, 8)
+               + repr(t.n_missing_libs).rjust(8)
                + ' had missing libraries\n'
-               + string.rjust(`t.n_expected_passes`, 8)
+               + repr(t.n_expected_passes).rjust(8)
                + ' expected passes\n'
-               + string.rjust(`t.n_expected_failures`, 8)
+               + repr(t.n_expected_failures).rjust(8)
                + ' expected failures\n'
                + '\n'
-               + string.rjust(`t.n_framework_failures`, 8)
+               + repr(t.n_framework_failures).rjust(8)
                + ' caused framework failures\n'
-               + string.rjust(`t.n_unexpected_passes`, 8)
+               + repr(t.n_unexpected_passes).rjust(8)
                + ' unexpected passes\n'
-               + string.rjust(`t.n_unexpected_failures`, 8)
+               + repr(t.n_unexpected_failures).rjust(8)
                + ' unexpected failures\n'
+               + repr(t.n_unexpected_stat_failures).rjust(8)
+               + ' unexpected stat failures\n'
                + '\n')
 
     if t.n_unexpected_passes > 0:
@@ -2143,6 +2238,10 @@ def summary(t, file):
         file.write('Unexpected failures:\n')
         printFailingTestInfosSummary(file, t.unexpected_failures)
 
+    if t.n_unexpected_stat_failures > 0:
+        file.write('Unexpected stat failures:\n')
+        printFailingTestInfosSummary(file, t.unexpected_stat_failures)
+
     if config.check_files_written:
         checkForFilesWrittenProblems(file)
 
@@ -2154,7 +2253,7 @@ def printUnexpectedTests(file, testInfoss):
     for testInfos in testInfoss:
         directories = testInfos.keys()
         for directory in directories:
-            tests = testInfos[directory].keys()
+            tests = list(testInfos[directory].keys())
             unexpected += tests
     if unexpected != []:
         file.write('Unexpected results from:\n')
@@ -2162,43 +2261,31 @@ def printUnexpectedTests(file, testInfoss):
         file.write('\n')
 
 def printPassingTestInfosSummary(file, testInfos):
-    directories = testInfos.keys()
+    directories = list(testInfos.keys())
     directories.sort()
-    maxDirLen = max(map ((lambda x : len(x)), directories))
+    maxDirLen = max(len(x) for x in directories)
     for directory in directories:
-        tests = testInfos[directory].keys()
+        tests = list(testInfos[directory].keys())
         tests.sort()
         for test in tests:
            file.write('   ' + directory.ljust(maxDirLen + 2) + test + \
-                      ' (' + join(testInfos[directory][test],',') + ')\n')
+                      ' (' + ','.join(testInfos[directory][test]) + ')\n')
     file.write('\n')
 
 def printFailingTestInfosSummary(file, testInfos):
-    directories = testInfos.keys()
+    directories = list(testInfos.keys())
     directories.sort()
-    maxDirLen = max(map ((lambda x : len(x)), directories))
+    maxDirLen = max(len(d) for d in directories)
     for directory in directories:
-        tests = testInfos[directory].keys()
+        tests = list(testInfos[directory].keys())
         tests.sort()
         for test in tests:
            reasons = testInfos[directory][test].keys()
            for reason in reasons:
                file.write('   ' + directory.ljust(maxDirLen + 2) + test + \
                           ' [' + reason + ']' + \
-                          ' (' + join(testInfos[directory][test][reason],',') + ')\n')
+                          ' (' + ','.join(testInfos[directory][test][reason]) + ')\n')
     file.write('\n')
 
-def getStdout(cmd):
-    if have_subprocess:
-        p = subprocess.Popen(cmd,
-                             stdout=subprocess.PIPE,
-                             stderr=subprocess.PIPE)
-        (stdout, stderr) = p.communicate()
-        r = p.wait()
-        if r != 0:
-            raise Exception("Command failed: " + str(cmd))
-        if stderr != '':
-            raise Exception("stderr from command: " + str(cmd))
-        return stdout
-    else:
-        raise Exception("Need subprocess to get stdout, but don't have it")
+def modify_lines(s, f):
+    return '\n'.join([f(l) for l in s.splitlines()])