[Git][ghc/ghc][wip/davide/io_manager_tests] Add test modifier to test over all IO Managers
David Eichmann pushed to branch wip/davide/io_manager_tests at Glasgow Haskell Compiler / GHC Commits: 254adb16 by David Eichmann at 2026-11-03T17:03:32+00:00 Add test modifier to test over all IO Managers - - - - - 7 changed files: - rts/RtsUtils.c - 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: ===================================== rts/RtsUtils.c ===================================== @@ -397,6 +397,7 @@ void printRtsInfo(const RtsConfig rts_config) { rts_config.rts_opts != NULL ? rts_config.rts_opts : ""); selectIOManager(); /* resolve the io-manager, accounting for flags */ mkRtsInfoPair("I/O manager default", showIOManager()); + mkRtsInfoPair("I/O managers", &IOMGRS_ENABLED_STR[1]); // skips the initial ' ' character in IOMGRS_ENABLED_STR printf(" ]\n"); } ===================================== testsuite/config/ghc ===================================== @@ -257,7 +257,9 @@ def get_compiler_info(): ) except Exception as err: - print("Exception thrown in testsuite/config/ghc.get_compiler_info: %s", err) + # We don't throw so that we can still discover tests before we've built + # GHC (i.e. config.compiler doesn't yet exist). + print("Exception thrown in testsuite/config/ghc.get_compiler_info: ", err) else: res = p.returncode == 0 @@ -315,6 +317,58 @@ 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 using `+RTS --info -RTS` + # and extract the "I/O managers" value from stdout. + def discover_io_managers_generic(extra_flags): + import tempfile + + test_src = 'main :: IO ()\nmain = return ()\n' + + res = [None] + + 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)) + try: + 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)) + res = list(discover_io_managers(exe, d)) + + except Exception as err: + # We don't throw so that we can still discover tests before we've built + # GHC (i.e. config.compiler doesn't yet exist). + print("Exception thrown in testsuite/config/ghc.discover_io_managers_generic: ", err) + + return res + + 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 @@ -367,6 +376,14 @@ class TestOptions: # add these ways to the default set self.extra_ways = [] # type: List[WayName] + # IO Managers to test (passed as the --io-manager RTS option) + # * [None] - only default IO Manager + # * [...] - list of io managers e.g. "mio" or "poll" + self.io_managers = [None] # type: List[Optional[str]] + + # IO Manager used in the current test case. Set before each test case. None means default IO Manager + self.io_manager_current = None # type: Optional[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 @@ -569,6 +569,34 @@ 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] ): + """ + Further split test cases into the given IO Managers if available. + Available IO Managers are determined by the test case's way (threaded/non-threads) and OS. + You'll likely want to use `extra_ways(['threaded2'])` if you want to test threaded IO Managers (e.g. mio). + """ + return lambda name, opts: _io_managers(name, opts, io_mgrs) + +def all_available_io_managers( name: TestName, opts ): + """ + Further split test cases into all available IO Managers. + Available IO Managers are determined by the test case's way (threaded/non-threads) and OS. + You'll likely want to use `extra_ways(['threaded2'])` to ensure threaded IO Managers are tested (e.g. mio). + """ + _io_managers(name, opts, config.io_managers_all) + +def all_available_io_managers_except(exclude: List[str]): + """ + Further split test cases into all available (except the given excluded) IO Managers. + Available IO Managers are determined by the test case's way (threaded/non-threads) and OS. + You'll likely want to use `extra_ways(['threaded2'])` to ensure threaded IO Managers are tested (e.g. mio). + """ + return lambda name, opts: _io_managers(name, opts, list(set(config.io_managers_all) - set(exclude))) # ----- @@ -1571,6 +1599,15 @@ do_not_copy = ('.hi', '.o', '.dyn_hi' , '.dyn_o', '.out' ,'.hi-boot', '.o-boot') # 12112 +def way_supports_io_manager(way: WayName, io_manager: Optional[str]) -> bool: + # All ways support a default IO Manager + if io_manager is None: + return True + + way_is_threaded = '-threaded' in config.way_flags[way] + available_io_managers = config.io_managers_threaded if way_is_threaded else config.io_managers_nonthreaded + return io_manager in available_io_managers + async def test_common_work(name: TestName, opts, func, args) -> None: try: @@ -1619,7 +1656,9 @@ async def test_common_work(name: TestName, opts, # 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) + if len(opts.io_managers) == 0: + framework_fail(name, None, 'opts.io_managers is empty') + t.total_test_cases += len(all_ways) * len(opts.io_managers) only_ways = getTestOpts().only_ways ok_way = lambda way: \ @@ -1634,6 +1673,18 @@ async def test_common_work(name: TestName, opts, # Which ways we are asked to skip do_ways = list(filter (ok_way,all_ways)) + # We split each way over available io managers + all_ways_and_io_managers = dict( + ( + way, + [io_manager + for io_manager in opts.io_managers + if way_supports_io_manager(way, io_manager) + ] + ) + for way in do_ways + ) + if config.accept: # For --accept, run the normal way and in each way that has a # way-specific expected output file (e.g. T20696.stderr-ext-interp). @@ -1702,19 +1753,23 @@ async def test_common_work(name: TestName, opts, # Run the required tests... for way in do_ways: - if stopping(): - break - start = time.monotonic() - 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(), - runtime=time.monotonic() - start) + for io_manager in all_ways_and_io_managers[way]: + if stopping(): + break + start = time.monotonic() + try: + getTestOpts().io_manager_current = io_manager + 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(), + runtime=time.monotonic() - start) + + 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: @@ -1741,6 +1796,8 @@ async def do_test(name: TestName, start = time.monotonic() full_name = name + '(' + way + ')' + if opts.io_manager_current is not None: + full_name += ' (IO Manager: {})'.format(opts.io_manager_current) test_n = len(allTestNames) progress_args = [ full_name, t.total_tests, test_n, [len(t.unexpected_passes), @@ -2140,7 +2197,7 @@ async def compile_grep_core(name: TestName, async def compile_and_run__(name: TestName, way: WayName, - top_mod: Path | None, + top_mod: Optional[Path], extra_mods: List[str], extra_hc_opts: str, backpack: bool=False, @@ -2423,6 +2480,9 @@ async def simple_run(name: TestName, way: WayName, prog: str, extra_run_opts: st my_rts_flags = rts_flags(way) + if opts.io_manager_current is not None: + my_rts_flags += ' +RTS --io-manager={0} -RTS '.format(opts.io_manager_current) + # Collect runtime stats if necessary: # isStatsTest and not isCompilerStatsTest(): # assume we are running a ghc compiled program. Collect stats. @@ -2448,6 +2508,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 opts.io_manager_current is not None: + available_io_managers = discover_io_managers(prog, opts.testdir) + if opts.io_manager_current 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(opts.io_manager_current, available_io_managers)) + # run the command exit_code = await runCmdPerf(opts.compiler_perf_counters, cmd, output_file, working_dir=opts.testdir, @@ -2509,7 +2575,7 @@ def rts_flags(way: WayName) -> str: async def interpreter_run(name: TestName, way: WayName, extra_hc_opts: str, - top_mod: Path | None + top_mod: Optional[Path] ) -> PassFail: opts = getTestOpts() ===================================== testsuite/driver/testutil.py ===================================== @@ -1,4 +1,5 @@ import os +import re import subprocess import shutil import tempfile @@ -179,3 +180,19 @@ 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: Union[str, Path], cwd: Union[str, Path]) -> Set[str]: + rp = subprocess.run( + [str(hs_exe), '+RTS', '--info', '-RTS'], + cwd=cwd, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL) + stdout = rp.stdout.decode('utf-8', errors='replace') + + m = re.search(r'\("I/O managers", "(.*)"\)', stdout) + if not m: + raise Exception('Expected stdout output of form \'... ("I/O managers", "select selectbis poll") ...\' but got:\n{}'.format(stdout)) + + return set(m.group(1).split()) ===================================== testsuite/tests/rts/all.T ===================================== @@ -674,8 +674,9 @@ 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')], - compile_and_run, ['']) + pre_cmd('$MAKE -s --no-print-directory IOManager.hs'), + extra_ways(['threaded2']), all_available_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/-/commit/254adb16b575fc1959aa4e05527f752a... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/254adb16b575fc1959aa4e05527f752a... 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
participants (1)
-
David Eichmann (@DavidEichmann)