dream_test/sandbox

Process isolation for test execution (sandboxing).

This module provides the core mechanism for running a function in an isolated BEAM process with timeout support. It is used internally by the runner and is also useful directly in tests that need strong crash/timeout boundaries.

What problem does this solve?

Sometimes you want to run code that might:

run_isolated runs the function in a separate BEAM process, so the caller stays healthy even if the function crashes.

Selective crash reports

When a test process crashes (e.g. panic as "boom"), Erlang can print a crash report (=CRASH REPORT==== ...). This is useful when debugging, but noisy during normal runs.

SandboxConfig.show_crash_reports lets you choose:

Example

import dream_test/matchers.{be_equal, or_fail_with, should}
import dream_test/sandbox.{
  SandboxCompleted, SandboxConfig, SandboxCrashed, SandboxTimedOut,
}
import dream_test/unit.{describe, it}

fn loop_forever() {
  loop_forever()
}

pub fn tests() {
  describe("Sandboxing", [
    it("run_isolated returns SandboxCompleted(value) on success", fn() {
      let config = SandboxConfig(timeout_ms: 100, show_crash_reports: False)
      let result = sandbox.run_isolated(config, fn() { 123 })

      result
      |> should
      |> be_equal(SandboxCompleted(123))
      |> or_fail_with("expected SandboxCompleted(123)")
    }),

    it(
      "run_isolated returns SandboxTimedOut when the function is too slow",
      fn() {
        let config = SandboxConfig(timeout_ms: 10, show_crash_reports: False)
        let result = sandbox.run_isolated(config, loop_forever)

        result
        |> should
        |> be_equal(SandboxTimedOut)
        |> or_fail_with("expected SandboxTimedOut")
      },
    ),

    it("run_isolated returns SandboxCrashed when the function panics", fn() {
      let config = SandboxConfig(timeout_ms: 100, show_crash_reports: False)
      let result = sandbox.run_isolated(config, fn() { panic as "boom" })

      let did_crash = case result {
        SandboxCrashed(_) -> True
        _ -> False
      }

      did_crash
      |> should
      |> be_equal(True)
      |> or_fail_with("expected SandboxCrashed(...)")
    }),
  ])
}

Types

Configuration for sandboxed test execution.

  • timeout_ms: how long to wait for the worker to finish before killing it
  • show_crash_reports: whether to allow the BEAM to print crash reports

Example

import dream_test/sandbox.{SandboxConfig}

let config = SandboxConfig(timeout_ms: 1_000, show_crash_reports: False)

Parameters

  • timeout_ms: how long to wait before returning SandboxTimedOut
  • show_crash_reports: whether to allow the BEAM to print crash reports

Returns

A SandboxConfig value.

pub type SandboxConfig {
  SandboxConfig(timeout_ms: Int, show_crash_reports: Bool)
}

Constructors

  • SandboxConfig(timeout_ms: Int, show_crash_reports: Bool)

Result of running a function in an isolated sandbox.

This is intentionally simple so it can be used in higher-level code (like a runner) without pulling in reporter concerns.

Variants

  • SandboxCompleted(value): the function completed successfully and returned value
  • SandboxTimedOut: the function did not finish within SandboxConfig.timeout_ms
  • SandboxCrashed(reason): the function crashed; reason is a best-effort description
pub type SandboxResult(a) {
  SandboxCompleted(a)
  SandboxTimedOut
  SandboxCrashed(reason: String)
}

Constructors

  • SandboxCompleted(a)

    Function completed successfully and returned a value.

  • SandboxTimedOut

    Test did not complete within the timeout period.

  • SandboxCrashed(reason: String)

    Test process crashed with the given reason.

Values

pub fn default_config() -> SandboxConfig

Default configuration with a 5 second timeout.

Uses show_crash_reports: False.

Returns

A SandboxConfig suitable for most tests.

Parameters

None.

pub fn run_isolated(
  config config: SandboxConfig,
  test_function test_function: fn() -> a,
) -> SandboxResult(a)

Run a test function in an isolated process with timeout.

The test function runs in a separate BEAM process that is monitored. If the process completes normally, its result is returned. If the process crashes or times out, an appropriate SandboxResult is returned.

Example

import dream_test/matchers.{be_equal, or_fail_with, should}
import dream_test/sandbox.{
  SandboxCompleted, SandboxConfig, SandboxCrashed, SandboxTimedOut,
}
import dream_test/unit.{describe, it}

fn loop_forever() {
  loop_forever()
}

pub fn tests() {
  describe("Sandboxing", [
    it("run_isolated returns SandboxCompleted(value) on success", fn() {
      let config = SandboxConfig(timeout_ms: 100, show_crash_reports: False)
      let result = sandbox.run_isolated(config, fn() { 123 })

      result
      |> should
      |> be_equal(SandboxCompleted(123))
      |> or_fail_with("expected SandboxCompleted(123)")
    }),

    it(
      "run_isolated returns SandboxTimedOut when the function is too slow",
      fn() {
        let config = SandboxConfig(timeout_ms: 10, show_crash_reports: False)
        let result = sandbox.run_isolated(config, loop_forever)

        result
        |> should
        |> be_equal(SandboxTimedOut)
        |> or_fail_with("expected SandboxTimedOut")
      },
    ),

    it("run_isolated returns SandboxCrashed when the function panics", fn() {
      let config = SandboxConfig(timeout_ms: 100, show_crash_reports: False)
      let result = sandbox.run_isolated(config, fn() { panic as "boom" })

      let did_crash = case result {
        SandboxCrashed(_) -> True
        _ -> False
      }

      did_crash
      |> should
      |> be_equal(True)
      |> or_fail_with("expected SandboxCrashed(...)")
    }),
  ])
}

Parameters

  • config: sandbox configuration (timeout + crash report behavior)
  • test_function: function to run in an isolated process

Returns

A SandboxResult(a) describing completion, timeout, or crash.

pub fn with_crash_reports(
  config config: SandboxConfig,
) -> SandboxConfig

Convenience helper for enabling crash reports in the sandbox.

This is useful when debugging failures locally.

Example

let config = sandbox.default_config() |> sandbox.with_crash_reports()

Parameters

  • config: an existing SandboxConfig

Returns

A new SandboxConfig with show_crash_reports: True.

Search Document