Repository: SCons/scons

Issue:
testsuite won't run on Windows with VS Code installed
Mostly filed for recording purposes - if others have more defails please add.

On systems with Visual Studio Code installed, with the Python extension and pylance (which is now the favored engine), Code "steals" the .py file association. This has the effect that tests which try to execute a script directly (usually via hand-crafted shebang, which typically isn't very useful on Windows anyway) end up not running, but opening an instance of Code with the my_python script ready for editing.  Because of the strange way scons-time tests work, this means the scons-time tests aren't usable.  

Further, in at least three instances, there's a fatal javascript exception on the screen, but at the moment I don't have any way to associate that with the test(s) that trigger it. **Update**: this comes from `test/sconsign/script/SConsignFile.py`, apparently. The error has something to wo with `%USERPROFILE%` underfined, and JavaScript is involved because it's VS Code firing this up again...  this may be related to the recent claim that we're executing Windows processes with too limited a set of environment vars.

In the end, probably due to the javascript-triggered error, the thread lock is never released, so the testsuite never "finishes".  

After manually killing things, we get an exception on unraisablehook: https://docs.python.org/3/library/sys.html#sys.unraisablehook

The runtest Tee class apparently needs to implement a flush method, as I've seen that complaint as well.

This is all fairly recent, observed now on two Windows systems, at least one of them which has successfully run the SCons testsuite without odd problems in the not too distant past.  We are not seeing these problems on the CI runs using AppVeyor.

 Not yet sure if this is an indication we're doing dodgy things and only now getting punished for it, or if VS Code is overstepping its bounds.




Discussion:
A little more on the JavaScript error: this comes from the three sconsign tests which set `CCCOM` to a dummy (script with shebang line).  Not running these in a safe way seems to be intentional, as there's a comment to that effect in the three tests (`SConsignFile.py`, `no-SConsignFile.py` and `Signatures.py`) like this:
```python
# TODO in the above, we would normally want to run a python program
# using "our python" as in:
#    CCCOM=[[r'%(_python_)s', r'%(fake_cc_py)s', 'sub2', '$TARGET', '$SOURCE']],
#    LINKCOM=[[r'%(_python_)s', r'%(fake_link_py)s', '$TARGET', '$SOURCE']],
# however we're looking at dependencies with sconsign, so that breaks things
```

I can arrange to skip the two tests on windows in the scons-time set that don't work because of this shebang stuff (it's too indirect to be able to actually fix, scons-time has its own lanucher to boot).  A quick test shows that works fine. Less sure what we can do about sconsign.  In any case, these are not scons itself, but auxiliary utilities, so it's irritating to be plagued by problems testing them....

The scons-time skips are in PR #4056, which will not close this issue.  Keep thinking if there's a better way to detect the situation. Still need to figure something for the sconsign tests.
Okay, a bit more information, still pinging back and forth with people.  The Code behavior is mentioned here:

https://code.visualstudio.com/updates/v1_60

So this has been the case since August.

This is the relevant snip:

> Following the new best practices, VS Code's checkbox Register VS Code to be the editor for associated file types will now be checked on by default during Windows installation. We also reviewed the list of associated file types and expanded it to include more of the frequently opened file types.

Have already gotten feedback from MS types about this being "best practices" and "very reasonable behavior" so it seems unlikley anything will change.
Confirmed, we're dealing with this almost perfect storm of issues:
1. We try to execute the `CCOM` we've built up using `cmd.exe` (the line looks like `file='C:\\WINDOWS\\System32\\cmd.exe', args=['C:\\WINDOWS\\System32\\cmd.exe', '/C', '"C:\\Users\\mats\\AppData\\Local\\Temp\\testcmd.13088.un1bsxea\\fake_cc.py sub2 "sub1\\hello.obj" "sub1\\hello.c""']`)
2. Visual Studio Code optionally sets the file association for `.py` to open in Code. You can decline this - or in my case, Chocolatey's packaging hid the question so I was never asked.  Unfortunately, this *also* sets the command to open with when running through cmd and using the .py association. This is *clearly* wrong (you run a command from a cmd shell, and a gui editor pops up instead), but Microsoft is sure they're right so it's unlikely to change.
3. `fake_cc.py` is opened to "run" by Code, but Code by default distrusts anything that hasn't previously been authorized, so it doesn't actually run, it just sits there.
4. Meanwhile, some part of Code takes a javascript exception because the `env` we pass doesn't have %USERPROFILE% set in it, and that turns out to be fatal (and considered unsupported in today's WIndows world).
5. Due to the untimely exit, we don't return to our wrapped `spawnve` function, so the lock it holds is not released.
6. The entire test run is wedged until killed forcibly.

Pretty, no?

Resolving patch:
diff --git a/SCons/Platform/win32.py b/SCons/Platform/win32.py
--- a/SCons/Platform/win32.py
+++ b/SCons/Platform/win32.py
@@ -381,7 +381,7 @@ def generate(env):
     # for SystemDrive because it's related.
     #
     # Weigh the impact carefully before adding other variables to this list.
-    import_env = ['SystemDrive', 'SystemRoot', 'TEMP', 'TMP' ]
+    import_env = ['SystemDrive', 'SystemRoot', 'TEMP', 'TMP', 'USERPROFILE']
     for var in import_env:
         v = os.environ.get(var)
         if v:
diff --git a/SCons/Script/Main.py b/SCons/Script/Main.py
--- a/SCons/Script/Main.py
+++ b/SCons/Script/Main.py
@@ -788,13 +788,12 @@ def homedir(d):
         return os.path.expanduser('~/'+d)
 
     if platform == 'win32' or platform == 'cygwin':
-        # Note we use $ here instead of %...% because older
-        # pythons (prior to 2.6?) didn't expand %...% on Windows.
-        # This set of dirs should work on XP, Vista, 7 and later.
         sysdirs=[
-            os.path.expandvars('$ALLUSERSPROFILE\\Application Data\\scons'),
-            os.path.expandvars('$USERPROFILE\\Local Settings\\Application Data\\scons')]
-        appdatadir = os.path.expandvars('$APPDATA\\scons')
+            os.path.expandvars('%AllUsersProfile%\\scons'),
+            # TODO older path, kept for compat
+            os.path.expandvars('%AllUsersProfile%\\Application Data\\scons'),
+            os.path.expandvars('%LocalAppData%\\scons')]
+        appdatadir = os.path.expandvars('%AppData%\\scons')
         if appdatadir not in sysdirs:
             sysdirs.append(appdatadir)
         sysdirs.append(homedir('.scons'))
diff --git a/runtest.py b/runtest.py
--- a/runtest.py
+++ b/runtest.py
@@ -258,31 +258,42 @@ def flush(self, data):
     sys.stderr = Tee(logfile, sys.stderr)
 
 # --- define helpers ----
-if sys.platform in ('win32', 'cygwin'):
-
-    def whereis(file):
-        pathext = [''] + os.environ['PATHEXT'].split(os.pathsep)
-        for d in os.environ['PATH'].split(os.pathsep):
-            f = os.path.join(d, file)
-            for ext in pathext:
-                fext = f + ext
-                if os.path.isfile(fext):
-                    return fext
-        return None
-
-else:
+if sys.platform == 'win32':
+    # thanks to Eryk Sun for this recipe
+    import ctypes
+
+    shlwapi = ctypes.OleDLL('shlwapi')
+    shlwapi.AssocQueryStringW.argtypes = (
+        ctypes.c_ulong,  # flags
+        ctypes.c_ulong,  # str
+        ctypes.c_wchar_p,  # pszAssoc
+        ctypes.c_wchar_p,  # pszExtra
+        ctypes.c_wchar_p,  # pszOut
+        ctypes.POINTER(ctypes.c_ulong),  # pcchOut
+    )
 
-    def whereis(file):
-        for d in os.environ['PATH'].split(os.pathsep):
-            f = os.path.join(d, file)
-            if os.path.isfile(f):
-                try:
-                    st = os.stat(f)
-                except OSError:
-                    continue
-                if stat.S_IMODE(st[stat.ST_MODE]) & 0o111:
-                    return f
-        return None
+    ASSOCF_NOTRUNCATE = 0x00000020
+    ASSOCF_INIT_IGNOREUNKNOWN = 0x00000400
+    ASSOCSTR_COMMAND = 1
+    ASSOCSTR_EXECUTABLE = 2
+    E_POINTER = ctypes.c_long(0x80004003).value
+
+    def get_template_command(filetype, verb=None):
+        flags = ASSOCF_INIT_IGNOREUNKNOWN | ASSOCF_NOTRUNCATE
+        assoc_str = ASSOCSTR_COMMAND
+        cch = ctypes.c_ulong(260)
+        while True:
+            buf = (ctypes.c_wchar * cch.value)()
+            try:
+                shlwapi.AssocQueryStringW(
+                    flags, assoc_str, filetype, verb, buf, ctypes.byref(cch)
+                )
+            except OSError as e:
+                if e.winerror != E_POINTER:
+                    raise
+                continue
+            break
+        return buf.value
 
 
 _ws = re.compile(r'\s')
@@ -528,6 +539,19 @@ def footer(self, f):
 if pythonpath:
     testenv['PYTHONPATH'] = testenv['PYTHONPATH'] + os.pathsep + pythonpath
 
+if sys.platform == 'win32':
+    # Windows doesn't support "shebang" lines directly (the Python launcher
+    # and Windows Store version do, but you have to get them launched first)
+    # so to directly launch a script we depend on an assoc for .py to work.
+    # Some systems may have none, and in some cases IDE programs take over 
+    # the assoc.  Detect this so the small number of tests affected can skip.
+    try:
+        python_assoc = get_template_command('.py')
+    except OSError:
+        python_assoc = None
+    if not python_assoc or "py" not in python_assoc:
+        testenv['SCONS_NO_DIRECT_SCRIPT'] = '1'
+
 os.environ.update(testenv)
 
 # Clear _JAVA_OPTIONS which java tools output to stderr when run breaking tests


Test patch:
diff --git a/test/scons-time/run/config/python.py b/test/scons-time/run/config/python.py
--- a/test/scons-time/run/config/python.py
+++ b/test/scons-time/run/config/python.py
@@ -30,33 +30,39 @@
 import os
 
 import TestSCons_time
-from TestCmd import IS_WINDOWS
 
-_python_ = TestSCons_time._python_
+from TestCmd import NEED_HELPER
+from TestSCons_time import _python_
 
 test = TestSCons_time.TestSCons_time()
-if IS_WINDOWS:
-    # tests expect Windows file assoc to run my_python, not in our control.
-    test.skip_test("Skipping test on win32 due to launch problems.")
+
+if NEED_HELPER:
+    test.skip_test("Test host cannot directly execute scripts, skipping test\n")
 
 test.write_sample_project('foo.tar.gz')
 
 my_python_py = test.workpath('my_python.py')
 
-test.write('config', """\
-python = r'%(my_python_py)s'
-""" % locals())
+test.write(
+    'config',
+    f"""\
+python = f'{my_python_py}'
+""",
+)
 
-test.write(my_python_py, """\
-#!%(_python_)s
+test.write(
+    my_python_py,
+    f"""\
+#!{_python_}
 import sys
 profile = ''
 for arg in sys.argv[1:]:
     if arg.startswith('--profile='):
         profile = arg[10:]
         break
-print('my_python.py: %%s' %% profile)
-""" % locals())
+print('my_python.py: %s' % profile)
+""",
+)
 
 os.chmod(my_python_py, 0o755)
 
diff --git a/test/scons-time/run/option/python.py b/test/scons-time/run/option/python.py
--- a/test/scons-time/run/option/python.py
+++ b/test/scons-time/run/option/python.py
@@ -30,29 +30,32 @@
 import os
 
 import TestSCons_time
-from TestCmd import IS_WINDOWS
 
-_python_ = TestSCons_time._python_
+from TestCmd import NEED_HELPER
+from TestSCons_time import _python_
 
 test = TestSCons_time.TestSCons_time()
-if IS_WINDOWS:
-    # tests expect Windows file assoc to run my_python, not in our control.
-    test.skip_test("Skipping test on win32 due to launch problems.")
+
+if NEED_HELPER:
+    test.skip_test("Test host cannot directly execute scripts, skipping test\n")
 
 test.write_sample_project('foo.tar.gz')
 
 my_python_py = test.workpath('my_python.py')
 
-test.write(my_python_py, """\
-#!%(_python_)s
+test.write(
+    my_python_py,
+    f"""\
+#!{_python_}
 import sys
 profile = ''
 for arg in sys.argv[1:]:
     if arg.startswith('--profile='):
         profile = arg[10:]
         break
-sys.stdout.write('my_python.py: %%s\\n' %% profile)
-""" % locals())
+sys.stdout.write('my_python.py: %s\\n' % profile)
+""",
+)
 
 os.chmod(my_python_py, 0o755)
 
diff --git a/test/sconsign/script/SConsignFile.py b/test/sconsign/script/SConsignFile.py
--- a/test/sconsign/script/SConsignFile.py
+++ b/test/sconsign/script/SConsignFile.py
@@ -31,15 +31,22 @@
 import TestSCons
 import TestSConsign
 
-_python_ = TestSCons._python_
+from TestSCons import _python_
+from TestCmd import NEED_HELPER
 
 test = TestSConsign.TestSConsign(match = TestSConsign.match_re)
+
+if NEED_HELPER:
+    test.skip_test("Test host cannot directly execute scripts, skipping test\n")
+
 test.subdir('sub1', 'sub2')
 
 fake_cc_py = test.workpath('fake_cc.py')
 fake_link_py = test.workpath('fake_link.py')
 
-test.write(fake_cc_py, r"""#!%(_python_)s
+test.write(
+    fake_cc_py,
+    fr"""#!{_python_}
 import os
 import re
 import sys
@@ -66,21 +73,25 @@ def process(infp, outfp):
             outfp.write(line)
 
 with open(sys.argv[2], 'w') as outf, open(sys.argv[3], 'r') as ifp:
-    outf.write('fake_cc.py:  %%s\n' %% sys.argv)
+    outf.write('fake_cc.py:  %s\n' % sys.argv)
     process(ifp, outf)
 
 sys.exit(0)
-""" % locals())
+""",
+)
 
-test.write(fake_link_py, r"""#!%(_python_)s
+test.write(
+    fake_link_py,
+    fr"""#!{_python_}
 import sys
 
 with open(sys.argv[1], 'w') as outf, open(sys.argv[2], 'r') as ifp:
-    outf.write('fake_link.py:  %%s\n' %% sys.argv)
+    outf.write('fake_link.py:  %s\n' % sys.argv)
     outf.write(ifp.read())
 
 sys.exit(0)
-""" % locals())
+""",
+)
 
 test.chmod(fake_cc_py, 0o755)
 test.chmod(fake_link_py, 0o755)
@@ -96,24 +107,29 @@ def process(infp, outfp):
 sub2_inc1_h     = 'sub2/inc1.h'
 sub2_inc2_h     = 'sub2/inc2.h'
 
-test.write(['SConstruct'], """\
+test.write(
+    ['SConstruct'],
+    f"""\
 SConsignFile()
 env1 = Environment(
     PROGSUFFIX='.exe',
     OBJSUFFIX='.obj',
-    CCCOM=[[r'%(fake_cc_py)s', 'sub2', '$TARGET', '$SOURCE']],
-    LINKCOM=[[r'%(fake_link_py)s', '$TARGET', '$SOURCE']],
+    CCCOM=[[r'{fake_cc_py}', 'sub2', '$TARGET', '$SOURCE']],
+    LINKCOM=[[r'{fake_link_py}', '$TARGET', '$SOURCE']],
 )
 env1.PrependENVPath('PATHEXT', '.PY')
 env1.Program('sub1/hello.c')
 env2 = env1.Clone(CPPPATH=['sub2'])
 env2.Program('sub2/hello.c')
-""" % locals())
+""",
+)
 # TODO in the above, we would normally want to run a python program
-# using "our python" as in:
-#    CCCOM=[[r'%(_python_)s', r'%(fake_cc_py)s', 'sub2', '$TARGET', '$SOURCE']],
-#    LINKCOM=[[r'%(_python_)s', r'%(fake_link_py)s', '$TARGET', '$SOURCE']],
-# however we're looking at dependencies with sconsign, so that breaks things
+# using "our python" like this:
+#    CCCOM=[[r'{_python_}', r'{fake_cc_py}', 'sub2', '$TARGET', '$SOURCE']],
+#    LINKCOM=[[r'{_python_}', r'{fake_link_py}', '$TARGET', '$SOURCE']],
+# however we're looking at dependencies with sconsign, so that breaks things.
+# It still breaks things on Windows if something else is registered as the
+# handler for .py files, as Visual Studio Code installs itself.
 
 test.write(['sub1', 'hello.c'], r"""
 sub1/hello.c
@@ -131,11 +147,11 @@ def process(infp, outfp):
 }
 """)
 
-test.write(['sub2', 'inc1.h'], r"""\
+test.write(['sub2', 'inc1.h'], r"""
 #define STRING1 "inc1.h"
 """)
 
-test.write(['sub2', 'inc2.h'], r"""\
+test.write(['sub2', 'inc2.h'], r"""
 #define STRING2 "inc2.h"
 """)
 
diff --git a/test/sconsign/script/Signatures.py b/test/sconsign/script/Signatures.py
--- a/test/sconsign/script/Signatures.py
+++ b/test/sconsign/script/Signatures.py
@@ -32,10 +32,14 @@
 import TestSCons
 import TestSConsign
 
-_python_ = TestSCons._python_
+from TestSCons import _python_
+from TestCmd import NEED_HELPER
 
 test = TestSConsign.TestSConsign(match = TestSConsign.match_re)
 
+if NEED_HELPER:
+    test.skip_test("Test host cannot directly execute scripts, skipping test\n")
+
 # Note:  We don't use os.path.join() representations of the file names
 # in the expected output because paths in the .sconsign files are
 # canonicalized to use / as the separator.
@@ -58,7 +62,9 @@
 fake_cc_py = test.workpath('fake_cc.py')
 fake_link_py = test.workpath('fake_link.py')
 
-test.write(fake_cc_py, r"""#!%(_python_)s
+test.write(
+    fake_cc_py,
+    fr"""#!{_python_}
 import os
 import re
 import sys
@@ -85,26 +91,32 @@ def process(infp, outfp):
             outfp.write(line)
 
 with open(sys.argv[2], 'w') as outf, open(sys.argv[3], 'r') as ifp:
-    outf.write('fake_cc.py:  %%s\n' %% sys.argv)
+    outf.write('fake_cc.py:  %s\n' % sys.argv)
     process(ifp, outf)
 
 sys.exit(0)
-""" % locals())
+""",
+)
 
-test.write(fake_link_py, r"""#!%(_python_)s
+test.write(
+    fake_link_py,
+    fr"""#!{_python_}
 import sys
 
 with open(sys.argv[1], 'w') as outf, open(sys.argv[2], 'r') as ifp:
-    outf.write('fake_link.py:  %%s\n' %% sys.argv)
+    outf.write('fake_link.py:  %s\n' % sys.argv)
     outf.write(ifp.read())
 
 sys.exit(0)
-""" % locals())
+""",
+)
 
 test.chmod(fake_cc_py, 0o755)
 test.chmod(fake_link_py, 0o755)
 
-test.write('SConstruct', """
+test.write(
+    'SConstruct',
+    f"""
 SConsignFile(None)
 Decider('timestamp-newer')
 env1 = Environment(
@@ -113,19 +125,22 @@ def process(infp, outfp):
     # Specify the command lines with lists-of-lists so
     # finding the implicit dependencies works even with
     # spaces in the fake_*_py path names.
-    CCCOM=[[r'%(fake_cc_py)s', 'sub2', '$TARGET', '$SOURCE']],
-    LINKCOM=[[r'%(fake_link_py)s', '$TARGET', '$SOURCE']],
+    CCCOM=[[r'{fake_cc_py}', 'sub2', '$TARGET', '$SOURCE']],
+    LINKCOM=[[r'{fake_link_py}', '$TARGET', '$SOURCE']],
 )
 env1.PrependENVPath('PATHEXT', '.PY')
 env1.Program('sub1/hello.c')
 env2 = env1.Clone(CPPPATH=['sub2'])
 env2.Program('sub2/hello.c')
-""" % locals())
+""",
+)
 # TODO in the above, we would normally want to run a python program
-# using "our python" as in:
-#    CCCOM=[[r'%(_python_)s', r'%(fake_cc_py)s', 'sub2', '$TARGET', '$SOURCE']],
-#    LINKCOM=[[r'%(_python_)s', r'%(fake_link_py)s', '$TARGET', '$SOURCE']],
-# however we're looking at dependencies with sconsign, so that breaks things
+# using "our python" like this:
+#    CCCOM=[[r'{_python_}', r'{fake_cc_py}', 'sub2', '$TARGET', '$SOURCE']],
+#    LINKCOM=[[r'{_python_}', r'{fake_link_py}', '$TARGET', '$SOURCE']],
+# however we're looking at dependencies with sconsign, so that breaks things.
+# It still breaks things on Windows if something else is registered as the
+# handler for .py files, as Visual Studio Code installs itself.
 
 test.write(['sub1', 'hello.c'], r"""\
 sub1/hello.c
@@ -154,7 +169,7 @@ def process(infp, outfp):
 
 database_name = test.get_sconsignname()
 
-test.run_sconsign(arguments = "-e hello.exe -e hello.obj sub1/{}".format(database_name),
+test.run_sconsign(arguments = f"-e hello.exe -e hello.obj sub1/{database_name}",
          stdout = r"""hello.exe: %(sig_re)s \d+ \d+
         %(sub1_hello_obj)s: %(sig_re)s \d+ \d+
         fake_link\.py: None \d+ \d+
@@ -165,7 +180,7 @@ def process(infp, outfp):
         %(sig_re)s \[.*\]
 """ % locals())
 
-test.run_sconsign(arguments = "-e hello.exe -e hello.obj -r sub1/{}".format(database_name),
+test.run_sconsign(arguments = f"-e hello.exe -e hello.obj -r sub1/{database_name}",
          stdout = r"""hello.exe: %(sig_re)s '%(date_re)s' \d+
         %(sub1_hello_obj)s: %(sig_re)s '%(date_re)s' \d+
         fake_link\.py: None '%(date_re)s' \d+
diff --git a/test/sconsign/script/no-SConsignFile.py b/test/sconsign/script/no-SConsignFile.py
--- a/test/sconsign/script/no-SConsignFile.py
+++ b/test/sconsign/script/no-SConsignFile.py
@@ -22,9 +22,6 @@
 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-#
-
-__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
 """
 Verify that the sconsign script works when using an individual
@@ -34,10 +31,14 @@
 import TestSCons
 import TestSConsign
 
-_python_ = TestSCons._python_
+from TestSCons import _python_
+from TestCmd import NEED_HELPER
 
 test = TestSConsign.TestSConsign(match = TestSConsign.match_re)
 
+if NEED_HELPER:
+    test.skip_test("Test host cannot directly execute scripts, skipping test\n")
+
 test.subdir('sub1', 'sub2')
 
 database_name = test.get_sconsignname()
@@ -55,7 +56,9 @@
 fake_cc_py = test.workpath('fake_cc.py')
 fake_link_py = test.workpath('fake_link.py')
 
-test.write(fake_cc_py, r"""#!%(_python_)s
+test.write(
+    fake_cc_py,
+    fr"""#!{_python_}
 import os
 import re
 import sys
@@ -82,21 +85,25 @@ def process(infp, outfp):
             outfp.write(line)
 
 with open(sys.argv[2], 'w') as outf, open(sys.argv[3], 'r') as ifp:
-    outf.write('fake_cc.py:  %%s\n' %% sys.argv)
+    outf.write('fake_cc.py:  %s\n' % sys.argv)
     process(ifp, outf)
 
 sys.exit(0)
-""" % locals())
+""",
+)
 
-test.write(fake_link_py, r"""#!%(_python_)s
+test.write(
+    fake_link_py,
+    fr"""#!{_python_}
 import sys
 
 with open(sys.argv[1], 'w') as outf, open(sys.argv[2], 'r') as ifp:
-    outf.write('fake_link.py:  %%s\n' %% sys.argv)
+    outf.write('fake_link.py:  %s\n' % sys.argv)
     outf.write(ifp.read())
 
 sys.exit(0)
-""" % locals())
+""",
+)
 
 test.chmod(fake_cc_py, 0o755)
 test.chmod(fake_link_py, 0o755)
@@ -112,7 +119,9 @@ def process(infp, outfp):
 sub2_inc1_h     = 'sub2/inc1.h'
 sub2_inc2_h     = 'sub2/inc2.h'
 
-test.write(['SConstruct'], """
+test.write(
+    ['SConstruct'],
+    f"""
 SConsignFile(None)
 env1 = Environment(
     PROGSUFFIX='.exe',
@@ -120,35 +129,38 @@ def process(infp, outfp):
     # Specify the command lines with lists-of-lists so
     # finding the implicit dependencies works even with
     # spaces in the fake_*_py path names.
-    CCCOM=[[r'%(fake_cc_py)s', 'sub2', '$TARGET', '$SOURCE']],
-    LINKCOM=[[r'%(fake_link_py)s', '$TARGET', '$SOURCE']],
+    CCCOM=[[r'{fake_cc_py}', 'sub2', '$TARGET', '$SOURCE']],
+    LINKCOM=[[r'{fake_link_py}', '$TARGET', '$SOURCE']],
 )
 env1.PrependENVPath('PATHEXT', '.PY')
 env1.Program('sub1/hello.c')
 env2 = env1.Clone(CPPPATH=['sub2'])
 env2.Program('sub2/hello.c')
-""" % locals())
+""",
+)
 # TODO in the above, we would normally want to run a python program
-# using "our python" as in:
-#    CCCOM=[[r'%(_python_)s', r'%(fake_cc_py)s', 'sub2', '$TARGET', '$SOURCE']],
-#    LINKCOM=[[r'%(_python_)s', r'%(fake_link_py)s', '$TARGET', '$SOURCE']],
-# however we're looking at dependencies with sconsign, so that breaks things
-
-test.write(['sub1', 'hello.c'], r"""\
+# using "our python" like this:
+#    CCCOM=[[r'{_python_}', r'{fake_cc_py}', 'sub2', '$TARGET', '$SOURCE']],
+#    LINKCOM=[[r'{_python_}', r'{fake_link_py}', '$TARGET', '$SOURCE']],
+# however we're looking at dependencies with sconsign, so that breaks things.
+# It still breaks things on Windows if something else is registered as the
+# handler for .py files, as Visual Studio Code installs itself.
+
+test.write(['sub1', 'hello.c'], r"""
 sub1/hello.c
 """)
 
-test.write(['sub2', 'hello.c'], r"""\
+test.write(['sub2', 'hello.c'], r"""
 #include <inc1.h>
 #include <inc2.h>
 sub2/hello.c
 """)
 
-test.write(['sub2', 'inc1.h'], r"""\
+test.write(['sub2', 'inc1.h'], r"""
 #define STRING1 "inc1.h"
 """)
 
-test.write(['sub2', 'inc2.h'], r"""\
+test.write(['sub2', 'inc2.h'], r"""
 #define STRING2 "inc2.h"
 """)
 
@@ -167,9 +179,9 @@ def process(infp, outfp):
         %(sig_re)s \[.*\]
 """ % locals()
 
-test.run_sconsign(arguments = "sub1/{}".format(database_name), stdout=expect)
+test.run_sconsign(arguments = f"sub1/{database_name}", stdout=expect)
 
-test.run_sconsign(arguments = "--raw sub1/{}".format(database_name),
+test.run_sconsign(arguments = f"--raw sub1/{database_name}",
          stdout = r"""hello.c: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2}
 hello.exe: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2}
         %(sub1_hello_obj)s: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2}
@@ -181,7 +193,7 @@ def process(infp, outfp):
         %(sig_re)s \[.*\]
 """ % locals())
 
-test.run_sconsign(arguments = "-v sub1/{}".format(database_name),
+test.run_sconsign(arguments = f"-v sub1/{database_name}",
          stdout = r"""hello.c:
     csig: %(sig_re)s
     timestamp: \d+
@@ -216,7 +228,7 @@ def process(infp, outfp):
     action: %(sig_re)s \[.*\]
 """ % locals())
 
-test.run_sconsign(arguments = "-c -v sub1/{}".format(database_name),
+test.run_sconsign(arguments = f"-c -v sub1/{database_name}",
          stdout = r"""hello.c:
     csig: %(sig_re)s
 hello.exe:
@@ -225,7 +237,7 @@ def process(infp, outfp):
     csig: %(sig_re)s
 """ % locals())
 
-test.run_sconsign(arguments = "-s -v sub1/{}".format(database_name),
+test.run_sconsign(arguments = f"-s -v sub1/{database_name}",
          stdout = r"""hello.c:
     size: \d+
 hello.exe:
@@ -234,7 +246,7 @@ def process(infp, outfp):
     size: \d+
 """ % locals())
 
-test.run_sconsign(arguments = "-t -v sub1/{}".format(database_name),
+test.run_sconsign(arguments = f"-t -v sub1/{database_name}",
          stdout = r"""hello.c:
     timestamp: \d+
 hello.exe:
@@ -243,14 +255,14 @@ def process(infp, outfp):
     timestamp: \d+
 """ % locals())
 
-test.run_sconsign(arguments = "-e hello.obj sub1/{}".format(database_name),
+test.run_sconsign(arguments = f"-e hello.obj sub1/{database_name}",
          stdout = r"""hello.obj: %(sig_re)s \d+ \d+
         %(sub1_hello_c)s: %(sig_re)s \d+ \d+
         fake_cc\.py: %(sig_re)s \d+ \d+
         %(sig_re)s \[.*\]
 """ % locals())
 
-test.run_sconsign(arguments = "-e hello.obj -e hello.exe -e hello.obj sub1/{}".format(database_name),
+test.run_sconsign(arguments = f"-e hello.obj -e hello.exe -e hello.obj sub1/{database_name}",
          stdout = r"""hello.obj: %(sig_re)s \d+ \d+
         %(sub1_hello_c)s: %(sig_re)s \d+ \d+
         fake_cc\.py: %(sig_re)s \d+ \d+
@@ -265,7 +277,7 @@ def process(infp, outfp):
         %(sig_re)s \[.*\]
 """ % locals())
 
-test.run_sconsign(arguments = "sub2/{}".format(database_name),
+test.run_sconsign(arguments = f"sub2/{database_name}",
          stdout = r"""hello.c: %(sig_re)s \d+ \d+
 hello.exe: %(sig_re)s \d+ \d+
         %(sub2_hello_obj)s: %(sig_re)s \d+ \d+
@@ -293,7 +305,7 @@ def process(infp, outfp):
 #        inc2.h: %(sig_re)s \d+ \d+
 #""" % locals())
 
-test.run_sconsign(arguments = "-e hello.obj sub2/{} sub1/{}".format(database_name, database_name),
+test.run_sconsign(arguments = f"-e hello.obj sub2/{database_name} sub1/{database_name}",
          stdout = r"""hello.obj: %(sig_re)s \d+ \d+
         %(sub2_hello_c)s: %(sig_re)s \d+ \d+
         %(sub2_inc1_h)s: %(sig_re)s \d+ \d+
diff --git a/testing/framework/TestCmd.py b/testing/framework/TestCmd.py
--- a/testing/framework/TestCmd.py
+++ b/testing/framework/TestCmd.py
@@ -312,17 +312,15 @@
 from collections import UserList, UserString
 from subprocess import PIPE, STDOUT
 
-
 IS_WINDOWS = sys.platform == 'win32'
 IS_MACOS = sys.platform == 'darwin'
 IS_64_BIT = sys.maxsize > 2**32
 IS_PYPY = hasattr(sys, 'pypy_translation_info')
-
+NEED_HELPER = os.environ.get('SCONS_NO_DIRECT_SCRIPT')
 
 # sentinel for cases where None won't do
 _Null = object()
 
-
 __all__ = [
     'diff_re',
     'fail_test',
diff --git a/testing/framework/TestSCons.py b/testing/framework/TestSCons.py
--- a/testing/framework/TestSCons.py
+++ b/testing/framework/TestSCons.py
@@ -307,8 +307,6 @@ def __init__(self, **kw):
                 kw['program'] = os.path.join(self.orig_cwd, kw['program'])
         if 'interpreter' not in kw and not os.environ.get('SCONS_EXEC'):
             kw['interpreter'] = [python, ]
-            if sys.version_info[0] < 3:
-                kw['interpreter'].append('-tt')
         if 'match' not in kw:
             kw['match'] = match_exact
         if 'workdir' not in kw:
diff --git a/testing/framework/TestSConsMSVS.py b/testing/framework/TestSConsMSVS.py
--- a/testing/framework/TestSConsMSVS.py
+++ b/testing/framework/TestSConsMSVS.py
@@ -763,8 +763,7 @@ def run(self, *args, **kw):
         return result
 
     def get_vs_host_arch(self):
-        """ Get an MSVS, SDK , and/or MSVS acceptable platform arch
-        """
+        """ Returns an MSVS, SDK , and/or MSVS acceptable platform arch. """
 
         # Dict to 'canonalize' the arch
         _ARCH_TO_CANONICAL = {
@@ -778,11 +777,6 @@ def get_vs_host_arch(self):
         }
 
         host_platform = platform.machine()
-        # TODO(2.5):  the native Python platform.machine() function returns
-        # '' on all Python versions before 2.6, after which it also uses
-        # PROCESSOR_ARCHITECTURE.
-        if not host_platform:
-            host_platform = os.environ.get('PROCESSOR_ARCHITECTURE', '')
 
         try:
             host = _ARCH_TO_CANONICAL[host_platform]