Sign in

    mooncram

    A simple and powerful testing toolkit for MoonBit CLI applications and Script

    Download zip
    Version
    0.0.9
    License
    Apache-2.0
    Last updated
    1 hour ago
    Downloads
    22

    #mooncram

    mooncram is a command-line testing framework designed specifically for MoonBit CLI applications and .mbtx scripts. Write a command and its expected output in Markdown, explain the behavior, and let mooncram check that the example still works.

    mooncram is inspired by scrut. It brings readable CLI tests to MoonBit, with support for local executable packages, standalone .mbtx scripts, and tools from Mooncakes.

    #Why mooncram?

    Unit tests check your functions. CLI tests check what someone using your program actually sees: arguments, output, error messages, and exit codes.

    Use mooncram to test a greeting script, a command's --version output, an invalid argument, or a pipeline of MoonBit tools. Your tests are ordinary Markdown files, so you can explain the intent alongside each example. When the behavior changes intentionally, mooncram can refresh the expected output for you.

    #Before running tests

    DecisionRule
    Write a commandCommands do not run through a shell. Environment variables and wildcards in arguments remain literal.
    Select output to assertBy default, only stdout and the program's exit code are asserted. To assert stderr, include both @STDOUT and @STDERR markers.
    Select a backendScripts and remote packages always use Wasm. --target selects the backend for local packages.
    Resolve pathsLocal modules are read from moon.mod directly in the startup directory. Scripts and relative file arguments use the block's working directory: the document directory by default, or its #cwd directory.
    Run or previewtest, update, and update --dry-run execute code with your permissions, without a sandbox or temporary workspace isolation. The default 60000 ms case timeout includes builds, remote fetching, execution, and output capture.
    Review an updatePassing cases are processed too; expectations and fences may be normalized. An execution error blocks all expectation updates in that document; other documents can still update.
    Interpret exit codesMooncram's exit code is independent of the tested program's expected code. Dry-run can report mismatches and return 0; use test for CI.

    #Getting started

    Install the MoonBit toolchain first. Put its bin directory on PATH. Each mooncram invocation selects the first executable moon and uses that installation's moonrun and moonx as needed.

    To try the current source:

    git clone https://github.com/moonbit-community/mooncram.git cd mooncram moon update moon run . -- test README.md

    That last command runs this example using examples/hello.mbtx:

    $ moonx ./examples/hello.mbtx "Moon Bit" Hello, Moon Bit!

    A successful run ends with:

    1 cases, 0 failed, 0 errors

    To install mooncram as a command, run:

    moon install https://github.com/moonbit-community/mooncram mooncram --help

    Keep the MoonBit binary directory on your PATH. The rest of this guide uses the installed mooncram command.

    #Write your first test

    Create two files beside each other:

    tests/ ├── hello.mbtx └── hello.md

    In tests/hello.mbtx, write a small program:

    fn main {
    println("Hello, MoonBit!")
    }

    In tests/hello.md, describe what you want to test:

    # Greeting The script greets the user. ```mooncram $ moonx ./hello.mbtx Hello, MoonBit! ```

    Then run:

    mooncram test tests/hello.md

    The $ line is the command. The lines below it are the expected stdout. The expected exit code is 0 unless you say otherwise. Programs run from the directory containing the test document, so ./hello.mbtx is the file beside it.

    Each command starts a new case. A document can contain several blocks, with headings and explanations between them. Only backtick fences labeled mooncram are tests. Illustrative documents below appear inside larger markdown fences, so they are not executed as part of the README.

    #Choose what to run

    #Local scripts

    Write moonx followed by a relative or absolute path to a .mbtx file. Arguments support single quotes, double quotes, and backslash escapes. For a script accepting a name, such as this repository's examples/hello.mbtx:

    ```mooncram $ moonx ./hello.mbtx "Moon Bit" Hello, Moon Bit! ```

    #Packages in your project

    Add your module's full name after mooncram. Suppose moon.mod declares name = "user/acme" and your executable lives in cmd/acme:

    ```mooncram user/acme $ moonx user/acme/cmd/acme --version acme 1.0.0 ```

    Launch mooncram from the directory containing that moon.mod. The declaration must match its name exactly and applies only to that block. The package must be executable, for example with pkgtype(kind: "executable") in moon.pkg. Write moonx and the full package name, including the module name. For an executable root package, use moonx user/acme.

    mooncram builds local packages before running them. Build diagnostics stay separate from the program output being tested. Packages use Wasm by default; mooncram test tests --target native selects native and requires a C toolchain. Standalone scripts always use Wasm.

    Arguments after the target are passed directly to the program, including a literal --. mooncram does not parse moonx options before the target.

    #Tools from Mooncakes

    Run a full coordinate and an exact version directly with moonx:

    ```mooncram $ moonx moonbit-community/mooncram@0.0.3 --version 0.0.3 ```

    Coordinates use user/module[/package]@version. Versions must be exact SemVer; ranges, omitted versions and latest are rejected.

    moonx downloads and caches remote tools. They always use Wasm, including when local packages use --target native.

    #Describe the expected result

    #Output and exit codes

    Output lines match exactly by default, including spaces. To test a failure, place the expected exit code on the last line:

    ```mooncram user/acme $ moonx user/acme/cmd/acme --unknown @STDOUT (empty) @STDERR error: unknown option --unknown [2] ```

    @STDOUT and @STDERR let you check both streams in one run. Include each marker exactly once. Add (empty) to require no output at all, or (ignore) to skip checking that stream. Without markers, only stdout is matched; stderr is available in failure diagnostics.

    A command with no expected output lines must produce empty stdout. If you only care whether it succeeds, use @STDOUT (ignore) and @STDERR (ignore) together.

    #Flexible output expectations

    For output that varies, match the stable parts with a glob or regex:

    ```mooncram user/acme $ moonx user/acme/cmd/acme --version acme * (glob) $ moonx user/acme/cmd/acme --version acme [0-9]+\.[0-9]+\.[0-9]+ (regex) ```

    SuffixWhat it checks
    NoneThe exact output line
    (glob)A whole-line pattern with *, ?, or character sets such as [a-z]
    (regex)A regular expression matching the whole line
    (escaped)An exact line written as a JSON string, such as "a\tb"
    (no-eol)The last output line has no terminating newline; may follow another suffix

    Each expectation matches one output line. Use JSON escaping for lines that look like test syntax: "[2]" (escaped), "$ this is output" (escaped), or "#cwd(output)" (escaped). Use "" (escaped) to assert an empty output line, especially at the end. CRLF is normalized to LF; captured output must be valid UTF-8.

    Prefer fixed input files when you can: a fixture makes a test reproducible while keeping its expectations precise. Use patterns for parts that need to vary, such as generated IDs or version numbers.

    #Set up the test environment

    #Environment variables

    Commands inherit your environment. Use a prefix assignment to set a variable for one command. The tested program reads the environment itself, for example in environment.mbtx:

    import { "moonbitlang/core/env" }

    fn main {
    let name = @env.get_env_var("NAME").unwrap_or("World")
    println("Hello, \{name}!")
    }

    ```mooncram $ NAME='Moon Bit' moonx ./environment.mbtx Hello, Moon Bit! $ NAME=World moonx ./environment.mbtx Hello, World! ```

    Temporary assignments apply only to that pipeline segment and its build; later commands inherit the original environment. Mooncram does not expand environment variables: ${NAME} and $NAME are ordinary text in arguments, assignment values, executable paths, working directories and expected output. Arguments and assignment values use normal splitting, quoting and escaping. For example, NAME=World moonx ./hello.mbtx "${NAME}" passes the literal argument ${NAME} while setting NAME to World in the child process. To pass a name as an argument, write it explicitly, as in moonx ./hello.mbtx World.

    #Working directory and fixtures

    Programs run in the directory containing the test document by default. Keep fixtures beside the document and pass their relative paths as arguments. To use another directory, put #cwd(path) on the first nonempty line in a block:

    ```mooncram #cwd(../examples) $ moonx ./hello.mbtx "Moon Bit" Hello, Moon Bit! ```

    The declaration occupies a whole line and may appear only once per block. The path is everything between #cwd( and the final ), without trimming, escaping, variable expansion or parenthesis matching. Spaces, backslashes, quotes and parentheses remain part of the path: #cwd(../fixtures with spaces) needs no quotes, and #cwd("foo") includes the double quotes in the directory name. Empty paths, a missing closing ), duplicate or late declarations are errors. Use (escaped) for output beginning with #cwd(; updates escape it automatically.

    The directory is relative to the document, must already exist, and applies only to that block. Local module lookup still uses the directory where you launched mooncram. Cases run in document order; files created by one remain visible to later cases. Programs use your current permissions and existing filesystem; mooncram does not create an isolated temporary workspace.

    #Pipelines

    Connect scripts, local packages, and remote tools with |. Every segment must explicitly name moonx:

    ```mooncram user/acme $ moonx ./input.mbtx | moonx user/acme/cmd/acme --uppercase HELLO, MOONBIT! ```

    Segments run concurrently. The last segment's stdout is matched. The expected exit code uses pipefail: the rightmost nonzero code, or 0 if all succeed. The first segment receives EOF on stdin.

    Each segment's stderr is captured separately, in pipeline order. Use @STDOUT and @STDERR to check both streams. Output redirection, including 2>&1 and 1>/dev/null, is rejected in every segment. Quote or escape such text to pass it as an ordinary argument.

    Commands use mooncram's own parser, rather than a shell. Shell commands such as echo and cat, operators such as && and ;, command substitution, redirection, and multiline shell expressions are unsupported. Put setup logic in a MoonBit script when needed.

    #Keep tests up to date

    If a program now prints a different greeting, test reports a diff between the expected and actual output. After reviewing the new behavior, preview and apply updated expectations:

    mooncram update tests --dry-run mooncram update tests mooncram test tests

    update writes directly into the original documents without a confirmation prompt. It also normalizes passing cases and keeps glob/regex patterns that still match. Commands and surrounding prose are preserved. A document with parsing, build, or execution errors is not updated, including its passing cases; other documents can still update. Review the full diff before applying it.

    --dry-run displays edits without writing Markdown, but still runs programs, which can change files themselves. For validation without execution, use:

    mooncram check tests

    check validates syntax, expectations, and local target resolution without building, fetching tools, or running programs. Use test afterward to verify compilation and behavior. A script generated by an earlier case may be reported missing by check, since no cases execute during that check.

    #Command reference and CI

    mooncram check [paths...] [--color auto|always|never] mooncram test [paths...] [--target wasm|native] [--timeout-ms N] mooncram update [paths...] [--target wasm|native] [--timeout-ms N] [--dry-run] mooncram docs --list mooncram docs <document-name> mooncram --help mooncram --version

    Coding agents and other tools can read the embedded English reference without access to this repository. Run mooncram docs --list for topic names and summaries. Use mooncram docs quickstart for a complete first test, mooncram docs recipes for runnable assertion templates, mooncram docs troubleshooting for failure diagnosis, or mooncram docs invocation for the command-line specification. Names are case-sensitive and omit .md. Running mooncram docs prints help. Documentation goes to stdout without color or a case summary; unknown names and invalid arguments exit with 2.

    For check, test, and update, pass files, directories, or both. Directories are searched recursively for .md files; no paths means the current directory. Hidden entries, _build, and target are skipped during traversal; directory symlinks are not followed. Files are deduplicated and sorted before their cases run.

    Execution errors include the source location, phase, pipeline segment, tool and working directory when available. Captured stdout/stderr is included in full; partial marks a stream cancelled before EOF. Invalid UTF-8 is shown as escaped bytes in diagnostics. Timeouts identify the last execution phase.

    The default target is wasm. The default timeout is 60000 milliseconds per case, including builds, downloads, execution, and output capture. check and docs reject --target and --timeout-ms. check, test, and update accept --color (default auto). docs accepts none of these options and prints plain text. Place case options after their subcommand; top-level help and help have no case options. NO_COLOR disables automatic color. Put -- before paths starting with -.

    In CI, install the toolchain and mooncram, then run mooncram test tests --color never. Use the exit status to decide whether it passed:

    StatusMeaning
    0Documentation, check, test, or update completed successfully
    1An output or exit-code assertion failed in test
    2Invalid arguments, a parsing/build/execution error, or no cases found

    update --dry-run returns 0 when it completes, even if edits are proposed. Use test for the CI pass/fail decision. These are mooncram's own exit codes: a program returning 2 can pass a case ending in [2], so mooncram test returns 0 when all cases pass.

    #Further reading

    #Development

    From the repository root:

    moon test moon test --target native moon run . -- test README.md README_CN.md node e2etests/integration.mjs moon info && moon fmt

    The integration suite requires Node.js and a native C toolchain and exercises both Wasm and native. See the CI workflow for the full checks.

    #License

    Source Files