tests: Add a helper to run external scripts

Some tests are really just a wrapper around a test script in
configs/. Add a helper method to wrap these scripts to make sure they
are executed in a consistent environment. This wrapper sets up a
global environment that is identical to that created by main() when it
executes the script. Unlike the old wrappers, it updates the module
search path to make relative imports work correctly in Python 3.

Change-Id: Ie9f81ec4e2689aa8cf5ecb9fc8025d3534b5c9ca
Signed-off-by: Andreas Sandberg <andreas.sandberg@arm.com>
Reviewed-on: https://gem5-review.googlesource.com/c/15976
Reviewed-by: Jason Lowe-Power <jason@lowepower.com>
Maintainer: Jason Lowe-Power <jason@lowepower.com>
diff --git a/tests/configs/dram-lowp.py b/tests/configs/dram-lowp.py
index 20c774e..418514d 100644
--- a/tests/configs/dram-lowp.py
+++ b/tests/configs/dram-lowp.py
@@ -50,19 +50,13 @@
 def run_test(root):
         # Called from tests/run.py
 
-        # Set the working directory in case we are executing from
-        # outside gem5's source tree
-        import os
-        os.chdir(os.path.join(os.path.dirname(__file__), "../"))
-
-        # The path to this script is the only parameter. Delete it so
-        # we can execute the script that we want to execute.
         import sys
-        del sys.argv[1:]
-
-        # Add a specific page policy and specify the number of ranks
-        sys.argv.append('-p%s' % page_policy)
-        sys.argv.append('-r 2')
+        argv = [
+                sys.argv[0],
+                # Add a specific page policy and specify the number of ranks
+                '-p%s' % page_policy,
+                '-r 2',
+        ]
 
         # Execute the script we are wrapping
-        execfile(srcpath('configs/dram/low_power_sweep.py'), globals())
+        run_config('configs/dram/low_power_sweep.py', argv=argv)
diff --git a/tests/configs/learning-gem5-p1-simple.py b/tests/configs/learning-gem5-p1-simple.py
index 9ad7085..aba538a 100644
--- a/tests/configs/learning-gem5-p1-simple.py
+++ b/tests/configs/learning-gem5-p1-simple.py
@@ -33,12 +33,4 @@
 root = None
 
 def run_test(root):
-        # Called from tests/run.py
-
-        # Set the working directory in case we are executing from
-        # outside gem5's source tree
-        import os
-        os.chdir(os.path.join(os.path.dirname(__file__), "../"))
-
-        # Execute the script we are wrapping
-        execfile(srcpath('configs/learning_gem5/part1/simple.py'))
+        run_config('configs/learning_gem5/part1/simple.py')
diff --git a/tests/configs/learning-gem5-p1-two-level.py b/tests/configs/learning-gem5-p1-two-level.py
index c0a1e46..0c355e8 100644
--- a/tests/configs/learning-gem5-p1-two-level.py
+++ b/tests/configs/learning-gem5-p1-two-level.py
@@ -32,27 +32,5 @@
 # For some reason, this is implicitly needed by run.py
 root = None
 
-import m5
-
 def run_test(root):
-        # Called from tests/run.py
-
-
-        # Set the working directory in case we are executing from
-        # outside gem5's source tree
-        import os
-        os.chdir(os.path.join(os.path.dirname(__file__), "../"))
-
-        # Add paths that we need
-        m5.util.addToPath('../configs/learning_gem5/part1')
-        m5.util.addToPath('../configs/')
-
-        # The path to this script is the only parameter. Delete it so we can
-        # execute the script that we want to execute.
-        import sys
-        del sys.argv[1:]
-        # Note: at this point, we could add options we want to test.
-        # For instance, sys.argv.append('--l2_size=512kB')
-
-        # Execute the script we are wrapping
-        execfile(srcpath('configs/learning_gem5/part1/two_level.py'))
+        run_config('configs/learning_gem5/part1/two_level.py')
diff --git a/tests/configs/memcheck.py b/tests/configs/memcheck.py
index 97f8d13..ec20431 100644
--- a/tests/configs/memcheck.py
+++ b/tests/configs/memcheck.py
@@ -54,18 +54,11 @@
 def run_test(root):
         # Called from tests/run.py
 
-        # Set the working directory in case we are executing from
-        # outside gem5's source tree
-        import os
-        os.chdir(os.path.join(os.path.dirname(__file__), "../"))
-
-        # The path to this script is the only parameter. Delete it so
-        # we can execute the script that we want to execute.
         import sys
-        del sys.argv[1:]
-
-        # Add a specific max tick
-        sys.argv.append('-m %d' % maxtick)
+        argv = [
+                sys.argv[0],
+                '-m %d' % maxtick,
+        ]
 
         # Execute the script we are wrapping
-        execfile(srcpath('configs/example/memcheck.py'), globals())
+        run_config('configs/example/memcheck.py', argv=argv)
diff --git a/tests/run.py b/tests/run.py
index 845a3eb..93ea82e 100644
--- a/tests/run.py
+++ b/tests/run.py
@@ -177,6 +177,32 @@
     """Path to file in gem5's source tree"""
     return joinpath(os.path.dirname(__file__), "..", path)
 
+def run_config(config, argv=None):
+    """Execute a configuration script that is external to the test system"""
+
+    src_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))
+    abs_path = joinpath(src_root, config)
+
+    code = compile(open(abs_path, 'r').read(), abs_path, 'exec')
+    scope = {
+        '__file__' : config,
+        '__name__' : '__m5_main__',
+    }
+
+    # Set the working directory in case we are executing from
+    # outside gem5's source tree
+    os.chdir(src_root)
+
+    # gem5 normally adds the script's directory to the path to make
+    # script-relative imports work.
+    sys.path = [ os.path.dirname(abs_path), ] + sys.path
+
+    if argv is None:
+        sys.argv = [ config, ]
+    else:
+        sys.argv = argv
+    exec(code, scope)
+
 # build configuration
 sys.path.append(joinpath(tests_root, 'configs'))
 test_filename = config