CI Test Fixtures
Generate a synthetic dataset for tests in which every field is derived from the
record number, with no random values and no clock, so every run writes the same
file, byte for byte. Use expanso-edge validate in the same pipeline to fail
the build when a job config has a typo.
Components: generate input (count: 200) → file output, with
pipeline.threads: 1.
Needs: nothing beyond a node. The job reads no external data.
This job ran end to end on an expanso-edge v2.1.21 node in local mode, not
through Expanso Cloud.
Complete job
name: ci-fixtures
type: pipeline
# One-shot job: stop after one failed execution instead of retrying.
restart_policy: never
selector:
match_labels:
pipeline_role: ci
config:
input:
generate:
count: 200
interval: ""
mapping: |
let n = count("fixtures_RUN_ID")
let first = [
"Ada", "Grace", "Alan", "Edsger", "Barbara", "Ken",
"Frances", "Donald"
]
root.id = "cust-%05d".format($n)
root.name = "%s Tester%d".format($first.index($n % 8), $n)
root.email = "tester%[email protected]".format($n)
root.tier = ["gold", "silver", "bronze"].index($n % 3)
root.balance = "%d.%02d".format(
($n * 7919 % 100000 / 100).floor().int64(),
($n * 7919 % 100).int64()
)
root.created_at = (1767225600 + $n * 86400).ts_format(
"2006-01-02T15:04:05Z", "UTC"
)
root.active = $n % 7 != 0
pipeline:
# One processing thread keeps output order equal to generation
# order. With the default parallelism the same records came out
# in a different order on each run, so the file was not
# byte-identical.
threads: 1
processors: []
output:
file:
path: "/var/tmp/expanso-fixtures/fixtures.jsonl"
codec: lines
Two settings make the output repeatable:
threads: 1. With the default parallelism, the same 200 records came out in a different order on each run. One thread keeps them in generation order.- A new counter name for every run. A
count()counter keeps counting for as long as the node process runs, across jobs. ReplaceRUN_IDwith a value that is unique to the run, such as your CI build number, so the ids start atcust-00001every time.
The selector sends the job to a node labelled pipeline_role: ci; set that
label on your node, or change the selector to a label it already carries.
With no matching node, the job is stored and never runs.
Use it in CI
Check the config first: expanso-edge validate exits with a non-zero status
on an unknown component or field, which fails the step. Then substitute the
run id and deploy:
expanso-edge validate ci-fixtures.yaml
sed "s/RUN_ID/$BUILD_ID/" ci-fixtures.yaml > ci-fixtures-run.yaml
expanso-cli job deploy ci-fixtures-run.yaml
BUILD_ID stands for your CI system's unique build number. The output path
is on the node that runs the job; create its directory there first with
mkdir -p /var/tmp/expanso-fixtures. Each run in the proof wrote to a fresh
file, so remove the previous fixtures.jsonl before the next run, and remove
the finished job with expanso-cli job delete ci-fixtures --yes
(without --yes the command waits at a confirmation prompt, which fails in a
non-interactive CI step).
A misspelled component name fails like this, from the run:
(line 10, col 3) [input]. Unknown component or field 'generat'. Check that the component name is spelled correctly.
Check it
Every record matches this JSON Schema. The run checked all 200 against it:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": [
"id",
"name",
"email",
"tier",
"balance",
"created_at",
"active"
],
"properties": {
"id": {
"type": "string",
"pattern": "^cust-[0-9]{5}$"
},
"name": {
"type": "string",
"minLength": 3
},
"email": {
"type": "string",
"pattern": "^tester[0-9]+@example\\.invalid$"
},
"tier": {
"enum": [
"gold",
"silver",
"bronze"
]
},
"balance": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]{2}$"
},
"created_at": {
"type": "string",
"pattern": "^20[0-9]{2}-[0-9]{2}-[0-9]{2}T00:00:00Z$"
},
"active": {
"type": "boolean"
}
}
}
The first line of the file, pretty-printed:
{
"active": true,
"balance": "79.19",
"created_at": "2026-01-02T00:00:00Z",
"id": "cust-00001",
"name": "Grace Tester1",
"tier": "silver"
}
What the run proved
- Two runs each wrote 200 records,
cust-00001tocust-00200in order. - The two files were byte-identical: the same SHA-256.
- Every record validated against the schema above with an independent JSON Schema checker.
- A copy of the job with
generatemisspelled asgeneratmadeexpanso-edge validateexit 1. - The job exactly as published,
restart_policy: neverincluded, ran on a local-mode node runningexpanso-edgev2.1.21. Each of the two runs completed after 1 execution.
Limits
- Proved on a local-mode node only, not through Expanso Cloud.
- With
restart_policy: never, a failed run stops after one execution instead of being re-run. See Proving your own run. - The dataset is fixed by the mapping. Edit the mapping to change the fields; keep every value derived from the counter to stay deterministic.
- The output is a file on the node. To use it in a CI job on another machine,
copy it from the node or replace the
fileoutput.
Related
- Build by Job: the task-to-components matrix
generateinput and Testing & Debugging