diff --git a/testsuite/tests/ghci-browser/all.T b/testsuite/tests/ghci-browser/all.T new file mode 100644 index 0000000000000000000000000000000000000000..243d8acbcea2a37f863996308a35db589d3bb912 --- /dev/null +++ b/testsuite/tests/ghci-browser/all.T @@ -0,0 +1,134 @@ +# Note [Testing wasm ghci browser mode] +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# Wasm ghci browser mode supports connecting with a user-controlled +# browser tab or an automated headless browser tab. The wasm dyld +# script supports using puppeteer/playwright to launch the headless +# browser, and this can be used either interactively by the user, or +# fully automatically when running the GHC testsuite. +# +# The first issue is deciding whether to run these tests at all, even +# when we're indeed testing the wasm32 target! The user may be using +# nodejs without the optional npm dependencies needed to make browser +# mode works, and it's non-trivial to probe browser installations and +# infer the right puppeteer/playwright launch options. +# +# We must ensure the testsuite is still green when browser mode isn't +# supported, therefore we don't do anything fancy here. These browser +# tests are opt-in, the user must explicitly set environment variables +# like FIREFOX_LAUNCH_OPTS to test a specific browser. These variables +# are *not* handled by the wasm dyld script to avoid confusion, the +# test runner is in charge of setting the right flags to invoke ghci. +# +# The next issue is #25415. The testsuite driver has more than a dozen +# different functions to "run something", some of which inserts a +# target wrapper, some of which doesn't, all without knowing if the +# thing to be run is a host or target thing! For instance, attempting +# to define these tests as ghci_script would result in the driver +# attempting to launch host ghci using the wasm interpreter. The same +# issue exists in many places other than ghci_script, and while the +# situation ought to be fixed, that is undoubtedly scope creep that +# belongs to a standalone chunk of work. +# +# To ensure wasm ghci browser mode is tested in CI without further +# complicating the testsuite driver, I choose to avoid any +# modifications to the driver and global config. So it's best to just +# define our own ghci_browser test function solely in this file, it +# does its job well for our rather specific use case. + +setTestOpts( + [ + unless(arch("wasm32"), skip), + high_memory_usage, + only_ways(["ghci", "ghci-opt"]), + extra_ways(["ghci", "ghci-opt"]), + ] +) + + +# you can pass more arguments here; the additional argument list is +# the last argument of test() calls. see do_test function in the +# driver and how it invokes the func argument for details. +async def ghci_browser(name_with_browser, way): + nb = name_with_browser.rsplit("-", 1) + name = nb[0] + browser = nb[1] + + opts = getTestOpts() + + way_flags = " ".join(config.way_flags[way]) + flags = " ".join(get_compiler_flags()) + + if browser == "firefox": + o = ghc_env["FIREFOX_LAUNCH_OPTS"] + env_flags = f"GHCI_BROWSER=1 GHCI_BROWSER_PUPPETEER_LAUNCH_OPTS='{o}'" + elif browser == "chrome": + o = ghc_env["CHROME_LAUNCH_OPTS"] + env_flags = f"GHCI_BROWSER=1 GHCI_BROWSER_PUPPETEER_LAUNCH_OPTS='{o}'" + else: + assert browser == "webkit" + o = ghc_env["WEBKIT_LAUNCH_OPTS"] + env_flags = f"GHCI_BROWSER=1 GHCI_BROWSER_PLAYWRIGHT_BROWSER_TYPE=webkit GHCI_BROWSER_PLAYWRIGHT_LAUNCH_OPTS='{o}'" + + # ghci_script formats the command then calls simple_run. simple_run + # formats the command then calls runCmdPerf. runCmdPerf formats the + # command and calls runCmd. runCmd formats the command then calls + # the timeout program that calls /bin/sh which does another layer of + # formatting. good luck making your command containing a json + # payload to survive how many layers of pure insanity. + cmd = ( + f"cd '{opts.testdir}' && " + + env_flags.replace("{", "{{").replace("}", "}}") + + f" {{compiler}} {way_flags} {flags} {opts.extra_run_opts}" + ) + + exit_code = await runCmd( + cmd, + stdin=in_testdir(name, "script"), + stdout=in_testdir(name, "run.stdout"), + stderr=in_testdir(name, "run.stderr"), + timeout_multiplier=opts.run_timeout_multiplier, + ) + + if exit_code != opts.exit_code: + if config.verbose >= 1: + print( + f"Wrong exit code for {name_with_browser}({way})(expected {opts.exit_code}, actual {exit_code})", + ) + dump_stdout(name) + dump_stderr(name) + message = format_bad_exit_code_message(exit_code) + return failBecause(message) + + # don't handle stderr for now, i don't wanna write a filter for the + # js stack traces + if not (await stdout_ok(name, way)): + return failBecause( + "bad stdout", stderr=read_stderr(name), stdout=read_stdout(name) + ) + + return passed() + + +# for browser001 we generate tests like browser001-firefox, but we +# don't want to have duplicate script/stdout files in tree since the +# behavior should be coherent across all browsers. hence this layer of +# indirection: the rest of the test driver recognizes +# browser001-firefox as test name, though we explicitly add +# browser001.script/browser001.stdout as extra_files to ensure they're +# copied, and ghci_browser would properly make use of them. +def browser_test(name, setup, func, args): + for browser in ["firefox", "chrome", "webkit"]: + test( + f"{name}-{browser}", + [ + unless(f"{browser.upper()}_LAUNCH_OPTS" in ghc_env, skip), + extra_files([f"{name}.script", f"{name}.stdout"]), + ] + + setup, + func, + args, + ) + + +browser_test("browser001", [], ghci_browser, []) diff --git a/testsuite/tests/ghci-browser/browser001.script b/testsuite/tests/ghci-browser/browser001.script new file mode 100644 index 0000000000000000000000000000000000000000..a0132e1c7241e8cda7072636d747aa8ebf171afd --- /dev/null +++ b/testsuite/tests/ghci-browser/browser001.script @@ -0,0 +1,58 @@ +import Language.Haskell.TH + +:{ +genFib :: Int -> Q Exp +genFib n = + pure $ + LamCaseE + [ Match (LitP $ IntegerL $ fromIntegral i) (NormalB $ LitE $ IntegerL r) [] + | (i, r) <- zip [0 .. n] fibs + ] + where + fibs = 0 : 1 : zipWith (+) fibs (drop 1 fibs) +:} + +:set -XTemplateHaskell + +:{ +fib :: Int -> Integer +fib = $(genFib 100) +:} + +fib 100 + +foreign import javascript "new Promise(res => setTimeout(() => res($2), $1))" foo :: Int -> Int -> IO Int + +foo 1024 114514 + +foreign import javascript "non_existent()" bar :: Int + +import Control.Exception +import GHC.Wasm.Prim + +catch (print bar) $ \(_ :: JSException) -> putStrLn "exception caught, life goes on" + +newtype JSButton = JSButton JSVal + +newtype JSCallback t = JSCallback JSVal + +foreign import javascript unsafe "document.createElement('button')" js_button_create :: IO JSButton +foreign import javascript unsafe "document.body.appendChild($1)" js_button_setup :: JSButton -> IO () + +btn <- js_button_create +js_button_setup + +foreign import javascript unsafe "$1.textContent" js_button_get_text :: JSButton -> IO JSString +foreign import javascript unsafe "$1.textContent = $2;" js_button_set_text :: JSButton -> JSString -> IO () + +js_button_set_text btn $ toJSString "1919810" + +foreign import javascript "wrapper sync" syncCallback :: IO () -> IO (JSCallback (IO ())) +foreign import javascript unsafe "$1.addEventListener('click', $2)" js_button_on_click :: JSButton -> JSCallback (IO ()) -> IO () + +cb <- syncCallback $ do { lbl <- fromJSString <$> js_button_get_text btn; print lbl } +js_button_on_click btn cb + +foreign import javascript unsafe "$1.click()" js_button_click :: JSButton -> IO () + +js_button_click btn diff --git a/testsuite/tests/ghci-browser/browser001.stdout b/testsuite/tests/ghci-browser/browser001.stdout new file mode 100644 index 0000000000000000000000000000000000000000..18ca110a13d781ee1d3ae275264f1f7544dec768 --- /dev/null +++ b/testsuite/tests/ghci-browser/browser001.stdout @@ -0,0 +1,4 @@ +354224848179261915075 +114514 +exception caught, life goes on +"1919810"