David Eichmann pushed to branch wip/davide/io_manager_tests at Glasgow Haskell Compiler / GHC Commits: 92d7bd73 by GHC GitLab CI at 2026-09-21T14:16:59+01:00 testsuite: Fix python type errors - - - - - 5f079336 by GHC GitLab CI at 2026-09-21T17:17:47+01:00 Add test modifier to test over all IO Managers - - - - - 6 changed files: - testsuite/config/ghc - testsuite/driver/runtests.py - testsuite/driver/testglobals.py - testsuite/driver/testlib.py - testsuite/driver/testutil.py - testsuite/tests/rts/all.T Changes: ===================================== testsuite/config/ghc ===================================== @@ -315,6 +315,52 @@ def get_compiler_info(): config.ghc_th_way = "normal" config.ghc_th_hi_ext = "hi" + # I/O manager discovery. + # + # Which I/O managers (GHC's `--io-manager` RTS flag) a given RTS build + # supports depends on the target platform and on whether the RTS is the + # threaded or non-threaded flavour (see rts/IOManager.h's + # IOMGR_ENABLED_* macros). Rather than duplicate that platform/way + # dependent logic here, we ask the RTS itself: giving it a bogus + # '--io-manager=' choice makes it print, on stderr, the list of I/O + # managers it was actually built to support (see IOManager.h's + # IOMGRS_ENABLED_STR, used when reporting the error in RtsFlags.c), + # which we then parse out. + def discover_io_managers_generic(extra_flags): + import tempfile + + test_src = 'module Main where\nmain :: IO ()\nmain = return ()\n' + + with tempfile.TemporaryDirectory() as d: + src = Path(d) / 'IOMgrProbe.hs' + src.write_text(test_src) + exe = Path(d) / ('ioMgrProbe' + config.exeext) + + # -rtsopts is needed so that the resulting binary will even + # consider an --io-manager= flag (rather than immediately + # rejecting all "unsafe" RTS options). + cmd = '{} -v0 -rtsopts -o {} {} {}'.format( + config.compiler, + exe, + src, + ' '.join(extra_flags)) + cp = subprocess.run( + cmd, + shell=True, + cwd=d, + stdout=subprocess.DEVNULL, + stderr=None if config.verbose >= 3 else subprocess.DEVNULL) + if cp.returncode != 0 or not exe.exists(): + raise Exception("Error in testsuite/config/ghc.discover_io_managers: failed to compile with: {}".format(cmd)) + + return list(discover_io_managers(exe, d)) + + config.io_managers_nonthreaded = discover_io_managers_generic([]) + config.io_managers_threaded = ( + discover_io_managers_generic(['-threaded']) if config.ghc_with_threaded_rts + else []) + config.io_managers_all = list(set(config.io_managers_nonthreaded).union(set(config.io_managers_threaded))) + config.cmm_cpp_is_gcc = test_compile( ['-c'], test_filename='test.cmm', ===================================== testsuite/driver/runtests.py ===================================== @@ -28,7 +28,7 @@ import subprocess import asyncio -from testutil import getStdout, str_warn, str_info, print_table, shorten_metric_name, str_removeprefix +from testutil import discover_io_managers, getStdout, str_warn, str_info, print_table, shorten_metric_name, str_removeprefix from testglobals import getConfig, ghc_env, TestConfig, t, \ TestOptions, brokens, PerfMetric from my_typing import TestName ===================================== testsuite/driver/testglobals.py ===================================== @@ -126,6 +126,15 @@ class TestConfig: self.way_flags = {} # type: Dict[WayName, List[str]] self.way_rts_flags = {} # type: Dict[WayName, List[str]] + # I/O managers (GHC's `--io-manager` RTS flag) supported by the threaded runtime. + self.io_managers_threaded = [] # type: List[str] + + # I/O managers (GHC's `--io-manager` RTS flag) supported by the non-threaded runtime. + self.io_managers_nonthreaded = [] # type: List[str] + + # All I/O managers (GHC's `--io-manager` RTS flag) + self.io_managers_all = [] # type: List[str] + # Do we have a functional LLVM toolchain? self.have_llvm = False @@ -227,7 +236,7 @@ class TestConfig: self.allowed_perf_changes = {} # The test environment. - self.test_env = 'local' + self.test_env = TestEnv('local') # terminal supports colors self.supports_colors = False @@ -366,6 +375,11 @@ class TestOptions: # add these ways to the default set self.extra_ways = [] # type: List[WayName] + # IO Managers to test + # * None - only default IO Manager + # * [...] - list of io managers e.g. "mio" or "poll" + self.io_managers = [] # type: None | List[str] + # the result we normally expect for this test self.expect = 'pass' ===================================== testsuite/driver/testlib.py ===================================== @@ -23,7 +23,7 @@ import subprocess from testglobals import config, ghc_env, default_testopts, brokens, t, \ TestRun, TestResult, TestOptions, PerfMetric -from testutil import strip_quotes, lndir, link_or_copy_file, passed, \ +from testutil import discover_io_managers, strip_quotes, lndir, link_or_copy_file, passed, \ failBecause, residency_testing_metrics, \ stable_perf_counters, \ PassFail, badResult, str_warn, str_removeprefix @@ -190,7 +190,7 @@ def no_lint( name, opts ): def stage1(name, opts): # See Note [Why is there no stage1 setup function?] - framework_fail(name, 'stage1 setup function does not exist', + framework_fail(name, None, 'add your test to testsuite/tests/stage1 instead') # Note [Why is there no stage1 setup function?] @@ -341,7 +341,7 @@ def req_th( name, opts ): req_interp(name, opts); if ghc_dynamic(): - return _omit_ways(name, opts, ['profasm', 'profthreaded']) + return _omit_ways(name, opts, [WayName('profasm'), WayName('profthreaded')]) def req_plugins( name, opts ): """ @@ -355,7 +355,7 @@ def req_plugins( name, opts ): req_interp(name, opts) # Plugins aren't supported with the external interpreter (#14335) - expect_broken_for(14335,['ext-interp'])(name,opts) + expect_broken_for(IssueNumber(14335),[WayName('ext-interp')])(name,opts) if config.cross: opts.skip = True @@ -569,6 +569,33 @@ def extra_ways( ways: List[WayName] ): return helper +# ----- + +def _io_managers( name: TestName, opts, io_mgrs: List[str] ) -> None: + opts.io_managers = io_mgrs + +def io_managers( io_mgrs: List[str] ): + """ + Run this test once for each of the given I/O managers (identified by + the name used with GHC's `--io-manager` RTS flag, e.g. 'mio', + 'select', 'winio'), instead of the usual set of ways. I/O managers + that are not supported on the current platform/build are silently + skipped. + """ + return lambda name, opts: _io_managers(name, opts, io_mgrs) + +def all_io_managers( name: TestName, opts ): + """ + Run this test once for each I/O manager supported by this + platform/build. + """ + _io_managers(name, opts, config.io_managers_all) + +def all_io_managers_except(exclude: List[str]): + """ + Like all_io_managers, but excluding the given I/O managers. + """ + return lambda name, opts: _io_managers(name, opts, list(set(config.io_managers_all) - set(exclude))) # ----- @@ -661,18 +688,15 @@ def collect_size ( deviation, path ): def collect_size_func ( deviation, path_func ): return collect_generic_stat ( 'size', deviation, lambda way: os.path.getsize(in_testdir(path_func())) ) -def get_dir_size(path): +def get_dir_size(path) -> int: total = 0 - try: - with os.scandir(path) as it: - for entry in it: - if entry.is_file(): - total += entry.stat().st_size - elif entry.is_dir(): - total += get_dir_size(entry.path) - return total - except FileNotFoundError: - print("Exception: Could not find: " + path) + with os.scandir(path) as it: + for entry in it: + if entry.is_file(): + total += entry.stat().st_size + elif entry.is_dir(): + total += get_dir_size(entry.path) + return total def collect_size_dir ( deviation, path ): return collect_size_dir_func ( deviation, lambda: path ) @@ -790,7 +814,7 @@ def find_non_inplace_so(lib): return _find_so(lib,path_from_ghcPkg(lib, "dynamic-library-dirs"),False) -def collect_generic_stat ( metric, deviation: Optional[int], get_stat: Callable[[WayName], str]): +def collect_generic_stat ( metric, deviation: Optional[int], get_stat: Callable[[WayName], Any]): """ Define a generic stat test, which computes the statistic by calling the function given as the third argument. @@ -1135,7 +1159,7 @@ def _collect_compiler_perf_counters(counters: Set[str], deviation: Optional[int] # Slightly hacky, we need the requested perf_counters in 'simple_run'. # Thus, we have to globally register these counters opts.compiler_perf_counters += list(counters) - _collect_stats(name, opts, counters, deviation, False, True, True) + _collect_stats(name, opts, counters, deviation, None, True, True) return f @@ -1334,21 +1358,22 @@ def check_errmsg(needle): # grep_errmsg(regex,[groups]) # If groups are given, return only the matched groups # that matches the regex. -def grep_errmsg_norm(needle:str, groups = None): +def grep_errmsg_norm(needle:str, groups_may = None): - def get_match(str:str): - m = re.search(needle,str) - if m: - return "".join([m.group(g) for g in groups if m.group(g) is not None]) - else: - return None - - def norm(str) -> str: - if groups == None: + def norm(string) -> str: + if groups_may == None: return "".join( filter(lambda l: re.search(needle,l), - str.splitlines(True))) + string.splitlines(True))) else: - matches = [get_match(x) for x in str.splitlines(True)] + groups = groups_may + def get_match(string: str): + m = re.search(needle, string) + if m: + return "".join([m.group(g) for g in groups if m.group(g) is not None]) + else: + return None + + matches = [get_match(x) for x in string.splitlines(True)] res = "\n".join([x for x in matches if x]) return res return norm @@ -1573,6 +1598,12 @@ do_not_copy = ('.hi', '.o', '.dyn_hi' , '.dyn_o', '.out' ,'.hi-boot', '.o-boot') # 12112 +def way_supports_io_manager(way: WayName, io_manager: None | str) -> bool: + if not io_manager: + return True + way_is_threaded = '-threaded' in config.way_rts_flags[way] + return io_manager in config.io_managers_threaded if way_is_threaded else io_manager in config.io_managers_nonthreaded + async def test_common_work(name: TestName, opts, func, args) -> None: try: @@ -1582,11 +1613,14 @@ async def test_common_work(name: TestName, opts, package_conf_cache_file_start_timestamp = get_package_cache_timestamp() # All the ways we might run this test + func_has_io_manager_param = False if func == compile or func == multimod_compile: all_ways = config.compile_ways elif func in [compile_and_run, multi_compile_and_run, multimod_compile_and_run]: all_ways = config.run_ways + func_has_io_manager_param = True elif func == ghci_script or func == ghci_multiunit_script: + func_has_io_manager_param = True if config.have_interp: all_ways = [WayName('ghci'), WayName('ghci-opt')] else: @@ -1616,12 +1650,28 @@ async def test_common_work(name: TestName, opts, if needsTargetWrapper(): opts.skip = True else: + if func in [backpack_run, warn_and_run]: + func_has_io_manager_param = True all_ways = [WayName('normal')] # A test itself can request extra ways by setting opts.extra_ways all_ways = list(OrderedDict.fromkeys(all_ways + [way for way in opts.extra_ways if way not in all_ways])) - t.total_test_cases += len(all_ways) + # We run each way over each io manager (or just None meaning the default IO Manager) + all_io_managers = opts.io_managers if func_has_io_manager_param and opts.io_managers else [None] + all_ways_and_io_managers = dict( + ( + way, + [io_manager + for io_manager in all_io_managers + if way_supports_io_manager(way, io_manager) + ] + ) + for way in all_ways + ) + + for io_managers in all_ways_and_io_managers.values(): + t.total_test_cases += len(io_managers) only_ways = getTestOpts().only_ways ok_way = lambda way: \ @@ -1704,17 +1754,26 @@ async def test_common_work(name: TestName, opts, # Run the required tests... for way in do_ways: - if stopping(): - break - try: - await do_test(name, way, func, args, files) - except KeyboardInterrupt: - stopNow() - except Exception as e: - traceback.print_exc() - framework_fail(name, way, traceback.format_exc()) + for io_manager in all_ways_and_io_managers[way]: + if stopping(): + break + try: + await do_test( + name, + way, + io_manager, + func, + args, + files) + except KeyboardInterrupt: + stopNow() + except Exception as e: + traceback.print_exc() + framework_fail(name, way, traceback.format_exc()) + + for skipped_way in set(all_ways) - set(do_ways): + t.n_tests_skipped += len(all_ways_and_io_managers[skipped_way]) - t.n_tests_skipped += len(set(all_ways) - set(do_ways)) if getTestOpts().expect == 'missing-lib': t.n_missing_libs += 1 if config.cleanup and do_ways: @@ -1733,6 +1792,7 @@ async def test_common_work(name: TestName, opts, async def do_test(name: TestName, way: WayName, + io_manager: None | str, func: Callable[..., Awaitable[PassFail]], args, files: Set[str] @@ -1740,6 +1800,8 @@ async def do_test(name: TestName, opts = getTestOpts() full_name = name + '(' + way + ')' + if io_manager: + full_name += ' (IO Manager: {})'.format(io_manager) test_n = len(allTestNames) progress_args = [ full_name, t.total_tests, test_n, [len(t.unexpected_passes), @@ -1820,7 +1882,20 @@ async def do_test(name: TestName, # Don't continue and try to run the test if the pre_cmd fails. return - result = await func(*[name,way] + args) + + func_has_io_manager_param = func in [ + backpack_run, + compile_and_run, + ghci_multiunit_script, + ghci_script, + multi_compile_and_run, + multimod_compile_and_run, + warn_and_run + ] + + io_manager_arg = [io_manager] if func_has_io_manager_param else [] + + result = await func(*[name,way] + io_manager_arg + args) if opts.expect not in ['pass', 'fail', 'missing-lib']: framework_fail(name, way, 'bad expected ' + opts.expect) @@ -1909,7 +1984,7 @@ def framework_warn(name: TestName, way: WayName, reason: str) -> None: # run_command. async def run_command( name, way, cmd ): - return await simple_run( name, '', override_options(cmd), '' ) + return await simple_run( name, WayName(''), None, override_options(cmd), '' ) async def makefile_test( name, way, target=None ): if target is None: @@ -1921,7 +1996,7 @@ async def makefile_test( name, way, target=None ): # ----------------------------------------------------------------------------- # GHCi tests -async def ghci_script( name, way, script): +async def ghci_script( name, way, io_manager, script): flags = ' '.join(get_compiler_flags()) way_flags = ' '.join(config.way_flags[way]) @@ -1932,9 +2007,9 @@ async def ghci_script( name, way, script): # NB: put way_flags before flags so that flags in all.T can override others getTestOpts().stdin = script - return await simple_run( name, way, cmd, getTestOpts().extra_run_opts ) + return await simple_run( name, way, io_manager, cmd, getTestOpts().extra_run_opts ) -async def ghci_multiunit_script( name, way, units, script): +async def ghci_multiunit_script( name, way, io_manager, units, script): flags = ' '.join(get_compiler_flags()) way_flags = ' '.join(config.way_flags[way]) unit_flags = ' '.join(['-unit @%s' % unit for unit in units]) @@ -1946,7 +2021,7 @@ async def ghci_multiunit_script( name, way, units, script): # NB: put way_flags before flags so that flags in all.T can override others getTestOpts().stdin = script - return await simple_run( name, way, cmd, getTestOpts().extra_run_opts ) + return await simple_run( name, way, io_manager, cmd, getTestOpts().extra_run_opts ) # ----------------------------------------------------------------------------- # Compile-only tests @@ -1995,7 +2070,7 @@ async def multi_compile_fail( name, way, top_mod, extra_mods, extra_hc_opts ): return await do_compile( name, way, True, top_mod, extra_mods, [], extra_hc_opts) async def make_depend( name, way, mods, extra_hc_opts ): - return await do_compile( name, way, False, ' '.join(mods), [], [], extra_hc_opts, mode = '-M') + return await do_compile( name, way, False, Path(' '.join(mods)), [], [], extra_hc_opts, mode = '-M') async def do_compile(name: TestName, way: WayName, @@ -2126,7 +2201,8 @@ async def compile_grep_core(name: TestName, async def compile_and_run__(name: TestName, way: WayName, - top_mod: Path, + io_manager: None | str, + top_mod: Path | None, extra_mods: List[str], extra_hc_opts: str, backpack: bool=False, @@ -2168,22 +2244,22 @@ async def compile_and_run__(name: TestName, 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 ) + return await simple_run( name, way, io_manager, cmd, getTestOpts().extra_run_opts ) -async def compile_and_run( name, way, extra_hc_opts ): - return await compile_and_run__( name, way, None, [], extra_hc_opts) +async def compile_and_run( name, way, io_manager, extra_hc_opts ): + return await compile_and_run__( name, way, io_manager, 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 backpack_run( name, way, io_manager, extra_hc_opts ): + return await compile_and_run__( name, way, io_manager, 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) +async def multimod_compile_and_run( name, way, io_manager, top_mod, extra_hc_opts ): + return await compile_and_run__( name, way, io_manager, top_mod, [], extra_hc_opts) -async def multi_compile_and_run( name, way, top_mod, extra_mods, extra_hc_opts ): - return await compile_and_run__( name, way, top_mod, extra_mods, extra_hc_opts) +async def multi_compile_and_run( name, way, io_manager, top_mod, extra_mods, extra_hc_opts ): + return await compile_and_run__( name, way, io_manager, top_mod, extra_mods, extra_hc_opts) -async def warn_and_run( name, way, extra_hc_opts ): - return await compile_and_run__( name, way, None, [], extra_hc_opts, compile_stderr = True) +async def warn_and_run( name, way, io_manager, extra_hc_opts ): + return await compile_and_run__( name, way, io_manager, None, [], extra_hc_opts, compile_stderr = True) async def static_stats( name, way ): opts = getTestOpts() @@ -2390,7 +2466,7 @@ async def simple_build(name: Union[TestName, str], # from /dev/null. Route output to testname.run.stdout and # testname.run.stderr. Returns the exit code of the run. -async def simple_run(name: TestName, way: WayName, prog: str, extra_run_opts: str) -> Any: +async def simple_run(name: TestName, way: WayName, io_manager: None | str, prog: str, extra_run_opts: str) -> Any: opts = getTestOpts() # figure out what to use for stdin @@ -2409,6 +2485,9 @@ async def simple_run(name: TestName, way: WayName, prog: str, extra_run_opts: st my_rts_flags = rts_flags(way) + if io_manager: + my_rts_flags += '+RTS --io-manager={0} -RTS'.format(io_manager) + # Collect runtime stats if necessary: # isStatsTest and not isCompilerStatsTest(): # assume we are running a ghc compiled program. Collect stats. @@ -2434,6 +2513,12 @@ async def simple_run(name: TestName, way: WayName, prog: str, extra_run_opts: st output_file = perfStatsFile(False, name) + # Check that IO Manager is available + if io_manager: + available_io_managers = discover_io_managers(prog, opts.testdir) + if io_manager not in available_io_managers: + framework_fail(name, way, 'IO Manager {} unexpectedly unavailable (available IO Managers: {}). `-non-threaded`/`-threaded` is only supported through way RTS flags (not manually added via test modifiers/args) when testing over IO Managers.'.format(io_manager, available_io_managers)) + # run the command exit_code = await runCmdPerf(opts.compiler_perf_counters, cmd, output_file, working_dir=opts.testdir, @@ -2495,7 +2580,7 @@ def rts_flags(way: WayName) -> str: async def interpreter_run(name: TestName, way: WayName, extra_hc_opts: str, - top_mod: Path + top_mod: Path | None ) -> PassFail: opts = getTestOpts() @@ -2866,11 +2951,11 @@ async def compare_outputs( if config.accept_platform: if_verbose(1, 'Accepting new output for platform "' + config.platform + '".') - expected_path += '-' + config.platform + expected_path = Path(str(expected_path) + '-' + config.platform) elif config.accept_os: if_verbose(1, 'Accepting new output for os "' + config.os + '".') - expected_path += '-' + config.os + expected_path = Path(str(expected_path) + '-' + config.os) else: if_verbose(1, 'Accepting new output.') @@ -3500,7 +3585,7 @@ def find_expected_file(name: TestName, suff: str, way: WayName) -> Path: for f in files: if in_srcdir(f).exists(): - return f + return Path(f) return basename ===================================== testsuite/driver/testutil.py ===================================== @@ -1,4 +1,5 @@ import os +import re import subprocess import shutil import tempfile @@ -179,3 +180,21 @@ def shorten_metric_name(name: str) -> str: "binary_size/if_compression(3)": "size/3", } return dic.get(name, name) + +# I/O manager discovery for a haskell binary. Returns the set of IO Manager +# strings supported by the binary +def discover_io_managers(hs_exe: str | Path, cwd: str | Path) -> Set[str]: + rp = subprocess.run( + [str(hs_exe), '+RTS', '--io-manager=not-a-real-io-manager', '-RTS'], + cwd=cwd, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE) + stderr = rp.stderr.decode('utf-8', errors='replace') + + m = re.search(r'choices are:\s*(.*)', stderr) + if not m: + raise Exception("Expected stderr output of form 'choices are: ...' but got: {}".format(stderr)) + + # 'auto' is a meta-choice (let the RTS pick a default for us), not + # an I/O manager implementation in its own right. + return set(m.group(1).split()) - {'auto'} ===================================== testsuite/tests/rts/all.T ===================================== @@ -661,8 +661,8 @@ test('T23142', [unless(debug_rts(), skip), req_interp], makefile_test, ['T23142' test('T23400', [], compile_and_run, ['-with-rtsopts -A8k']) -test('IOManager', [js_skip, when(arch('wasm32'), skip), when(opsys('mingw32'), skip), - pre_cmd('$MAKE -s --no-print-directory IOManager.hs')], +test('IOManager', [js_skip, when(arch('wasm32'), skip), + pre_cmd('$MAKE -s --no-print-directory IOManager.hs'), all_io_managers_except('winio')], compile_and_run, ['']) test('T24142', [req_target_smp], compile_and_run, ['-threaded -with-rtsopts "-N2"']) View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/b876112e0b2239b3387c04c61ad4359... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/b876112e0b2239b3387c04c61ad4359... You're receiving this email because of your account on gitlab.haskell.org. Manage all notifications: https://gitlab.haskell.org/-/profile/notifications | Help: https://gitlab.haskell.org/help