diff --git a/testsuite/driver/testglobals.py b/testsuite/driver/testglobals.py
index 6100c3065cc47885cc06a9f77893d6d410cb49e2..f22ebb994c6befcff83af7095cef560d3048d60c 100644
--- a/testsuite/driver/testglobals.py
+++ b/testsuite/driver/testglobals.py
@@ -352,6 +352,9 @@ class TestOptions:
        self.ignore_stdout = False
        self.ignore_stderr = False
 
+       # don't use the executable extension
+       self.ignore_extension = False
+
        # Backpack test
        self.compile_backpack = False
 
diff --git a/testsuite/driver/testlib.py b/testsuite/driver/testlib.py
index 62c2ae321eb6302402d13a3368066df0e51c15c3..f27b868906649205e7626dd5b7fcde2a35949374 100644
--- a/testsuite/driver/testlib.py
+++ b/testsuite/driver/testlib.py
@@ -138,9 +138,6 @@ def no_deps( name, opts):
 def skip( name, opts ):
     opts.skip = True
 
-def js_arch() -> bool:
-    return arch("javascript");
-
 # disable test on JS arch
 def js_skip( name, opts ):
     if js_arch():
@@ -379,6 +376,18 @@ def ignore_stdout(name, opts):
 def ignore_stderr(name, opts):
     opts.ignore_stderr = True
 
+def ignore_extension(name, opts):
+    """
+    Some tests generate files that are not expected to be suffixed with an
+    extension type, such as .exe on windows. This option allows these tests to
+    have finer-grained control over the filename that the testsuite will look
+    for. Examples of such tests are hpc tests which expect a .tix extension and
+    hp2ps tests which expect .hp. For these tests, on windows and without
+    ignoring the extension, the testsuite will look for, e.g., 'foo.exe.tix'
+    instead of 'foo.tix'.
+    """
+    opts.ignore_extension = True
+
 def combined_output( name, opts ):
     opts.combined_output = True
 
@@ -792,6 +801,8 @@ KNOWN_OPERATING_SYSTEMS = set([
 def exe_extension() -> str:
     if config.arch == 'wasm32':
         return '.wasm'
+    elif config.os == "mingw32":
+        return '.exe'
     return ''
 
 def opsys( os: str ) -> bool:
@@ -810,6 +821,9 @@ def msys( ) -> bool:
 def cygwin( ) -> bool:
     return config.cygwin
 
+def js_arch() -> bool:
+    return arch("javascript");
+
 def have_vanilla( ) -> bool:
     return config.have_vanilla
 
@@ -1577,6 +1591,10 @@ async def ghci_script( name, way, script):
 async def compile( name, way, extra_hc_opts ):
     return await do_compile( name, way, False, None, [],  [], extra_hc_opts )
 
+async def compile_artifact( name, way, extra_hc_opts ):
+    # We suppress stderr so that the link output isn't compared
+    return await do_compile( name, way, False, None, [], [], extra_hc_opts, should_link=True, compare_stderr=False )
+
 async def compile_fail( name, way, extra_hc_opts ):
     return await do_compile( name, way, True, None, [], [], extra_hc_opts )
 
@@ -1592,9 +1610,6 @@ async def backpack_compile( name, way, extra_hc_opts ):
 async def backpack_compile_fail( name, way, extra_hc_opts ):
     return await do_compile( name, way, True, None, [], [], extra_hc_opts, backpack=True )
 
-async def backpack_run( name, way, extra_hc_opts ):
-    return await compile_and_run__( name, way, None, [], extra_hc_opts, backpack=True )
-
 async def multimod_compile( name, way, top_mod, extra_hc_opts ):
     return await do_compile( name, way, False, top_mod, [], [], extra_hc_opts )
 
@@ -1623,6 +1638,8 @@ async def do_compile(name: TestName,
                extra_mods: List[str],
                units: List[str],
                extra_hc_opts: str,
+               should_link=False,
+               compare_stderr=True,
                **kwargs
                ) -> PassFail:
     # print 'Compile only, extra args = ', extra_hc_opts
@@ -1632,7 +1649,7 @@ async def do_compile(name: TestName,
        return result
     extra_hc_opts = result.hc_opts
 
-    result = await simple_build(name, way, extra_hc_opts, should_fail, top_mod, units, False, True, **kwargs)
+    result = await simple_build(name, way, extra_hc_opts, should_fail, top_mod, units, should_link, True, **kwargs)
 
     if badResult(result):
         return result
@@ -1645,7 +1662,7 @@ async def do_compile(name: TestName,
     actual_stderr_file = add_suffix(name, 'comp.stderr')
     diff_file_name = in_testdir(add_suffix(name, 'comp.diff'))
 
-    if not await compare_outputs(way, 'stderr',
+    if compare_stderr and not await compare_outputs(way, 'stderr',
                            join_normalisers(getTestOpts().extra_errmsg_normaliser,
                                             normalise_errmsg),
                            expected_stderr_file, actual_stderr_file,
@@ -1747,7 +1764,8 @@ async def compile_and_run__(name: TestName,
                       extra_mods: List[str],
                       extra_hc_opts: str,
                       backpack: bool=False,
-                      compile_stderr: bool=False
+                      compile_stderr: bool=False,
+                      use_extension: bool=True
                       ) -> PassFail:
     # print 'Compile and run, extra args = ', extra_hc_opts
 
@@ -1780,8 +1798,11 @@ async def compile_and_run__(name: TestName,
              stderr = diff_file_name.read_text()
              diff_file_name.unlink()
              return failBecause('ghc.stderr mismatch', stderr=stderr)
-#
-        cmd = './' + name + exe_extension()
+
+        opts = getTestOpts()
+        extension = exe_extension() if not opts.ignore_extension else ""
+
+        cmd = './' + name + extension
 
         # we don't check the compiler's stderr for a compile-and-run test
         return await simple_run( name, way, cmd, getTestOpts().extra_run_opts )
@@ -1789,6 +1810,9 @@ async def compile_and_run__(name: TestName,
 async def compile_and_run( name, way, extra_hc_opts ):
     return await compile_and_run__( name, way, None, [], extra_hc_opts)
 
+async def backpack_run( name, way, extra_hc_opts ):
+    return await compile_and_run__( name, way, None, [], extra_hc_opts, backpack=True )
+
 async def multimod_compile_and_run( name, way, top_mod, extra_hc_opts ):
     return await compile_and_run__( name, way, top_mod, [], extra_hc_opts)
 
@@ -2296,8 +2320,8 @@ def write_file(f: Path, s: str) -> None:
 # operate on bytes.
 
 async def check_hp_ok(name: TestName) -> bool:
-    actual_name = name + exe_extension()
     opts = getTestOpts()
+    actual_name = name + exe_extension() if not opts.ignore_extension else name
 
     # do not qualify for hp2ps because we should be in the right directory
     hp2psCmd = 'cd "{opts.testdir}" && {{hp2ps}} {actual_name}'.format(**locals())
diff --git a/testsuite/tests/hpc/function/test.T b/testsuite/tests/hpc/function/test.T
index 24dfa70f83b8fce5a43b6fde27fe8185e20b1578..31f6c39e7980fa60c502f5e7b405f5930405c672 100644
--- a/testsuite/tests/hpc/function/test.T
+++ b/testsuite/tests/hpc/function/test.T
@@ -5,5 +5,6 @@ hpc_prefix = "perl hpcrun.pl --clear --exeext={exeext} --hpc={hpc}"
 test('tough',
      [extra_files(['../hpcrun.pl']),
       cmd_prefix(hpc_prefix),
+      ignore_extension,
       when(arch('wasm32'), fragile(23243))],
      compile_and_run, ['-fhpc'])
diff --git a/testsuite/tests/hpc/function2/test.T b/testsuite/tests/hpc/function2/test.T
index 3740e0e7b0757c8cb44dcb07bdde91ebc68b7758..d0899f8739877326556f30d6ef5b324292bdad49 100644
--- a/testsuite/tests/hpc/function2/test.T
+++ b/testsuite/tests/hpc/function2/test.T
@@ -10,6 +10,7 @@ test('tough2',
      [extra_files(['../hpcrun.pl', 'subdir/']),
        literate,
        cmd_prefix(hpc_prefix),
+       ignore_extension,
        omit_ways(ghci_ways + prof_ways), # profile goes in the wrong place
        when(arch('wasm32'), fragile(23243)) ],
      multimod_compile_and_run, ['subdir/tough2.lhs', '-fhpc'])
diff --git a/testsuite/tests/hpc/simple/test.T b/testsuite/tests/hpc/simple/test.T
index cd7cd8b0949529ab79af04f0b946d7862492b57b..38f4876c7f26a448cb5070aaabde7d32ddd73984 100644
--- a/testsuite/tests/hpc/simple/test.T
+++ b/testsuite/tests/hpc/simple/test.T
@@ -3,6 +3,7 @@ setTestOpts([omit_ghci, when(fast(), skip), js_skip])
 hpc_prefix = "perl hpcrun.pl --clear --exeext={exeext} --hpc={hpc}"
 
 test('hpc001', [extra_files(['../hpcrun.pl']), cmd_prefix(hpc_prefix),
-     when(arch('wasm32'), fragile(23243))
+     when(arch('wasm32'), fragile(23243)),
+     ignore_extension
      ],
      compile_and_run, ['-fhpc'])
diff --git a/testsuite/tests/perf/size/all.T b/testsuite/tests/perf/size/all.T
index 969185e438da4a483d65a7011962159fb0719c7e..3b1b46e78ad41c8ceac7640aa2a1074722b57aa2 100644
--- a/testsuite/tests/perf/size/all.T
+++ b/testsuite/tests/perf/size/all.T
@@ -1,3 +1,6 @@
 test('size_hello_obj', [collect_size(5, 'size_hello_obj.o')], compile, [''])
 
+test('size_hello_artifact', [collect_size(5, 'size_hello_artifact' + exe_extension())],
+                             compile_artifact, [''])
+
 test('libdir',[collect_size_dir(10, config.libdir)], static_stats, [] )
diff --git a/testsuite/tests/perf/size/size_hello_artifact.hs b/testsuite/tests/perf/size/size_hello_artifact.hs
new file mode 100644
index 0000000000000000000000000000000000000000..d38500d168f7d97f37d6a487c598bf201628b7b2
--- /dev/null
+++ b/testsuite/tests/perf/size/size_hello_artifact.hs
@@ -0,0 +1,4 @@
+-- same as size_hello_obj but we test the size of the resulting executable.
+module Main where
+
+main = print "Hello World!"
diff --git a/testsuite/tests/profiling/should_run/all.T b/testsuite/tests/profiling/should_run/all.T
index e09976508e0d14dca85175304a091b505a6de39e..a113645e229db0176d2dc5dd35964a6856e111df 100644
--- a/testsuite/tests/profiling/should_run/all.T
+++ b/testsuite/tests/profiling/should_run/all.T
@@ -4,6 +4,7 @@ setTestOpts(js_skip) # JS backend doesn't support profiling yet
 
 test('heapprof002',
      [extra_files(['heapprof001.hs']),
+      ignore_extension,
       pre_cmd('cp heapprof001.hs heapprof002.hs'), extra_ways(['normal_h']),
       extra_run_opts('7')],
      compile_and_run, [''])
diff --git a/testsuite/tests/typecheck/testeq1/test.T b/testsuite/tests/typecheck/testeq1/test.T
index 58dfc33e166fd2f332eedc4b23995c72359ece5e..c9bc9d411528fd8fa1c3afa451db0a9a9cb1a0f4 100644
--- a/testsuite/tests/typecheck/testeq1/test.T
+++ b/testsuite/tests/typecheck/testeq1/test.T
@@ -1,6 +1,7 @@
 
 test('typecheck.testeq1', [ extra_files(['FakePrelude.hs', 'Main.hs', 'TypeCast.hs', 'TypeEq.hs'])
                           , when(fast(), skip)
+                          , ignore_extension
                           , js_broken(22355)
                           # https://gitlab.haskell.org/ghc/ghc/-/issues/23238
                           , when(arch('wasm32'), skip)