Gru is a drop-in CLI runner for Bruno .bru collections, plus a Go SDK (gruno) for embedding Bruno-compatible execution in Go tests or services. It tracks Bru semantics closely while adding safety (remote ref controls), richer import/test generation, programmatic hooks, and new data-driven runs.
- SDK interface:
gruno.New(ctx)returns agruno.Grunointerface for running single files or folders in-process. - Parser & executor: Recursive-descent parser covering meta (name/seq/tags/timeout/skip/script), headers, query, path/query params, vars/vars:post-response, body types (json/xml/text/form-urlencoded/multipart-form/graphql+vars), auth none/basic/bearer, asserts, docs, tests, tag filtering, skip.
- HTTP runner: Env/var expansion with deterministic unresolved-var errors; context-aware HTTP; JS assertions via goja; pre/post request scripts; Go pre/post hooks; external hook commands.
- CLI:
cmd/gruCobra app with tag filters, env/var overrides, delay/bail/recursive, reporters (json/junit/html) with header masking, logging controls, TLS/proxy flags, data-driven iterations (CSV/JSON/iteration-count, optional parallel). - Import:
gru import openapi|wsdlwith automatic test generation enabled by default (disable via--disable-test-generation), Swagger→OAS3 upgrade, remote/file-ref policies, include-path filter, Bruno-style path params, optional strictness tiers (loose|standard|strict) for generated assertions, output directory or--output-file. - WSDL: import + mock fixtures covering SOAP faults, facets, attachments; MTOM multipart/related streaming supported for binary parts.
- Parity guardrails: Sampledata validated against official
bruCLI; stateful mock server mirrors httpbin plus domain flows (users/shipping/finance/trace IDs) and Bruno’s GitHub mini-collection. - Tests: Parser/JS/runner unit coverage, importer tests, integration checks
TestBruCLISingleFile+TestRunFolderSampledata.
# build CLI
go build -o bin/gru ./cmd/gru
# run sample collection (uses bundled env)
gru run sampledata --env sampledata/environments/local.bru -r
# default target is current directory
gru run --env local
# run from another directory (CI-friendly)
gru -C sampledata run --env environments/local.bru -r
# run only Compat folder with tag include
gru run sampledata/Compat --env sampledata/environments/local.bru --tags smoke
# data-driven: CSV rows become iterations (vars available as {{col}} and bru.runner.iterationData)
gru run sampledata --csv-file-path users.csv --env sampledata/environments/local.bru --parallel
# data-driven: JSON array
gru run sampledata --json-file-path data.json --iteration-count 0
# fixed iteration count (no data file)
gru run sampledata --iteration-count 3
# env and inline vars (alias: --env-var)
gru run sampledata --env environments/local.bru --var baseUrl=https://api.test --var token=abcd
# write reporters with header redaction
gru run sampledata --env sampledata/environments/local.bru -r \
--output report.json --format json \
--reporter-junit report.xml --reporter-html report.html \
--reporter-skip-headers Authorization
# TLS/proxy knobs
gru run sampledata --env sampledata/environments/local.bru \
--insecure --cacert root.pem --ignore-truststore --noproxy --disable-cookies
# hooks (OS commands run before/after each request)
gru run sampledata --run-pre-request "./sign.sh" --run-post-request "./audit.sh"
# MTOM / multipart-related (root XML + binary attachment)
cat > cases/mtom.bru <<'EOF'
meta { name: MTOM }
post { url: {{baseUrl}}/mtom }
headers { Content-Type: multipart/related; type="application/xop+xml"; start="<rootpart>" }
body:multipart-form {
root: <Envelope><Body>ping</Body></Envelope>;type=application/xop+xml;cid=<rootpart>
file: @./payload.bin;type=application/octet-stream;cid=<attach1>
}
EOF
gru run cases --env environments/local.bru
# MTOM with inline text part (no file)
body:multipart-form {
root: <Envelope><Body>ping</Body></Envelope>;type=application/xop+xml;cid=<rootpart>
note: sample-text;type=text/plain;cid=<note1>
}# OpenAPI → Bruno collection with generated tests (default)
gru import openapi -s api.yaml -o out/collection
# Parity-only import (no generated tests)
gru import openapi -s api.yaml -o out/collection --disable-test-generation
# Limit to select paths and allow remote refs
gru import openapi -s api.yaml -o out/collection -i /v1/users,/v1/orders --allow-remote-refs
# Stricter schema assertions (nested arrays/enums/integer finiteness)
gru import openapi -s api.yaml -o out/collection --strictness strict
# WSDL import (tests on by default; disable if you only want requests)
gru import wsdl -s service.wsdl -o out/wsdl --disable-test-generation
# Output a single JSON file instead of a directory
gru import openapi -s api.yaml -f out/collection.jsonImport defaults:
- Tests generated unless
--disable-test-generation. OpenAPI tests include per-request assertions for required/type/format/range/enum/array/property-count/discriminator.--strictnesstoggles depth: loose (minimal), standard (default), strict (deep nested arrays/objects + numeric/enums). - Remote
$refblocked unless--allow-remote-refs; file refs limited to same tree unless--allow-file-refs. - Swagger 2.0 is auto-converted to OAS3; path params rendered as
:id; include-only paths via-i/--include-path.
- OpenAPI: tests are generated into each
.brufile’stests { ... }block by default. Assertions cover required fields, types, formats, ranges, enums, array sizes, object property counts, and discriminator checks (depth controlled by--strictness). - WSDL: generated requests include response validation tests by default; disable with
--disable-test-generationif you only want request skeletons. - All generated tests are plain Bruno tests you can edit after import.
- Root CAs: system truststore is used unless
--ignore-truststore, in which case only--cacert(if provided) is trusted. --cacertappends to the system pool by default; combine with--ignore-truststoreto pin to that CA only.- Proxy: honours standard env vars unless
--noproxy;NO_PROXYrules still apply when proxies are enabled. - Cookies: cookie jar is enabled by default; disable with
--disable-cookies. - TLS client certs: JSON via
--client-cert-configaccepts{ "cert": "...", "key": "..." }or Bruno-style domain entries; first valid cert/key is used.
- Working dir:
-C/--directory <path>changes to a directory before running the command (useful for CI and parity with Bru's "cd then run" flow). - Version:
gru versionprints the module name and version (e.g.,pkt.systems/gruno v1.2.3). - Env/vars:
--env <file>(relative names resolve toenvironments/<name>.bru), inline overrides via--var key=valueor--env-var. - Filtering:
--tags,--exclude-tags,--tests-only. - Flow control:
--delay,--bail,-r/--recursive, per-request--timeout. - Data-driven:
--csv-file-path,--json-file-path,--iteration-count(default 1),--parallel(runs cases per iteration concurrently). - Hooks:
--run-pre-request <cmd>/--run-post-request <cmd>; non-zero exit aborts the run (stdout/stderr streamed). - Logging:
--structuredJSON logs;--log-level trace|debug|info|warn|error(defaults to info; honours LOG_LEVEL when flag unset);--log-caller. - TLS/transport:
--insecure,--cacert,--ignore-truststore,--client-cert-config,--noproxy,--disable-cookies. - Reporters:
-o/--outputwith-f/--format json|junit|htmlor explicit--reporter-json|junit|html;--reporter-skip-headersor--reporter-skip-all-headersto strip/mask.
ctx := context.Background()
g, _ := gruno.New(ctx)
sum, _ := g.RunFolder(ctx, "sampledata", gruno.RunOptions{
EnvPath: "sampledata/environments/local.bru",
Vars: map[string]string{"HELLO": "world"},
})
log.Printf("passed=%d failed=%d", sum.Passed, sum.Failed)v := gruno.Version() // e.g. "v1.2.3"httpClient := &http.Client{Timeout: 5 * time.Second}
g, _ := gruno.New(ctx,
gruno.WithHTTPClient(httpClient),
gruno.WithPreRequestHook(func(ctx context.Context, info gruno.HookInfo, req *http.Request, logger pslog.Base) error {
req.Header.Set("X-Signature", sign(req))
return nil
}),
gruno.WithPostRequestHook(func(ctx context.Context, info gruno.HookInfo, res gruno.CaseResult, logger pslog.Base) error {
if !res.Passed {
logger.Warn("case failed", "file", info.FilePath, "err", res.ErrorText)
}
return nil
}),
)
res, _ := g.RunFile(ctx, "sampledata/Users/get_user.bru", gruno.RunOptions{
EnvPath: "sampledata/environments/local.bru",
Timeout: 10 * time.Second,
})sum, _ := g.RunFolder(ctx, "sampledata", gruno.RunOptions{
EnvPath: "sampledata/environments/local.bru",
CSVFilePath: "users.csv", // or JSONFilePath
Parallel: true, // optional
IterationCount: 0, // ignored when CSV/JSON present
})g, _ := gruno.New(ctx)
bru := `meta { name: MTOM }
post { url: {{baseUrl}}/mtom }
headers { Content-Type: multipart/related; type="application/xop+xml"; start="<rootpart>" }
body:multipart-form {
root: <Envelope><Body>ping</Body></Envelope>;type=application/xop+xml;cid=<rootpart>
file: @./payload.bin;type=application/octet-stream;cid=<attach1>
}
tests {
test("mtom parts", function() {
expect(res.status).to.equal(200);
});
}`
_ = os.WriteFile("cases/mtom.bru", []byte(bru), 0o644)
sum, _ := g.RunFile(ctx, "cases/mtom.bru", gruno.RunOptions{
EnvPath: "environments/local.bru",
})Iteration metadata is available to JS via bru.runner.iterationIndex, bru.runner.totalIterations, bru.runner.iterationData.get("field"), and bru.getVar("field"); pre-request scripts can use the same helpers.
_ = gruno.ImportOpenAPI(ctx, gruno.ImportOptions{
Source: "api.yaml",
OutputDir: "out/collection",
CollectionName: "My API",
GroupBy: "tags", // or "path"
DisableTests: false, // tests generated by default
AllowRemoteRefs:false, // default
})sampledata/contains compatibility suites plus Bruno’s mini “GitHub” collection undersampledata/GitHub/.sampledata/environments/local.bruseeds variables; adjustbaseUrlwhen running against your own server.
- Go 1.24+
- Bruno CLI in PATH only for parity tests (non-
-shortruns).
make releasebuilds and zips multi-arch binaries (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64).make test-windowsruns the Windows test suite under Wine (go test -short -exec "wine").