David Eichmann pushed to branch wip/davide/io_manager_tests at Glasgow Haskell Compiler / GHC
Commits:
-
fe436a93
by David Eichmann at 2026-11-04T13:48:12+00:00
9 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/cmm/should_run/all.T
- testsuite/tests/primops/should_run/all.T
- testsuite/tests/rts/all.T
Changes:
| ... | ... | @@ -397,6 +397,7 @@ void printRtsInfo(const RtsConfig rts_config) { |
| 397 | 397 | rts_config.rts_opts != NULL ? rts_config.rts_opts : "");
|
| 398 | 398 | selectIOManager(); /* resolve the io-manager, accounting for flags */
|
| 399 | 399 | mkRtsInfoPair("I/O manager default", showIOManager());
|
| 400 | + mkRtsInfoPair("I/O managers", &IOMGRS_ENABLED_STR[1]); // skips the initial ' ' character in IOMGRS_ENABLED_STR
|
|
| 400 | 401 | printf(" ]\n");
|
| 401 | 402 | }
|
| 402 | 403 |
| ... | ... | @@ -257,7 +257,9 @@ def get_compiler_info(): |
| 257 | 257 | )
|
| 258 | 258 | |
| 259 | 259 | except Exception as err:
|
| 260 | - print("Exception thrown in testsuite/config/ghc.get_compiler_info: %s", err)
|
|
| 260 | + # We don't throw so that we can still discover tests before we've built
|
|
| 261 | + # GHC (i.e. config.compiler doesn't yet exist).
|
|
| 262 | + print("Exception thrown in testsuite/config/ghc.get_compiler_info: ", err)
|
|
| 261 | 263 | |
| 262 | 264 | else:
|
| 263 | 265 | res = p.returncode == 0
|
| ... | ... | @@ -315,6 +317,58 @@ def get_compiler_info(): |
| 315 | 317 | config.ghc_th_way = "normal"
|
| 316 | 318 | config.ghc_th_hi_ext = "hi"
|
| 317 | 319 | |
| 320 | + # I/O manager discovery.
|
|
| 321 | + #
|
|
| 322 | + # Which I/O managers (GHC's `--io-manager` RTS flag) a given RTS build
|
|
| 323 | + # supports depends on the target platform and on whether the RTS is the
|
|
| 324 | + # threaded or non-threaded flavour (see rts/IOManager.h's
|
|
| 325 | + # IOMGR_ENABLED_* macros). Rather than duplicate that platform/way
|
|
| 326 | + # dependent logic here, we ask the RTS itself using `+RTS --info -RTS`
|
|
| 327 | + # and extract the "I/O managers" value from stdout.
|
|
| 328 | + def discover_io_managers_generic(extra_flags):
|
|
| 329 | + import tempfile
|
|
| 330 | + |
|
| 331 | + test_src = 'main :: IO ()\nmain = return ()\n'
|
|
| 332 | + |
|
| 333 | + res = [None]
|
|
| 334 | + |
|
| 335 | + with tempfile.TemporaryDirectory() as d:
|
|
| 336 | + src = Path(d) / 'IOMgrProbe.hs'
|
|
| 337 | + src.write_text(test_src)
|
|
| 338 | + exe = Path(d) / ('IoMgrProbe' + config.exeext)
|
|
| 339 | + |
|
| 340 | + # -rtsopts is needed so that the resulting binary will even
|
|
| 341 | + # consider an --io-manager= flag (rather than immediately
|
|
| 342 | + # rejecting all "unsafe" RTS options).
|
|
| 343 | + cmd = '{} -v0 -rtsopts -o {} {} {}'.format(
|
|
| 344 | + config.compiler,
|
|
| 345 | + exe,
|
|
| 346 | + src,
|
|
| 347 | + ' '.join(extra_flags))
|
|
| 348 | + try:
|
|
| 349 | + cp = subprocess.run(
|
|
| 350 | + cmd,
|
|
| 351 | + shell=True,
|
|
| 352 | + cwd=d,
|
|
| 353 | + stdout=subprocess.DEVNULL,
|
|
| 354 | + stderr=None if config.verbose >= 3 else subprocess.DEVNULL)
|
|
| 355 | + if cp.returncode != 0 or not exe.exists():
|
|
| 356 | + raise Exception("Error in testsuite/config/ghc.discover_io_managers: failed to compile with: {}".format(cmd))
|
|
| 357 | + res = list(discover_io_managers(exe, d))
|
|
| 358 | + |
|
| 359 | + except Exception as err:
|
|
| 360 | + # We don't throw so that we can still discover tests before we've built
|
|
| 361 | + # GHC (i.e. config.compiler doesn't yet exist).
|
|
| 362 | + print("Exception thrown in testsuite/config/ghc.discover_io_managers_generic: ", err)
|
|
| 363 | + |
|
| 364 | + return res
|
|
| 365 | + |
|
| 366 | + config.io_managers_nonthreaded = discover_io_managers_generic([])
|
|
| 367 | + config.io_managers_threaded = (
|
|
| 368 | + discover_io_managers_generic(['-threaded']) if config.ghc_with_threaded_rts
|
|
| 369 | + else [])
|
|
| 370 | + config.io_managers_all = list(set(config.io_managers_nonthreaded).union(set(config.io_managers_threaded)))
|
|
| 371 | + |
|
| 318 | 372 | config.cmm_cpp_is_gcc = test_compile(
|
| 319 | 373 | ['-c'],
|
| 320 | 374 | test_filename='test.cmm',
|
| ... | ... | @@ -28,7 +28,7 @@ import subprocess |
| 28 | 28 | |
| 29 | 29 | import asyncio
|
| 30 | 30 | |
| 31 | -from testutil import getStdout, str_warn, str_info, print_table, shorten_metric_name, str_removeprefix
|
|
| 31 | +from testutil import discover_io_managers, getStdout, str_warn, str_info, print_table, shorten_metric_name, str_removeprefix
|
|
| 32 | 32 | from testglobals import getConfig, ghc_env, TestConfig, t, \
|
| 33 | 33 | TestOptions, brokens, PerfMetric
|
| 34 | 34 | from my_typing import TestName
|
| ... | ... | @@ -126,6 +126,15 @@ class TestConfig: |
| 126 | 126 | self.way_flags = {} # type: Dict[WayName, List[str]]
|
| 127 | 127 | self.way_rts_flags = {} # type: Dict[WayName, List[str]]
|
| 128 | 128 | |
| 129 | + # I/O managers (GHC's `--io-manager` RTS flag) supported by the threaded runtime.
|
|
| 130 | + self.io_managers_threaded = [] # type: List[str]
|
|
| 131 | + |
|
| 132 | + # I/O managers (GHC's `--io-manager` RTS flag) supported by the non-threaded runtime.
|
|
| 133 | + self.io_managers_nonthreaded = [] # type: List[str]
|
|
| 134 | + |
|
| 135 | + # All I/O managers (GHC's `--io-manager` RTS flag)
|
|
| 136 | + self.io_managers_all = [] # type: List[str]
|
|
| 137 | + |
|
| 129 | 138 | # Do we have a functional LLVM toolchain?
|
| 130 | 139 | self.have_llvm = False
|
| 131 | 140 | |
| ... | ... | @@ -367,6 +376,14 @@ class TestOptions: |
| 367 | 376 | # add these ways to the default set
|
| 368 | 377 | self.extra_ways = [] # type: List[WayName]
|
| 369 | 378 | |
| 379 | + # IO Managers to test (passed as the --io-manager RTS option)
|
|
| 380 | + # * [None] - only default IO Manager
|
|
| 381 | + # * [...] - list of io managers e.g. "mio" or "poll"
|
|
| 382 | + self.io_managers = [None] # type: List[Optional[str]]
|
|
| 383 | + |
|
| 384 | + # IO Manager used in the current test case. Set before each test case. None means default IO Manager
|
|
| 385 | + self.io_manager_current = None # type: Optional[str]
|
|
| 386 | + |
|
| 370 | 387 | # the result we normally expect for this test
|
| 371 | 388 | self.expect = 'pass'
|
| 372 | 389 |
| ... | ... | @@ -23,7 +23,7 @@ import subprocess |
| 23 | 23 | |
| 24 | 24 | from testglobals import config, ghc_env, default_testopts, brokens, t, \
|
| 25 | 25 | TestRun, TestResult, TestOptions, PerfMetric
|
| 26 | -from testutil import strip_quotes, lndir, link_or_copy_file, passed, \
|
|
| 26 | +from testutil import discover_io_managers, strip_quotes, lndir, link_or_copy_file, passed, \
|
|
| 27 | 27 | failBecause, residency_testing_metrics, \
|
| 28 | 28 | stable_perf_counters, \
|
| 29 | 29 | PassFail, badResult, str_warn, str_removeprefix
|
| ... | ... | @@ -565,10 +565,38 @@ def valid_way( way: WayName ) -> bool: |
| 565 | 565 | def extra_ways( ways: List[WayName] ):
|
| 566 | 566 | def helper( name: TestName, opts ):
|
| 567 | 567 | _lint_ways(name, ways)
|
| 568 | - opts.extra_ways = [way for way in ways if valid_way(way)]
|
|
| 568 | + opts.extra_ways += [way for way in ways if valid_way(way) and way not in opts.extra_ways]
|
|
| 569 | 569 | |
| 570 | 570 | return helper
|
| 571 | 571 | |
| 572 | +# -----
|
|
| 573 | + |
|
| 574 | +def _io_managers( name: TestName, opts, io_mgrs: List[str] ) -> None:
|
|
| 575 | + opts.io_managers = io_mgrs
|
|
| 576 | + |
|
| 577 | +def io_managers( io_mgrs: List[str] ):
|
|
| 578 | + """
|
|
| 579 | + Further split test cases into the given IO Managers if available.
|
|
| 580 | + Available IO Managers are determined by the test case's way (threaded/non-threads) and OS.
|
|
| 581 | + You'll likely want to use `extra_ways(['threaded2'])` if you want to test threaded IO Managers (e.g. mio).
|
|
| 582 | + """
|
|
| 583 | + return lambda name, opts: _io_managers(name, opts, io_mgrs)
|
|
| 584 | + |
|
| 585 | +def all_available_io_managers( name: TestName, opts ):
|
|
| 586 | + """
|
|
| 587 | + Further split test cases into all available IO Managers.
|
|
| 588 | + Available IO Managers are determined by the test case's way (threaded/non-threads) and OS.
|
|
| 589 | + You'll likely want to use `extra_ways(['threaded2'])` to ensure threaded IO Managers are tested (e.g. mio).
|
|
| 590 | + """
|
|
| 591 | + _io_managers(name, opts, config.io_managers_all)
|
|
| 592 | + |
|
| 593 | +def all_available_io_managers_except(exclude: List[str]):
|
|
| 594 | + """
|
|
| 595 | + Further split test cases into all available (except the given excluded) IO Managers.
|
|
| 596 | + Available IO Managers are determined by the test case's way (threaded/non-threads) and OS.
|
|
| 597 | + You'll likely want to use `extra_ways(['threaded2'])` to ensure threaded IO Managers are tested (e.g. mio).
|
|
| 598 | + """
|
|
| 599 | + return lambda name, opts: _io_managers(name, opts, list(set(config.io_managers_all) - set(exclude)))
|
|
| 572 | 600 | |
| 573 | 601 | # -----
|
| 574 | 602 | |
| ... | ... | @@ -1064,6 +1092,9 @@ def have_dynamic_prof( ) -> bool: |
| 1064 | 1092 | def have_profiling( ) -> bool:
|
| 1065 | 1093 | return config.have_profiling
|
| 1066 | 1094 | |
| 1095 | +def have_smp() -> bool:
|
|
| 1096 | + return config.ghc_has_smp
|
|
| 1097 | + |
|
| 1067 | 1098 | def have_threaded( ) -> bool:
|
| 1068 | 1099 | return config.ghc_with_threaded_rts
|
| 1069 | 1100 | |
| ... | ... | @@ -1571,6 +1602,15 @@ do_not_copy = ('.hi', '.o', '.dyn_hi' |
| 1571 | 1602 | , '.dyn_o', '.out'
|
| 1572 | 1603 | ,'.hi-boot', '.o-boot') # 12112
|
| 1573 | 1604 | |
| 1605 | +def way_supports_io_manager(way: WayName, io_manager: Optional[str]) -> bool:
|
|
| 1606 | + # All ways support a default IO Manager
|
|
| 1607 | + if io_manager is None:
|
|
| 1608 | + return True
|
|
| 1609 | + |
|
| 1610 | + way_is_threaded = '-threaded' in config.way_flags[way]
|
|
| 1611 | + available_io_managers = config.io_managers_threaded if way_is_threaded else config.io_managers_nonthreaded
|
|
| 1612 | + return io_manager in available_io_managers
|
|
| 1613 | + |
|
| 1574 | 1614 | async def test_common_work(name: TestName, opts,
|
| 1575 | 1615 | func, args) -> None:
|
| 1576 | 1616 | try:
|
| ... | ... | @@ -1619,7 +1659,22 @@ async def test_common_work(name: TestName, opts, |
| 1619 | 1659 | # A test itself can request extra ways by setting opts.extra_ways
|
| 1620 | 1660 | all_ways = list(OrderedDict.fromkeys(all_ways + [way for way in opts.extra_ways if way not in all_ways]))
|
| 1621 | 1661 | |
| 1622 | - t.total_test_cases += len(all_ways)
|
|
| 1662 | + # We split each way over available io managers
|
|
| 1663 | + if len(opts.io_managers) == 0:
|
|
| 1664 | + framework_fail(name, None, 'opts.io_managers is empty')
|
|
| 1665 | + all_ways_and_io_managers = dict(
|
|
| 1666 | + (
|
|
| 1667 | + way,
|
|
| 1668 | + [io_manager
|
|
| 1669 | + for io_manager in opts.io_managers
|
|
| 1670 | + if way_supports_io_manager(way, io_manager)
|
|
| 1671 | + ]
|
|
| 1672 | + )
|
|
| 1673 | + for way in all_ways
|
|
| 1674 | + )
|
|
| 1675 | + |
|
| 1676 | + for io_managers in all_ways_and_io_managers.values():
|
|
| 1677 | + t.total_test_cases += len(io_managers)
|
|
| 1623 | 1678 | |
| 1624 | 1679 | only_ways = getTestOpts().only_ways
|
| 1625 | 1680 | ok_way = lambda way: \
|
| ... | ... | @@ -1702,19 +1757,23 @@ async def test_common_work(name: TestName, opts, |
| 1702 | 1757 | |
| 1703 | 1758 | # Run the required tests...
|
| 1704 | 1759 | for way in do_ways:
|
| 1705 | - if stopping():
|
|
| 1706 | - break
|
|
| 1707 | - start = time.monotonic()
|
|
| 1708 | - try:
|
|
| 1709 | - await do_test(name, way, func, args, files)
|
|
| 1710 | - except KeyboardInterrupt:
|
|
| 1711 | - stopNow()
|
|
| 1712 | - except Exception as e:
|
|
| 1713 | - traceback.print_exc()
|
|
| 1714 | - framework_fail(name, way, traceback.format_exc(),
|
|
| 1715 | - runtime=time.monotonic() - start)
|
|
| 1760 | + for io_manager in all_ways_and_io_managers[way]:
|
|
| 1761 | + if stopping():
|
|
| 1762 | + break
|
|
| 1763 | + start = time.monotonic()
|
|
| 1764 | + try:
|
|
| 1765 | + getTestOpts().io_manager_current = io_manager
|
|
| 1766 | + await do_test(name ,way ,func ,args ,files)
|
|
| 1767 | + except KeyboardInterrupt:
|
|
| 1768 | + stopNow()
|
|
| 1769 | + except Exception as e:
|
|
| 1770 | + traceback.print_exc()
|
|
| 1771 | + framework_fail(name, way, traceback.format_exc(),
|
|
| 1772 | + runtime=time.monotonic() - start)
|
|
| 1773 | + |
|
| 1774 | + for skipped_way in set(all_ways) - set(do_ways):
|
|
| 1775 | + t.n_tests_skipped += len(all_ways_and_io_managers[skipped_way])
|
|
| 1716 | 1776 | |
| 1717 | - t.n_tests_skipped += len(set(all_ways) - set(do_ways))
|
|
| 1718 | 1777 | if getTestOpts().expect == 'missing-lib': t.n_missing_libs += 1
|
| 1719 | 1778 | |
| 1720 | 1779 | if config.cleanup and do_ways:
|
| ... | ... | @@ -1741,6 +1800,8 @@ async def do_test(name: TestName, |
| 1741 | 1800 | start = time.monotonic()
|
| 1742 | 1801 | |
| 1743 | 1802 | full_name = name + '(' + way + ')'
|
| 1803 | + if opts.io_manager_current is not None:
|
|
| 1804 | + full_name += ' (IO Manager: {})'.format(opts.io_manager_current)
|
|
| 1744 | 1805 | test_n = len(allTestNames)
|
| 1745 | 1806 | progress_args = [ full_name, t.total_tests, test_n,
|
| 1746 | 1807 | [len(t.unexpected_passes),
|
| ... | ... | @@ -2140,7 +2201,7 @@ async def compile_grep_core(name: TestName, |
| 2140 | 2201 | |
| 2141 | 2202 | async def compile_and_run__(name: TestName,
|
| 2142 | 2203 | way: WayName,
|
| 2143 | - top_mod: Path | None,
|
|
| 2204 | + top_mod: Optional[Path],
|
|
| 2144 | 2205 | extra_mods: List[str],
|
| 2145 | 2206 | extra_hc_opts: str,
|
| 2146 | 2207 | backpack: bool=False,
|
| ... | ... | @@ -2423,6 +2484,9 @@ async def simple_run(name: TestName, way: WayName, prog: str, extra_run_opts: st |
| 2423 | 2484 | |
| 2424 | 2485 | my_rts_flags = rts_flags(way)
|
| 2425 | 2486 | |
| 2487 | + if opts.io_manager_current is not None:
|
|
| 2488 | + my_rts_flags += ' +RTS --io-manager={0} -RTS '.format(opts.io_manager_current)
|
|
| 2489 | + |
|
| 2426 | 2490 | # Collect runtime stats if necessary:
|
| 2427 | 2491 | # isStatsTest and not isCompilerStatsTest():
|
| 2428 | 2492 | # assume we are running a ghc compiled program. Collect stats.
|
| ... | ... | @@ -2448,6 +2512,12 @@ async def simple_run(name: TestName, way: WayName, prog: str, extra_run_opts: st |
| 2448 | 2512 | |
| 2449 | 2513 | output_file = perfStatsFile(False, name)
|
| 2450 | 2514 | |
| 2515 | + # Check that IO Manager is available
|
|
| 2516 | + if opts.io_manager_current is not None:
|
|
| 2517 | + available_io_managers = discover_io_managers(prog, opts.testdir)
|
|
| 2518 | + if opts.io_manager_current not in available_io_managers:
|
|
| 2519 | + 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))
|
|
| 2520 | + |
|
| 2451 | 2521 | # run the command
|
| 2452 | 2522 | exit_code = await runCmdPerf(opts.compiler_perf_counters, cmd, output_file,
|
| 2453 | 2523 | working_dir=opts.testdir,
|
| ... | ... | @@ -2509,7 +2579,7 @@ def rts_flags(way: WayName) -> str: |
| 2509 | 2579 | async def interpreter_run(name: TestName,
|
| 2510 | 2580 | way: WayName,
|
| 2511 | 2581 | extra_hc_opts: str,
|
| 2512 | - top_mod: Path | None
|
|
| 2582 | + top_mod: Optional[Path]
|
|
| 2513 | 2583 | ) -> PassFail:
|
| 2514 | 2584 | opts = getTestOpts()
|
| 2515 | 2585 |
| 1 | 1 | import os
|
| 2 | +import re
|
|
| 2 | 3 | import subprocess
|
| 3 | 4 | import shutil
|
| 4 | 5 | import tempfile
|
| ... | ... | @@ -179,3 +180,19 @@ def shorten_metric_name(name: str) -> str: |
| 179 | 180 | "binary_size/if_compression(3)": "size/3",
|
| 180 | 181 | }
|
| 181 | 182 | return dic.get(name, name)
|
| 183 | + |
|
| 184 | +# I/O manager discovery for a haskell binary. Returns the set of IO Manager
|
|
| 185 | +# strings supported by the binary
|
|
| 186 | +def discover_io_managers(hs_exe: Union[str, Path], cwd: Union[str, Path]) -> Set[str]:
|
|
| 187 | + rp = subprocess.run(
|
|
| 188 | + [str(hs_exe), '+RTS', '--info', '-RTS'],
|
|
| 189 | + cwd=cwd,
|
|
| 190 | + stdout=subprocess.PIPE,
|
|
| 191 | + stderr=subprocess.DEVNULL)
|
|
| 192 | + stdout = rp.stdout.decode('utf-8', errors='replace')
|
|
| 193 | + |
|
| 194 | + m = re.search(r'\("I/O managers", "(.*)"\)', stdout)
|
|
| 195 | + if not m:
|
|
| 196 | + raise Exception('Expected stdout output of form \'... ("I/O managers", "select selectbis poll") ...\' but got:\n{}'.format(stdout))
|
|
| 197 | + |
|
| 198 | + return set(m.group(1).split()) |
| ... | ... | @@ -56,7 +56,7 @@ test('T25601', |
| 56 | 56 | test('T27447',
|
| 57 | 57 | [ req_cmm
|
| 58 | 58 | , extra_ways(['optasm'])
|
| 59 | - , when(have_llvm(), extra_ways(['optasm', 'optllvm']))
|
|
| 59 | + , when(have_llvm(), extra_ways(['optllvm']))
|
|
| 60 | 60 | ],
|
| 61 | 61 | multi_compile_and_run,
|
| 62 | 62 | ['T27447', [('T27447_cmm.cmm', '')], '']) |
| ... | ... | @@ -41,9 +41,9 @@ test('T14664', normal, compile_and_run, ['']) |
| 41 | 41 | test('CStringLength', normal, compile_and_run, ['-O2'])
|
| 42 | 42 | test('NonNativeSwitch', normal, compile_and_run, ['-O2'])
|
| 43 | 43 | test('Sized', [
|
| 44 | + extra_ways(['ghci','ghci-opt']),
|
|
| 44 | 45 | # optasm is broken with js backend: #25922
|
| 45 | - extra_ways(['optasm','ghci','ghci-opt']) if not js_arch()
|
|
| 46 | - else extra_ways(['ghci','ghci-opt'])
|
|
| 46 | + when(not js_arch(), extra_ways(['optasm']))
|
|
| 47 | 47 | ], compile_and_run, [''])
|
| 48 | 48 | |
| 49 | 49 | test('LevPolyPtrEquality1', normal, compile_and_run, [''])
|
| ... | ... | @@ -674,8 +674,10 @@ test('T23142', [unless(debug_rts(), skip), req_interp], makefile_test, ['T23142' |
| 674 | 674 | test('T23400', [], compile_and_run, ['-with-rtsopts -A8k'])
|
| 675 | 675 | |
| 676 | 676 | test('IOManager', [js_skip, when(arch('wasm32'), skip), when(opsys('mingw32'), skip),
|
| 677 | - pre_cmd('$MAKE -s --no-print-directory IOManager.hs')],
|
|
| 678 | - compile_and_run, [''])
|
|
| 677 | + pre_cmd('$MAKE -s --no-print-directory IOManager.hs'),
|
|
| 678 | + when(have_smp(), extra_ways(['threaded2'])),
|
|
| 679 | + all_available_io_managers_except('winio')],
|
|
| 680 | + compile_and_run, [''])
|
|
| 679 | 681 | |
| 680 | 682 | test('T24142', [req_target_smp], compile_and_run, ['-threaded -with-rtsopts "-N2"'])
|
| 681 | 683 |