-module(dream_test@sandbox). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/dream_test/sandbox.gleam"). -export([run_isolated/2, default_config/0, with_crash_reports/1]). -export_type([sandbox_config/0, sandbox_result/1, worker_message/1]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " Process isolation for test execution (sandboxing).\n" "\n" " This module provides the core mechanism for running a function in an\n" " isolated BEAM process with timeout support. It is used internally by the\n" " runner and is also useful directly in tests that need strong crash/timeout\n" " boundaries.\n" "\n" " ## What problem does this solve?\n" "\n" " Sometimes you want to run code that might:\n" "\n" " - `panic`\n" " - hang (never return)\n" " - take “too long” and should be timed out\n" "\n" " `run_isolated` runs the function in a separate BEAM process, so the caller\n" " stays healthy even if the function crashes.\n" "\n" " ## Selective crash reports\n" "\n" " When a test process crashes (e.g. `panic as \"boom\"`), Erlang can print a\n" " crash report (`=CRASH REPORT==== ...`). This is useful when debugging, but\n" " noisy during normal runs.\n" "\n" " `SandboxConfig.show_crash_reports` lets you choose:\n" "\n" " - `False` (default): suppress crash reports and return `SandboxCrashed(...)`\n" " - `True`: allow crash reports to print, and still return `SandboxCrashed(...)`\n" "\n" " ## Example\n" "\n" " ```gleam\n" " import dream_test/matchers.{be_equal, or_fail_with, should}\n" " import dream_test/sandbox.{\n" " SandboxCompleted, SandboxConfig, SandboxCrashed, SandboxTimedOut,\n" " }\n" " import dream_test/unit.{describe, it}\n" "\n" " fn loop_forever() {\n" " loop_forever()\n" " }\n" "\n" " pub fn tests() {\n" " describe(\"Sandboxing\", [\n" " it(\"run_isolated returns SandboxCompleted(value) on success\", fn() {\n" " let config = SandboxConfig(timeout_ms: 100, show_crash_reports: False)\n" " let result = sandbox.run_isolated(config, fn() { 123 })\n" "\n" " result\n" " |> should\n" " |> be_equal(SandboxCompleted(123))\n" " |> or_fail_with(\"expected SandboxCompleted(123)\")\n" " }),\n" "\n" " it(\n" " \"run_isolated returns SandboxTimedOut when the function is too slow\",\n" " fn() {\n" " let config = SandboxConfig(timeout_ms: 10, show_crash_reports: False)\n" " let result = sandbox.run_isolated(config, loop_forever)\n" "\n" " result\n" " |> should\n" " |> be_equal(SandboxTimedOut)\n" " |> or_fail_with(\"expected SandboxTimedOut\")\n" " },\n" " ),\n" "\n" " it(\"run_isolated returns SandboxCrashed when the function panics\", fn() {\n" " let config = SandboxConfig(timeout_ms: 100, show_crash_reports: False)\n" " let result = sandbox.run_isolated(config, fn() { panic as \"boom\" })\n" "\n" " let did_crash = case result {\n" " SandboxCrashed(_) -> True\n" " _ -> False\n" " }\n" "\n" " did_crash\n" " |> should\n" " |> be_equal(True)\n" " |> or_fail_with(\"expected SandboxCrashed(...)\")\n" " }),\n" " ])\n" " }\n" " ```\n" ). -type sandbox_config() :: {sandbox_config, integer(), boolean()}. -type sandbox_result(IBF) :: {sandbox_completed, IBF} | sandbox_timed_out | {sandbox_crashed, binary()}. -type worker_message(IBG) :: {test_completed, IBG} | {worker_down, binary()}. -file("src/dream_test/sandbox.gleam", 307). ?DOC(" Handle timeout by killing the worker and returning timeout result.\n"). -spec handle_timeout(gleam@erlang@process:pid_()) -> sandbox_result(any()). handle_timeout(Worker_pid) -> gleam@erlang@process:kill(Worker_pid), sandbox_timed_out. -file("src/dream_test/sandbox.gleam", 299). ?DOC(" Handle a message from the worker or monitor.\n"). -spec handle_worker_message(worker_message(ICA)) -> sandbox_result(ICA). handle_worker_message(Message) -> case Message of {test_completed, Result} -> {sandbox_completed, Result}; {worker_down, Reason} -> {sandbox_crashed, Reason} end. -file("src/dream_test/sandbox.gleam", 286). ?DOC(" Wait for the worker to complete or timeout.\n"). -spec wait_for_result( sandbox_config(), gleam@erlang@process:selector(worker_message(IBW)), gleam@erlang@process:pid_(), gleam@erlang@process:monitor() ) -> sandbox_result(IBW). wait_for_result(Config, Selector, Worker_pid, _) -> case gleam_erlang_ffi:select(Selector, erlang:element(2, Config)) of {ok, Message} -> handle_worker_message(Message); {error, nil} -> handle_timeout(Worker_pid) end. -file("src/dream_test/sandbox.gleam", 277). ?DOC(" Format an exit reason as a string for error reporting.\n"). -spec format_exit_reason(gleam@erlang@process:exit_reason()) -> binary(). format_exit_reason(Reason) -> case Reason of normal -> <<"normal"/utf8>>; killed -> <<"killed"/utf8>>; {abnormal, Dynamic_reason} -> gleam@string:inspect(Dynamic_reason) end. -file("src/dream_test/sandbox.gleam", 271). ?DOC(" Map a process down event to a WorkerMessage.\n"). -spec map_down_to_message(gleam@erlang@process:down()) -> worker_message(any()). map_down_to_message(Down) -> Reason = format_exit_reason(erlang:element(4, Down)), {worker_down, Reason}. -file("src/dream_test/sandbox.gleam", 252). ?DOC(" Build a selector that waits for either test completion or process down.\n"). -spec build_result_selector( gleam@erlang@process:subject(worker_message(IBP)), boolean() ) -> gleam@erlang@process:selector(worker_message(IBP)). build_result_selector(Result_subject, Show_crash_reports) -> case Show_crash_reports of true -> _pipe = gleam_erlang_ffi:new_selector(), _pipe@1 = gleam@erlang@process:select(_pipe, Result_subject), gleam@erlang@process:select_monitors( _pipe@1, fun map_down_to_message/1 ); false -> _pipe@2 = gleam_erlang_ffi:new_selector(), gleam@erlang@process:select(_pipe@2, Result_subject) end. -file("src/dream_test/sandbox.gleam", 230). ?DOC(" Spawn a worker process that runs the test and sends the result.\n"). -spec spawn_worker( gleam@erlang@process:subject(worker_message(IBM)), fun(() -> IBM), boolean() ) -> gleam@erlang@process:pid_(). spawn_worker(Result_subject, Test_function, Show_crash_reports) -> proc_lib:spawn(fun() -> case Show_crash_reports of true -> Result = Test_function(), gleam@erlang@process:send( Result_subject, {test_completed, Result} ); false -> case sandbox_ffi:run_catching(Test_function) of {ok, Result@1} -> gleam@erlang@process:send( Result_subject, {test_completed, Result@1} ); {error, Reason} -> gleam@erlang@process:send( Result_subject, {worker_down, Reason} ) end end end). -file("src/dream_test/sandbox.gleam", 215). ?DOC( " Run a test function in an isolated process with timeout.\n" "\n" " The test function runs in a separate BEAM process that is monitored.\n" " If the process completes normally, its result is returned.\n" " If the process crashes or times out, an appropriate SandboxResult is returned.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " import dream_test/matchers.{be_equal, or_fail_with, should}\n" " import dream_test/sandbox.{\n" " SandboxCompleted, SandboxConfig, SandboxCrashed, SandboxTimedOut,\n" " }\n" " import dream_test/unit.{describe, it}\n" "\n" " fn loop_forever() {\n" " loop_forever()\n" " }\n" "\n" " pub fn tests() {\n" " describe(\"Sandboxing\", [\n" " it(\"run_isolated returns SandboxCompleted(value) on success\", fn() {\n" " let config = SandboxConfig(timeout_ms: 100, show_crash_reports: False)\n" " let result = sandbox.run_isolated(config, fn() { 123 })\n" "\n" " result\n" " |> should\n" " |> be_equal(SandboxCompleted(123))\n" " |> or_fail_with(\"expected SandboxCompleted(123)\")\n" " }),\n" "\n" " it(\n" " \"run_isolated returns SandboxTimedOut when the function is too slow\",\n" " fn() {\n" " let config = SandboxConfig(timeout_ms: 10, show_crash_reports: False)\n" " let result = sandbox.run_isolated(config, loop_forever)\n" "\n" " result\n" " |> should\n" " |> be_equal(SandboxTimedOut)\n" " |> or_fail_with(\"expected SandboxTimedOut\")\n" " },\n" " ),\n" "\n" " it(\"run_isolated returns SandboxCrashed when the function panics\", fn() {\n" " let config = SandboxConfig(timeout_ms: 100, show_crash_reports: False)\n" " let result = sandbox.run_isolated(config, fn() { panic as \"boom\" })\n" "\n" " let did_crash = case result {\n" " SandboxCrashed(_) -> True\n" " _ -> False\n" " }\n" "\n" " did_crash\n" " |> should\n" " |> be_equal(True)\n" " |> or_fail_with(\"expected SandboxCrashed(...)\")\n" " }),\n" " ])\n" " }\n" " ```\n" "\n" " ## Parameters\n" "\n" " - `config`: sandbox configuration (timeout + crash report behavior)\n" " - `test_function`: function to run in an isolated process\n" "\n" " ## Returns\n" "\n" " A `SandboxResult(a)` describing completion, timeout, or crash.\n" ). -spec run_isolated(sandbox_config(), fun(() -> IBK)) -> sandbox_result(IBK). run_isolated(Config, Test_function) -> Result_subject = gleam@erlang@process:new_subject(), Worker_pid = spawn_worker( Result_subject, Test_function, erlang:element(3, Config) ), Worker_monitor = gleam@erlang@process:monitor(Worker_pid), Selector = build_result_selector(Result_subject, erlang:element(3, Config)), wait_for_result(Config, Selector, Worker_pid, Worker_monitor). -file("src/dream_test/sandbox.gleam", 323). ?DOC( " Default configuration with a 5 second timeout.\n" "\n" " Uses `show_crash_reports: False`.\n" "\n" " ## Returns\n" "\n" " A `SandboxConfig` suitable for most tests.\n" "\n" " ## Parameters\n" "\n" " None.\n" ). -spec default_config() -> sandbox_config(). default_config() -> {sandbox_config, 5000, false}. -file("src/dream_test/sandbox.gleam", 344). ?DOC( " Convenience helper for enabling crash reports in the sandbox.\n" "\n" " This is useful when debugging failures locally.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " let config = sandbox.default_config() |> sandbox.with_crash_reports()\n" " ```\n" "\n" " ## Parameters\n" "\n" " - `config`: an existing `SandboxConfig`\n" "\n" " ## Returns\n" "\n" " A new `SandboxConfig` with `show_crash_reports: True`.\n" ). -spec with_crash_reports(sandbox_config()) -> sandbox_config(). with_crash_reports(Config) -> {sandbox_config, erlang:element(2, Config), true}.