Search by

hampel / rig

hampel

Exercise a package by hand, for real - a harness for the things a test suite must not do

Package info

github.com/hampel/rig

pkg:composer/hampel/rig

Statistics

Installs: 447

Dependents: 8

Suggesters: 0

Stars: 0

Open Issues: 0

1.1.0 2026-08-27 06:00 UTC

This package is auto-updated.

Last update: 2026-08-27 13:07:34 UTC


README

Tests Latest Version on Packagist Total Downloads

Exercise a package by hand, for real.

A rig is a small harness you install into a package under development so you can drive its code the way a consumer would — and watch what actually happens. Exercises are plain PHP scripts that live with the package. They are meant to have side effects: post the message, call the API, write the file. That is the point.

This is not a test runner, and it is not trying to become one. A test asserts, runs unattended, and reports a verdict — so it must not touch anything real. A rig does the opposite: it does the real thing and shows you the result, and you decide what it means. Both are worth having, and neither substitutes for the other.

Why you might want one

The sharpest case is a package that wraps somebody else's API. It is a pile of assumptions — endpoint paths, required fields, response shapes, what an error looks like — every one of them true on the day it was written. Then the remote API moves, nothing local changes, and the package is quietly wrong.

A test suite cannot find that, and a green one is what keeps it invisible: the tests pass because they mock the HTTP client, so the mock encodes exactly the same assumption the code does. Both sides stay agreed with each other and disagreed with reality. Static analysis is no better — a path spelled custom_fields/ where the vendor now documents custom_field/ is not a type error, it is a claim about a system PHPStan cannot see. Only a real call settles it, and the answer often is not an exception but a number that looks slightly off: a filter the API has quietly stopped honouring returns 200 OK and every record in the account. You cannot assert your way to is this count suspiciously equal to that one without already knowing the answer you are looking for. A person reading two numbers can.

Install

composer require --dev hampel/rig

It has no dependencies. Nothing but PHP is added to your vendor directory, which matters: a harness that dragged a framework in would put hundreds of classes where your package's static analysis can see them, and your package would stop being able to tell you it used something it never declared.

Use

Write exercises as .php files in a harness/ directory in your package:

<?php

/**
 * Exercise: post a notification and show what came back.
 *
 * @var Hampel\Rig\Io $io
 */

use Acme\Notifier\Webhook;

$io->title('acme/notifier · post');

$webhook = new Webhook(getenv('WEBHOOK_URL'));

$io->attempt('post a message', fn () => $webhook->post('rig was here'));

Then:

vendor/bin/rig                  # list the exercises this package offers
vendor/bin/rig post             # run one
vendor/bin/rig post --php=php8.4   # run it on another PHP

Each exercise is handed one variable, $io. Everything else is ordinary PHP — the package is autoloaded, so use it however a consumer would.

What $io offers

title($text) a heading, with a rule under it
info($text) context, rather than an outcome
success($text), warn($text), error($text) one line, in green, amber or red
line($text = '') a bare line — call it with nothing for a blank one
value($label, $value) a label and a value, aligned into a column
values([$label => $value, ...]) a group of them, aligned to the group's own widest label
attempt($description, $callback) run something, and report what returned or what was thrown
stringify($value) any value as the single line it would be printed as
isDecorated() whether output is coloured

Nothing here asserts anything, and attempt() is the reason to say so twice: a throwable is reported as an outcome, not a failure, because the error paths are usually what you came to look at. The exit status is the exercise's own.

value() aligns labels shorter than 14 characters into a fixed column. A longer label keeps its line but takes a single space instead, so a run of them lands at ragged indents — which is worst for exactly the two-numbers-side-by-side case above. values() is the way out: its column is the widest label in the call, and never narrower than value()'s, so a group of short labels reads identically to the value() calls around it.

$io->values([
    'unfiltered total_results' => 26,
    'filtered total_results'   => 24,
]);

Options

option
--in-process run in the rig's own process rather than a fresh one
--php=<binary> run the exercise on another PHP binary
--package=<path> exercise a package in another directory
--harness=<dir> where the exercises are; default harness
--env=<file> environment file to load; default .env, relative to the package unless absolute
--agent-may-load-env load it even in an agent session; see Credentials
--list list exercises even when one is named
--version, --help

Credentials

Exercises that do real things need real credentials. rig reads a .env beside the package before running, into getenv():

WEBHOOK_URL=https://example.test/services/T000/B000/XXXX

Values already set in the environment win, so a single run can override the file without editing it:

WEBHOOK_URL=https://example.test/other vendor/bin/rig post

Agent sessions

rig does not read the environment file when CLAUDECODE is set — the variable Claude Code exports into every shell it opens. It says so, and runs the exercise anyway, on whatever defaults the exercise's own code chooses.

The file belongs to whoever owns the credentials in it, and it tends to say do the real thing, because that is how they run their own exercises. An agent asked to look at some output inherits that decision without having made it — which is how a harness comes to send real mail nobody asked for. Withholding the file is what lets an exercise's own safe default apply.

--agent-may-load-env loads it regardless, for an agent that has been asked to do the real thing.

This covers the file rig loads and nothing else. An exercise that reads a credential by itself, or a key committed to configuration, is untouched by it. With the variable absent nothing changes, so running exercises by hand behaves exactly as before.

Ignore environment files when you add harness/, whether or not anything needs credentials yet. A harness that starts out driving pure code grows an API call eventually, and by then the file is already tracked — the leak happens at the commit that adds the first real value, not at the one that adds the rule. In .gitignore:

.env
.env.*
!.env.example

None of those are anchored with a leading /, on purpose: that would cover the repository root only, and --env= resolves relative to the package, so an environment file can legitimately sit in a subdirectory. .env.* catches the variants the option invites — .env.staging, .env.local — and the last line keeps a committed .env.example documenting which variables an exercise expects.

This holds while environment files are named .env or .env.<something>. --env= will load any file you point it at, and one named otherwise is not covered.

The parser is deliberately minimal — KEY=VALUE, # comments, optional surrounding quotes, nothing else. An exercise is ordinary PHP and can read configuration however it likes if it needs more.

Keeping the harness out of the dist archive

Exercises belong in the repository and not in the tarball your consumers install. In .gitattributes:

/harness export-ignore

Two pieces of repository metadata, then — this one, and the environment-file block in .gitignore. Add both when you create harness/, so neither is waiting on the day it turns out to matter.

Why a fresh process by default

Each exercise runs in its own PHP process. Not for speed — for three properties worth having:

  • a fatal error in an exercise cannot take the rig down with it, so you see the error
  • the exit status is the exercise's, not the rig's
  • --php can point at any PHP binary, which is how you drive the same exercise at both ends of the range your package claims to support

--in-process opts out, for when you want the exercise inside the current process — an attached debugger, or an exercise that opens a REPL.

Requirements

PHP 8.3 or later. No dependencies.

Licence

MIT. See LICENSE.md.