The Anvil Book
Learn to use Anvil from nothing: install it, write test steps, put them in sequences, run them, and read what Anvil says back.
Alpha. This edition is written and checked against Anvil 0.5.0 on Linux x86-64; installing on Windows is covered and checked too. Anvil is young, and some of what you will do here — cloning the repository to get the step SDK, above all — will get shorter. Where 0.5.0 has a rough edge or a known defect, the book says so where you meet it, rather than pretending it is not there.
Contents
- Introduction — what Anvil is, the words it uses, and what you need.
- Installing — the engine, the step SDK, your working folder.
- Your first step — a C# method the engine can call.
- Your first sequence — the file that says what to run, and how to run it.
- Measuring and judging — limits, pass, fail and error.
- Setup, main, cleanup and retries
- Inputs, variables and flow
- Instruments that stay open — references to objects that live in the executor.
- Subsequences
- Reports and running unattended
- The Sequence Editor — not written yet. In 0.5.0 the packaged editor opens, but it cannot run a sequence (#80).
- Steps in Python and Rust
- When something goes wrong
Read chapters 1 to 4 in order. After that each chapter stands mostly on its own, although each one adds a file or two to the same C# project.
Conventions
Everything happens in one folder, ~/anvil-book. Commands start with $ and
are run from that folder, unless the text says otherwise. The lines after a
command are what it printed — pasted from a real run, not retyped.
Anvil’s own diagnostic messages are still partly in Spanish in 0.5.0 (#59). The book shows them as they are printed and explains what they say.
How this book stays true
Every command in these chapters has been run against the release named above,
and none of the output was written by hand. The files you are asked to write
live in listings/, the terminal sessions in
listings/sessions/, and check.sh runs
every session again and checks that each file still appears word for word in
a chapter:
$ docs/book/check.sh path/to/anvil-v0.5.0-x86_64-linux-musl
What could not be run is marked not verified, with those words.
1. Introduction
Anvil is a test sequencer: it runs a list of test steps against a unit on a bench, judges what they measure and writes down what happened. If you have used NI TestStand or OpenTAP, the job is the same one.
What makes Anvil different is where things live.
- A sequence is a text file, not a program. It lists the steps, the limits each measurement must fall within, and what to do before and after. You can read it in a diff, and changing a limit does not mean recompiling anything.
- A step is ordinary code in your language — C#, Python or Rust — served by an executor: a small process (or, for Rust, a module) that publishes the steps it knows how to run. Anvil calls each step by name.
- The step measures; Anvil judges. A step returns a number. Whether that number is acceptable is written in the sequence, and Anvil decides. The same step can serve two products with different tolerances without being touched.
The words
| Word | What it means here |
|---|---|
| sequence | A .yseq (or .yaml) file: the steps to run and how to judge them. |
| step | One action or measurement, called by a name like board/measure_rail. |
| executor | The program that runs steps. A sequence says where each one is. |
| module | The first half of a step’s name. In C# it comes from the class. |
| limit | The acceptance criterion for a measurement, written in the sequence. |
| verdict | What a step or a sequence ended as: pass, fail, error or skipped. |
The difference between fail and error matters more than anything else in
this book, and chapter 5 is about it: fail says something about the unit,
error says something about the bench. A unit that measures out of range
fails. An instrument that does not answer is an error — Anvil could not judge
the unit, so it does not pretend to.
What this book covers
Writing steps in C#, writing sequences, running them from the command line and reading the results. One chapter shows what changes when the step is written in Python or Rust instead.
It does not cover talking to real instruments — every step here pretends to measure — nor how Anvil is built inside. For that, see the documentation index.
What you need
- Linux or Windows, on x86-64. Everything in the book was run on Linux. Chapter 2 also covers installing on Windows, and says what of the rest was run there.
- The .NET 10 SDK (
dotnet --versionshould print10.something). - git, to get the step SDK.
- For chapter 12 only: Python 3.10 or newer, and a Rust toolchain.
No instrument, no licence and no network connection to a bench.
2. Installing
You will end this chapter with one folder holding three things: the Anvil engine, a copy of the Anvil repository (for the C# step SDK), and an empty C# project where your steps will go.
$ mkdir ~/anvil-book
$ cd ~/anvil-book
The engine
Anvil is one statically linked binary; there is nothing to install system-wide. Download it from the release page, check it, and unpack it:
$ curl -sSLO https://github.com/anlaco/anvil/releases/download/v0.5.0/anvil-v0.5.0-x86_64-linux-musl.tar.gz
$ curl -sSLO https://github.com/anlaco/anvil/releases/download/v0.5.0/SHA256SUMS
$ sha256sum --check --ignore-missing SHA256SUMS
anvil-v0.5.0-x86_64-linux-musl.tar.gz: OK
$ tar xzf anvil-v0.5.0-x86_64-linux-musl.tar.gz
(sha256sum prints OK in your system’s language.)
The folder it creates holds anvil, the engine, and anvil-exec-wasm, which
chapter 12 uses. Put it on your PATH — in every new terminal you open for
this book:
$ export PATH="$PWD/anvil-v0.5.0-x86_64-linux-musl:$PATH"
and check:
$ anvil --version
anvil 0.5.0
anvil --version writes to the error stream, not to standard output, so
anvil --version | something reads nothing
(#75).
On Windows, follow the Windows section at the end of this chapter instead.
The step SDK
Your steps will use a small library, Anvil.Step. In 0.5.0 it is not yet
published on NuGet, so you take it from the repository, at the tag that matches
your engine:
$ git clone --depth 1 --branch v0.5.0 https://github.com/anlaco/anvil.git anvil
git will print a note about a “detached HEAD”: that is what checking out a tag looks like, and it is fine. You will not build Anvil from this copy — you only point your project at the SDK inside it. When the package reaches NuGet, this step goes away.
The project
$ dotnet --version
$ dotnet new console --output Bench
The first command must print a version starting with 10.. The second creates
Bench/, the executor you will fill with steps from chapter 3 on.
Your folder now looks like this:
~/anvil-book/
├── anvil/ the repository, for the SDK
├── anvil-v0.5.0-x86_64-linux-musl/ the engine
└── Bench/ your steps
The Sequence Editor
The release also carries the Sequence Editor, a graphical editor for the same
files, as anvil-editor-v0.5.0-x86_64-linux.AppImage, a .deb and a Windows
installer. This book does not use it yet (see the contents).
If you try the AppImage and it stops at dlopen(): error loading libfuse.so.2,
your system lacks FUSE 2, which recent Ubuntu releases no longer install. The
AppImage runs without it once extracted:
$ chmod +x anvil-editor-v0.5.0-x86_64-linux.AppImage
$ ./anvil-editor-v0.5.0-x86_64-linux.AppImage --appimage-extract
$ ./squashfs-root/AppRun
On Windows
Everything above has a Windows equivalent, in Windows PowerShell — the one Windows already has; open it from the Start menu. This section was run on a GitHub Actions Windows machine (Windows Server 2025, build 26100, Windows PowerShell 5.1), with the downloads marked as coming from the Internet as a browser marks them. It has not been run on a Windows desktop: what Windows’ own security prompts do there is not verified.
Download
On the release page,
under Assets, download two files. Your browser saves them in Downloads:
anvil-v0.5.0-x86_64-windows.zip— the engine;SHA256SUMS— the checksums, to confirm the download is intact.
Check and unpack
PS> mkdir $HOME\anvil-book
PS> cd $HOME\anvil-book
PS> Move-Item "$HOME\Downloads\anvil-v0.5.0-x86_64-windows.zip", "$HOME\Downloads\SHA256SUMS" .
PS> (Get-FileHash .\anvil-v0.5.0-x86_64-windows.zip -Algorithm SHA256).Hash
2F2AA5AF9044B2BBBA42E66434361EBC220EF1849E73F927FFC50121307EC96A
PS> Select-String anvil-v0.5.0-x86_64-windows.zip .\SHA256SUMS
C:\Users\you\anvil-book\SHA256SUMS:5:2f2aa5af9044b2bbba42e66434361ebc220ef1849e73f927ffc50121307ec96a anvil-v0.5.0-x86_64-windows.zip
The two hashes must be the same digits; PowerShell prints them in capitals and the file in lower case. Then unpack:
PS> Expand-Archive .\anvil-v0.5.0-x86_64-windows.zip -DestinationPath .
It creates anvil-v0.5.0-x86_64-windows\, with anvil.exe and
anvil-exec-wasm.exe. They are built not to need the Visual C++ runtime; the
test machine had it installed anyway, so a machine without it is not
verified.
If you unpack with the Explorer instead (right click → Extract All), the files
inside may keep the “downloaded from the Internet” mark. On the test machine
anvil.exe ran from PowerShell with the mark on; if your Windows refuses to run
it, remove the mark from the whole folder:
PS> Get-ChildItem -Recurse .\anvil-v0.5.0-x86_64-windows | Unblock-File
Neither anvil.exe nor the editor installer is digitally signed in 0.5.0, so
Windows may warn before running them. That warning was not reproduced here.
Put it on the path
In every PowerShell window you open for this book:
PS> $env:Path = "$PWD\anvil-v0.5.0-x86_64-windows;$env:Path"
PS> anvil --version
anvil 0.5.0
The SDK and the project
The same commands as on Linux, and git and the .NET 10 SDK must be installed first:
PS> git clone --depth 1 --branch v0.5.0 https://github.com/anlaco/anvil.git anvil
PS> dotnet --version
PS> dotnet new console --output Bench
C:\Users\you\anvil-book\
├── anvil\ the repository, for the SDK
├── anvil-v0.5.0-x86_64-windows\ the engine
└── Bench\ your steps
The rest of the book on Windows
The chapters are written for a Linux shell. Chapters 3 and 4 were also run on
the Windows machine, up to running sequences/first.yseq and reading its JSON
report: the C# project, dotnet run and anvil behave the same and print the
same output. Paths work with either / or \. Where a command in the book is
shell syntax, use:
| in the book | in Windows PowerShell |
|---|---|
~/anvil-book | $HOME\anvil-book |
export PATH="$PWD/anvil-v0.5.0-x86_64-linux-musl:$PATH" | $env:Path = "$PWD\anvil-v0.5.0-x86_64-windows;$env:Path" |
2>/dev/null | 2>$null |
echo $? | $LASTEXITCODE |
cat data.json | Get-Content data.json |
ss -ltn | grep 9201 | Get-NetTCPConnection -LocalPort 9201 -State Listen |
A few sessions pipe into tail or grep only to shorten what they show; on
Windows, run the command without them. Chapters 5 to 13 and the Python and Rust
steps of chapter 12 have not been run on Windows.
The Sequence Editor on Windows
Download anvil-editor-v0.5.0-x86_64-windows-setup.exe from the same page and
run it. On the test machine it installed for the current user, into
%LOCALAPPDATA%\Programs\@anvileditor\, and added Anvil Sequence Editor to
the Start menu. That was checked with a silent install (setup.exe /S); the
installer’s own window, and whether it asks for administrator rights, were not.
Remember that in 0.5.0 the packaged editor cannot run a sequence
(#80).
3. Your first step
A step is a C# method with an attribute on it. In this chapter you write one
that pretends to measure a supply rail, and you turn Bench/ into an executor
that serves it.
Point the project at the SDK
Replace the contents of Bench/Bench.csproj with:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../anvil/executors/csharp/src/Anvil.Step/Anvil.Step.csproj" />
<ProjectReference Include="../anvil/executors/csharp/src/Anvil.Step.Generator/Anvil.Step.Generator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>
The two references are the SDK and its generator: at compile time, the
generator reads your methods and writes the list of steps your executor
publishes, so there is no registration file to keep up to date. The paths are
relative to Bench/, which is why the repository had to be cloned as
anvil next to it. When Anvil.Step is on NuGet, both lines become a single
PackageReference.
The entry point
Replace Bench/Program.cs with this one line:
return await Anvil.Step.StepHost.RunAsync(args, Anvil.Step.Generated.AnvilSteps.Register);
AnvilSteps.Register is the code the generator wrote. StepHost.RunAsync
reads the command line, starts a server and waits for Anvil to call.
The step
Create Bench/Board.cs:
using Anvil.Step;
/// <summary>Checks on the board under test.</summary>
public static partial class Board
{
/// <summary>Measures the supply rail, in volts.</summary>
[Step]
public static double MeasureRail() => 4.98;
}
That is a complete step. What each part does:
[Step]publishes the method.- The name comes from the code, in
snake_case: the classBoardis the moduleboard, the methodMeasureRailis the stepmeasure_rail, and sequences call itboard/measure_rail. - The
<summary>is the step’s description. Anvil shows it, so you write it once, next to the code it describes. - Returning a
doublemeans “this is a measurement”. Notice what is not here: nothing says whether 4.98 V is good. That belongs to the sequence.
The class is partial only so that later chapters can add steps to the same
module in files of their own.
A step can return:
| returns | means |
|---|---|
double | a measurement, for the sequence’s limit to judge |
bool | true is pass, false is fail — the step judges, nothing is measured |
void | pass, when the step is an action |
Outcome | anything else: Outcome.Measured(...), Passed, Failed, Errored, with a message |
Ask the executor what it serves
$ dotnet run --project Bench -- --list
life 275f5aeae20e4fc5acad7e3f418a6717, contract 4
board/measure_rail() — Measures the supply rail, in volts.
--list prints the catalog and exits, without starting anything. The life
line identifies this run of the executor and changes every time; contract 4
is the version of the protocol Anvil and the executor speak. The first run also
compiles the project, which takes a little while.
Serve it
Now start it for real, and leave this terminal open:
$ dotnet run --project Bench -- --port 9201
anvil C# executor: 1 step(s) on 127.0.0.1:9201, life bfd160da9cfc4688bd98c5eaa03a6736
The executor is listening on port 9201, only on your own machine
(127.0.0.1). Anvil does not start a C# executor for you: the process is
yours, so you bring it up, and you stop it with Ctrl+C.
| flag | |
|---|---|
--port | where to listen; 9201 by default |
--bind | which interface; 127.0.0.1 by default |
--list | print the catalog and exit |
Every time you change a step, stop the executor and start it again. The
running process serves the code it was built from; dotnet run rebuilds it.
4. Your first sequence
Open a second terminal — the first one is busy serving your step — and set it up:
$ cd ~/anvil-book
$ export PATH="$PWD/anvil-v0.5.0-x86_64-linux-musl:$PATH"
$ mkdir sequences
The file
Create sequences/first.yseq:
name: first
executors:
- { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }
main:
- name: board/measure_rail
executor: bench
limit: { type: range, min: 4.75, max: 5.25 }
It is YAML. Line by line:
namenames the sequence in reports.executorsis the list of executors this sequence talks to. Each has anameyou choose, atype—grpcfor a process listening on a port, as your C# executor is — and where to find it.mainis the list of steps to run. Chapter 6 addssetupandcleanup.- Each step has the
namethe executor publishes and theexecutorthat serves it, by the name you gave it above. limitis the acceptance criterion: arangepasses when the measurement is betweenminandmax.
Always write executor: on a step. A step without one is sent to a small
executor built into the engine for its own demonstrations, which does not know
your steps.
The extension can be .yseq or .yaml; Anvil reads both the same way.
Run it
$ anvil sequences/first.yseq
ejecutor de pasos escuchando en 40243
secuencia 'first' cargada (1 pasos en main, 0 subsecuencia(s) externa(s), 1 ejecutor(es))
motor conectado
conectado a los ejecutores de pasos (embebido en 127.0.0.1:40243)
catálogo pedido
1 paso(s) comprobados contra el catálogo de su ejecutor
=== first: pass ===
[pass] board/measure_rail:
conexión cerrada; esperando otra
$ echo $?
0
Two kinds of lines are mixed there.
The report is the part between === first: pass === and the step lines
under it: the sequence passed, and so did its one step. It goes to standard
output.
Everything else is diagnostics on the error stream: the engine starting its
built-in executor on a free port, loading the file, connecting, and — the line
that matters — 1 paso(s) comprobados contra el catálogo de su ejecutor,
“1 step checked against its executor’s catalog”. Before running anything,
Anvil asked bench what it serves and checked that the sequence only asks for
steps that exist. These messages are still in Spanish in 0.5.0
(#59).
The exit code is 0 because the sequence passed. Anything else — a fail,
an error, a file that does not load — exits with 1.
To see only the report, send the diagnostics away:
$ anvil sequences/first.yseq 2>/dev/null
=== first: pass ===
[pass] board/measure_rail:
The rest of the book does that, unless the diagnostics are the point.
The console report does not show the measured value — 4.98 appears nowhere. The reports in chapter 10 carry it.
When a name is wrong
Make a typo on purpose. sequences/unknown.yseq asks for measure_rial:
name: unknown
executors:
- { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }
main:
- name: board/measure_rial
executor: bench
$ anvil sequences/unknown.yseq 2>&1 | tail -n 3
la secuencia no casa con lo que ofrecen los ejecutores (1 problema(s)):
- step 'board/measure_rial': executor 'bench' does not serve it (it serves: board/measure_rail)
conexión cerrada; esperando otra
“The sequence does not match what the executors offer”: bench does not serve
board/measure_rial, and the message lists what it does serve. Nothing ran.
This check happens on every run, before the first step, so a misspelt step
never gets as far as the bench.
5. Measuring and judging
A step reports what it saw. The sequence says what is acceptable. Anvil compares the two. Keeping the criterion out of the step is what lets you tighten a tolerance by editing a text file that anyone can review, without rebuilding the executor.
Three more steps
Create Bench/Board.Judging.cs, which adds three steps to the board module:
using Anvil.Step;
public static partial class Board
{
/// <summary>Measures the leakage current with the board idle, in amps.</summary>
[Step]
public static double MeasureLeakage() => 0.0004;
/// <summary>Checks that the power LED is lit.</summary>
[Step]
public static bool CheckLed() => true;
/// <summary>Reads the on-board temperature sensor.</summary>
[Step]
public static Outcome ReadTemperature() =>
Outcome.Errored("the temperature sensor did not answer");
}
Stop the executor with Ctrl+C in its terminal and start it again with
dotnet run --project Bench -- --port 9201, so it serves the new steps.
Limits
sequences/judging.yseq uses each kind of judgement:
name: judging
executors:
- { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }
main:
- name: board/measure_rail
executor: bench
limit: { type: range, min: 4.75, max: 5.25 }
- name: board/measure_leakage
executor: bench
limit: { type: comparison, op: le, expected: 0.001 }
- name: board/check_led
executor: bench
$ anvil sequences/judging.yseq 2>/dev/null
=== judging: pass ===
[pass] board/measure_rail:
[pass] board/measure_leakage:
[pass] board/check_led:
board/measure_railreturns a number and has arangelimit.board/measure_leakagehas acomparison: the measurement is compared withexpectedusingop, one ofeq,ne,lt,le,gtorge. Here, “less than or equal to 1 mA”.board/check_ledreturns abool, so the step itself decides pass or fail and needs no limit.
Now the same rail against a limit it cannot meet, in
sequences/rail-tight.yseq:
name: rail_tight
executors:
- { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }
main:
- name: board/measure_rail
executor: bench
limit: { type: range, min: 5.0, max: 5.25 }
$ anvil sequences/rail-tight.yseq 2>/dev/null
=== rail_tight: fail ===
[fail] board/measure_rail: 4.98 fuera de rango [5, 5.25]
$ echo $?
1
The step still returned 4.98 and did not change; the verdict did. The message
reads “4.98 out of range [5, 5.25]”, and the exit code is 1.
Fail is about the unit, error is about the bench
board/read_temperature does not measure anything: it answers
Outcome.Errored, as a real step would when a sensor does not reply.
name: error
executors:
- { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }
main:
- name: board/read_temperature
executor: bench
- name: board/check_led
executor: bench
$ anvil sequences/error.yseq 2>/dev/null
=== error: error ===
[error] board/read_temperature: the temperature sensor did not answer
$ echo $?
1
The verdict is error, not fail, and the difference is deliberate. A fail
is a statement about the unit — it is out of spec, reject it. An error says
Anvil could not judge the unit at all: the bench is broken, the unit may be
perfectly good, and treating it as rejected would scrap a good board.
So when you write a step:
- If you measured, return the measurement and let the limit judge.
- If the step itself can decide, return
boolorOutcome.Failed. - If you could not measure or decide, return
Outcome.Erroredwith a message saying why. Never return a fail to mean “something went wrong”.
An exception that escapes your method also becomes error, and the executor
keeps running.
Two more things the output shows:
mainstops at the first step that does not pass.board/check_lednever ran.- A sequence’s verdict is the worst of its steps: any
errormakes iterror; otherwise anyfailmakes itfail. Both exit with1; the report tells them apart.
6. Setup, main, cleanup and retries
Real tests power a unit before measuring it and must power it off afterwards, whatever happened in between. Sequences have three phases for that.
Create Bench/Fixture.cs and restart the executor:
using Anvil.Step;
/// <summary>The test fixture the board sits in.</summary>
public static class Fixture
{
/// <summary>Clamps the board and powers it.</summary>
[Step]
public static void PowerOn() { }
/// <summary>Cuts power and releases the board.</summary>
[Step]
public static void PowerOff() { }
/// <summary>Opens the link to the board; the first attempt always times out.</summary>
[Step]
public static Outcome Connect(Ctx ctx) =>
ctx.Attempt == 1
? Outcome.Errored("no answer from the board")
: Outcome.Passed("connected on attempt " + ctx.Attempt);
/// <summary>Measures the rail while it settles: low on the first attempt.</summary>
[Step]
public static double MeasureSettlingRail(Ctx ctx) => ctx.Attempt == 1 ? 4.1 : 4.97;
}
Ctx is optional: declare it as a parameter and the SDK hands you the attempt
number, among other things. It is not an input the sequence sends.
Three phases
name: phases
executors:
- { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }
setup:
- name: fixture/power_on
executor: bench
main:
- name: fixture/measure_settling_rail
executor: bench
retries: 3
limit: { type: range, min: 4.75, max: 5.25 }
- name: board/check_led
executor: bench
cleanup:
- name: fixture/power_off
executor: bench
$ anvil sequences/phases.yseq 2>/dev/null
=== phases: fail ===
[pass] fixture/power_on:
[fail] fixture/measure_settling_rail: 4.1 fuera de rango [4.75, 5.25]
[pass] fixture/power_off:
setupruns first. If any setup step does not pass,mainis skipped entirely.mainruns next and stops at its first step that does not pass.cleanupalways runs, whatever happened before.fixture/power_offran even though the measurement failed. Put everything that makes the bench safe here.
Retries do not re-measure a failed limit
That sequence has retries: 3 on fixture/measure_settling_rail, and the step
reads 4.97 V from its second attempt on. Yet the report shows 4.1 and fail.
This is how 0.5.0 behaves
(#76): retries repeats a step
only while the step itself does not pass. The limit is applied once, after the last attempt. A
step that returns a measurement has passed as far as retries are concerned, so
it is not called again, and the limit then fails the first value.
Retries are for a step that fails or errors on its own — a link that times out once, for example:
name: retries
executors:
- { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }
setup:
- name: fixture/power_on
executor: bench
- name: fixture/connect
executor: bench
retries: 3
main:
- name: board/measure_rail
executor: bench
limit: { type: range, min: 4.75, max: 5.25 }
cleanup:
- name: fixture/power_off
executor: bench
$ anvil sequences/retries.yseq 2>/dev/null
=== retries: pass ===
[pass] fixture/power_on:
[pass] fixture/connect: connected on attempt 2
[pass] board/measure_rail:
[pass] fixture/power_off:
fixture/connect errored on attempt 1 and passed on attempt 2. retries is
the total number of attempts: the default is 1, meaning no retry, and 0 is
refused when the file loads.
The report keeps only the last attempt. The “on attempt 2” in the message is there because the step wrote it; Anvil does not record the earlier attempt.
Without the retries, the same link does not come up, and the setup rule shows:
name: setup_fails
executors:
- { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }
setup:
- name: fixture/power_on
executor: bench
- name: fixture/connect
executor: bench
main:
- name: board/measure_rail
executor: bench
limit: { type: range, min: 4.75, max: 5.25 }
cleanup:
- name: fixture/power_off
executor: bench
$ anvil sequences/setup-fails.yseq 2>/dev/null
=== setup_fails: error ===
[pass] fixture/power_on:
[error] fixture/connect: no answer from the board
[pass] fixture/power_off:
The setup errored, main never ran, and cleanup still did.
7. Inputs, variables and flow
So far every step has taken nothing and returned a fixed value. Real steps take parameters — which channel, which range — and later steps depend on earlier results.
Create Bench/Dmm.cs and Bench/Unit.cs, then restart the executor:
using Anvil.Step;
/// <summary>A multimeter with four input channels.</summary>
public static class Dmm
{
/// <summary>Measures the voltage on a channel.</summary>
/// <param name="channel">The input channel, 1 to 4.</param>
/// <param name="range">The measurement range.</param>
[Step]
public static Outcome MeasureVoltage(double channel, string range = "auto") =>
channel switch
{
1 => Outcome.Measured(1.1, "range " + range),
2 => Outcome.Measured(1.8, "range " + range),
3 => Outcome.Measured(3.3, "range " + range),
4 => Outcome.Measured(5.0, "range " + range),
_ => Outcome.Errored("the multimeter has no such channel"),
};
}
using Anvil.Step;
/// <summary>The unit's own identity.</summary>
public static class Unit
{
/// <summary>Reads the serial number from the unit's memory.</summary>
[Step]
public static Outcome ReadSerial() =>
Outcome.Passed("serial read").Output("serial", "SN-0042");
}
Inputs
A method’s parameters are the step’s inputs, named in snake_case like the
step. channel has no default, so it is required; range has one, so it is
optional. A parameter can be a double, a string, a bool, or a reference
(chapter 8); anything else does not compile.
--list shows that signature — dmm/measure_voltage(channel: number, range?: text) —
and Anvil checks every sequence against it.
Variables and results
name: data
executors:
- { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }
locals:
channel: 2
rail: 0.0
led_ok: false
main:
- name: unit/read_serial
executor: bench
- name: dmm/measure_voltage
executor: bench
inputs:
channel: '${locals.channel}'
range: "10V"
limit: { type: range, min: 1.7, max: 1.9 }
assign:
rail: result.measured_value
- name: board/check_led
executor: bench
assign:
led_ok: 'result.status == "pass"'
- name: rail_in_spec
type: pass_fail
condition: 'locals.rail > 1.75 && locals.led_ok'
$ anvil sequences/data.yseq --json data.json 2>/dev/null
=== data: pass ===
[pass] unit/read_serial: serial read
[pass] dmm/measure_voltage: range 10V
[pass] board/check_led:
[pass] rail_in_spec: condición cumplida
$ cat data.json
{
"sequence": "data",
"status": "pass",
"skipped_steps": 0,
"total_steps": 4,
"steps": [
{
"name": "unit/read_serial",
"status": "pass",
"phase": "main",
"message": "serial read",
"measured_value": null,
"limit_min": null,
"limit_max": null,
"expected_value": null,
"operator": null,
"inputs": {},
"outputs": {
"serial": "SN-0042"
}
},
{
"name": "dmm/measure_voltage",
"status": "pass",
"phase": "main",
"message": "range 10V",
"measured_value": 1.8,
"limit_min": 1.7,
"limit_max": 1.9,
"expected_value": null,
"operator": null,
"inputs": {
"channel": 2.0,
"range": "10V"
},
"outputs": {}
},
{
"name": "board/check_led",
"status": "pass",
"phase": "main",
"message": "",
"measured_value": null,
"limit_min": null,
"limit_max": null,
"expected_value": null,
"operator": null,
"inputs": {},
"outputs": {}
},
{
"name": "rail_in_spec",
"status": "pass",
"phase": "main",
"message": "condición cumplida",
"measured_value": null,
"limit_min": null,
"limit_max": null,
"expected_value": null,
"operator": null,
"inputs": {},
"outputs": {}
}
]
}
What happens there:
localsdeclares the sequence’s variables with their initial values. The type comes from the value:2and0.0are numbers,falseis a boolean,""would be text.inputsgives a step its parameters. A plain value is sent as it is ("10V"); a value in${...}is an expression, evaluated before the step runs (${locals.channel}sends2).assignstores something from the step’sresultin a local after it runs:result.measured_value, or an expression such asresult.status == "pass".- A step with
type: pass_failcalls no executor. Anvil evaluates itsconditionand the step passes or fails on it — the place for a verdict that combines several measurements.
The JSON report (chapter 10) records what each step was given in inputs and
what it returned in outputs. That is what makes a result reconstructible
later: two runs on different channels produce different reports.
Outputs from a C# step cannot be assigned yet
unit/read_serial returns a named output, serial, and the report above shows
it. But reading it into a variable does not work in 0.5.0:
name: serial
executors:
- { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }
locals:
serial: ""
main:
- name: unit/read_serial
executor: bench
assign:
serial: result.outputs.serial
$ anvil sequences/serial.yseq 2>&1 | tail -n 3
la secuencia no casa con lo que ofrecen los ejecutores (1 problema(s)):
- step 'unit/read_serial' (bench): 'assign' reads result.outputs.serial and the step does not return it (it returns: none)
conexión cerrada; esperando otra
Anvil checks assign against the outputs a step declares in its catalog, and
the C# SDK has no way yet to declare them — only the open step of chapter 8
declares one (#77). So the check refuses the sequence before it runs. Until that is
fixed, use named outputs from C# for the report only, and pass values between
steps through result.measured_value or result.status.
Flow
name: flow
executors:
- { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }
file_globals:
station: "bench-3"
locals:
channel: 1
main:
- name: pick_channel
type: statement
statement: 'locals.channel = locals.channel + 2'
- name: dmm/measure_voltage
executor: bench
inputs: { channel: '${locals.channel}' }
limit: { type: range, min: 3.0, max: 3.6 }
- name: board/measure_leakage
executor: bench
precondition: 'file_globals.station == "bench-1"'
limit: { type: comparison, op: le, expected: 0.001 }
- name: board/read_temperature
executor: bench
disable: true
$ anvil sequences/flow.yseq 2>/dev/null
=== flow: pass ===
[pass] pick_channel: statement ok
[pass] dmm/measure_voltage: range auto
[skipped] board/measure_leakage: precondición falsa
[skipped] board/read_temperature: disable
(2 de 4 pasos saltados)
type: statementruns an assignment inside the engine, without calling any executor:locals.channelbecomes 3 before the measurement uses it.file_globalsare variables visible to every sequence in the file. Steps read them; writing to one is refused when the file loads.preconditionis an expression; when it is false the step isskippedwithout being called. Here the station is notbench-1.disable: trueskips a step without evaluating anything.
Look at the verdict: the sequence passed with two steps skipped. A skipped step is neither a pass nor a fail, and it does not fail the sequence. If a measurement must always happen, do not put a precondition on it.
Checking a sequence without running it
A typo in an input name, in sequences/typo.yseq:
name: typo
executors:
- { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }
main:
- name: dmm/measure_voltage
executor: bench
inputs: { chanel: 2 }
$ anvil sequences/typo.yseq --validate
secuencia 'typo' cargada (1 pasos en main, 0 subsecuencia(s) externa(s), 1 ejecutor(es))
'typo' válida (1 paso(s) en main, 0 subsecuencia(s) externa(s))
$ anvil sequences/typo.yseq --validate --with-executors
ejecutor de pasos escuchando en 46505
secuencia 'typo' cargada (1 pasos en main, 0 subsecuencia(s) externa(s), 1 ejecutor(es))
'typo' válida (1 paso(s) en main, 0 subsecuencia(s) externa(s))
motor conectado
catálogo pedido
1 paso(s) comprobados contra el catálogo de su ejecutor
la secuencia no casa con lo que ofrecen los ejecutores (2 problema(s)):
- step 'dmm/measure_voltage' (bench): it takes no input called 'chanel' (it takes: channel, range)
- step 'dmm/measure_voltage' (bench): the input 'channel' is required and the sequence does not send it
conexión cerrada; esperando otra
--validate loads the file and checks it — the schema, the expressions, the
subsequences — without starting or connecting to anything, so it runs in CI
with no bench. It cannot know what inputs a step takes, so it says the file is
valid.
--validate --with-executors also asks each executor for its catalog, and
catches both problems: there is no input chanel, and the required channel
is missing. A real run makes the same check before its first step.
Do not treat a green --with-executors as a guarantee in 0.5.0: when an
executor fails while describing itself, it still passes
(#70).
8. Instruments that stay open
A session with a power supply holds a socket or a vendor driver handle. It cannot be sent to Anvil and back, and it should not be reopened for every step. So it stays inside the executor, and the sequence holds a reference to it: a handle that means something only to the executor that issued it.
In C# you write the class you would write anyway. Create
Bench/PowerSupply.cs and restart the executor:
using Anvil.Step;
/// <summary>A bench power supply, kept open across several steps.</summary>
[StepModule("psu")]
public sealed class PowerSupply
{
private double _volts;
/// <summary>Opens the session with the supply.</summary>
/// <param name="resource">Where the supply is.</param>
[StepConstructor]
public PowerSupply(string resource) => Resource = resource;
/// <summary>Where this supply is.</summary>
public string Resource { get; }
/// <summary>Sets the output voltage, in volts.</summary>
/// <param name="volts">What to set the output to.</param>
[Step]
public void SetVoltage(double volts) => _volts = volts;
/// <summary>Measures the current the load draws, in amps.</summary>
[Step]
public double MeasureCurrent() => _volts * 0.0625;
/// <summary>Cuts the output.</summary>
[Step]
public void OutputOff() => _volts = 0;
}
[StepConstructor]on the constructor publishes a step namedopen—psu/open— that creates the object, keeps it in the executor, and returns a reference to it as an output named after the module,psu.[StepModule("psu")]names the module. Without it the module would bepower_supply, from the class name.- Instance steps —
SetVoltage,MeasureCurrent,OutputOff— take an extra input,psu, of type reference. The SDK finds the object it points to and calls the method on it; your method never sees the handle.
--list shows it: psu/set_voltage(psu: reference, volts: number).
Using it from a sequence
name: supply
executors:
- { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }
locals:
psu: { type: reference, executor: bench }
setup:
- name: psu/open
executor: bench
inputs: { resource: "TCPIP::192.168.0.50::5025::SOCKET" }
assign:
psu: result.outputs.psu
main:
- name: psu/set_voltage
executor: bench
inputs: { psu: '${locals.psu}', volts: 12.0 }
- name: psu/measure_current
executor: bench
inputs: { psu: '${locals.psu}' }
limit: { type: range, min: 0.5, max: 0.8 }
cleanup:
- name: psu/output_off
executor: bench
inputs: { psu: '${locals.psu}' }
$ anvil sequences/supply.yseq --json supply.json 2>/dev/null
=== supply: pass ===
[pass] psu/open:
[pass] psu/set_voltage:
[pass] psu/measure_current:
[pass] psu/output_off:
$ grep -A 6 "\"outputs\": {$" supply.json | head -n 8
"outputs": {
"psu": {
"type": "reference",
"executor": "bench",
"lifetime": "ad96dffbdcf9425fb91af7bf196e34d8",
"payload": "s1"
}
localsdeclarespsuas a reference that comes frombench. It is the one kind of variable with no initial value: you cannot write a reference by hand, only receive it from a step.setupopens the supply and stores the reference withassign. This is the one output a C# step can have assigned in 0.5.0 (see chapter 7).mainpasses${locals.psu}to each step that needs the supply.cleanupturns the output off, which runs whatever happens inmain.
The report records the reference like any other input or output, so you can
tell afterwards which session each step used. lifetime identifies the run of
the executor that issued it — the same value as the life it prints when it
starts — and payload is the executor’s own name for the object.
9. Subsequences
When the same few steps appear in several places, move them into a subsequence and call it. A subsequence can live in the same file, or in a file of its own that several sequences share.
This chapter needs no new steps.
A file to share
sequences/rail-check.yseq measures one channel and hands the value back to
whoever called it:
name: rail_check
parameters:
channel: 1
volts: 0.0
locals:
measured: 0.0
main:
- name: dmm/measure_voltage
executor: bench
inputs: { channel: '${parameters.channel}' }
assign:
measured: result.measured_value
- name: return_volts
type: statement
statement: 'parameters.volts = locals.measured'
parametersare what the caller passes in, with default values.- It has no
executors:section. The executors are declared once, in the sequence you run, and subsequences refer to them by name. Anvil refuses a subsequence that declares its own. It also means this file cannot be run on its own. - Returning a value takes two steps.
assignalways writes to a local, so the measurement goes tolocals.measured, and astatementcopies it to the parameter the caller will read. Assigning directly to a parameter is refused when the file loads.
Calling it
name: board
executors:
- { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }
locals:
ch_3v3: 3
v_3v3: 0.0
ch_1v1: 1
v_1v1: 0.0
subsequences:
power_up:
setup:
- name: fixture/power_on
executor: bench
main:
- name: board/check_led
executor: bench
main:
- name: power_up
type: sequence_call
sequence: power_up
- name: measure_3v3
type: sequence_call
sequence: ./rail-check.yseq
args: { channel: locals.ch_3v3, volts: locals.v_3v3 }
- name: measure_1v1
type: sequence_call
sequence: ./rail-check.yseq
args: { channel: locals.ch_1v1, volts: locals.v_1v1 }
- name: rails_in_spec
type: pass_fail
condition: 'locals.v_3v3 > 3.2 && locals.v_3v3 < 3.4 && locals.v_1v1 > 1.0 && locals.v_1v1 < 1.2'
cleanup:
- name: fixture/power_off
executor: bench
$ anvil sequences/board.yseq 2>/dev/null
=== board: pass ===
[pass] power_up: sequence call 'power_up' → pass
[pass] fixture/power_on:
[pass] board/check_led:
[pass] measure_3v3: sequence call 'sequences/rail-check.yseq' → pass
[pass] dmm/measure_voltage: range auto
[pass] return_volts: statement ok
[pass] measure_1v1: sequence call 'sequences/rail-check.yseq' → pass
[pass] dmm/measure_voltage: range auto
[pass] return_volts: statement ok
[pass] rails_in_spec: condición cumplida
[pass] fixture/power_off:
subsequencesholdspower_up, a subsequence that lives in this file. It has its ownsetupandmain.- A step with
type: sequence_callcalls one.sequenceis either the name of a subsequence in the same file (power_up) or a path to a file, relative to the file doing the calling (./rail-check.yseq). argsconnects each parameter to one of the caller’s locals —channel: locals.ch_3v3. It is written without${...}because it is not a value: it names the variable, and when the subsequence writes the parameter, the caller’s local changes. That is howlocals.v_3v3gets its value.
The report nests each subsequence’s steps under the call — indented on the
console, under sub_steps in the JSON report — and the call takes the verdict
of the subsequence: if a step inside errors, the call is an error too, and
the calling sequence stops as it would for any other step.
10. Reports and running unattended
On a production line nobody reads the console. This chapter is about the files Anvil leaves behind and the options for running without a person in front of it.
Reports
--json <file> and --csv <file> write the report to a file, in addition to
the console. --quiet removes the console report:
$ anvil sequences/judging.yseq --quiet --csv judging.csv
ejecutor de pasos escuchando en 46037
motor conectado
catálogo pedido
conexión cerrada; esperando otra
$ cat judging.csv
sequence_name,status,step_name,step_status,message,measured_value,limit_min,limit_max,expected_value,operator,phase,inputs,outputs
judging,pass,board/measure_rail,pass,,4.98,4.75,5.25,,,main,,
judging,pass,board/measure_leakage,pass,,0.0004,,,0.001,<=,main,,
judging,pass,board/check_led,pass,,,,,,,main,,
One row per step. Unlike the console, the files carry the measurement and the
limit it was judged against: measured_value with limit_min and limit_max
for a range, expected_value and operator for a comparison. They also carry
the step’s inputs and outputs, and a subsequence’s steps are prefixed with
the name of their call. The JSON report has the same information, nested
(chapter 7 showed one).
Notice that --quiet did not silence the diagnostics on the error stream
(#35). If a script needs silence,
redirect it.
Changing limits without touching the sequence
A tolerance often depends on the product variant or the production lot, not on
the test. --limits loads a limits file that replaces the limits written in
the sequence, by step name. sequences/judging.limits.yaml:
board/measure_rail:
type: range
min: 5.0
max: 5.1
$ anvil sequences/judging.yseq --limits sequences/judging.limits.yaml 2>/dev/null
=== judging: fail ===
[fail] board/measure_rail: 4.98 fuera de rango [5, 5.1]
The same sequence, a tighter tolerance, a different verdict. The limits file applies to every step with that name, in subsequences too.
A name that matches no step would leave the sequence’s own limit in force
without anyone noticing, so Anvil warns — even under --quiet. With
sequences/judging.typo.limits.yaml:
board/measure_rial:
type: range
min: 5.0
max: 5.1
$ anvil sequences/judging.yseq --limits sequences/judging.typo.limits.yaml
ejecutor de pasos escuchando en 40627
secuencia 'judging' cargada (3 pasos en main, 0 subsecuencia(s) externa(s), 1 ejecutor(es))
sidecar de límites 'sequences/judging.typo.limits.yaml' aplicado (0 paso(s) afectado(s))
aviso: 1 límite(s) del sidecar 'sequences/judging.typo.limits.yaml' no afectan a ningún paso: board/measure_rial
aviso: el sidecar no afectó a ningún paso. Comprueba que los nombres coincidan con los de los pasos de la secuencia
motor conectado
conectado a los ejecutores de pasos (embebido en 127.0.0.1:40627)
catálogo pedido
3 paso(s) comprobados contra el catálogo de su ejecutor
=== judging: pass ===
[pass] board/measure_rail:
[pass] board/measure_leakage:
[pass] board/check_led:
conexión cerrada; esperando otra
The aviso lines say that one limit in the file affects no step,
board/measure_rial, and that the file changed nothing: check the names. The
sequence passed against its own limit.
Moving an executor without editing the sequence
--executor name=host:port points a declared executor somewhere else for this
run — the same sequence against the executor on your desk or on the line’s PC:
$ anvil sequences/first.yseq --executor bench=127.0.0.1:9201
The name must be one the sequence declares; any other is an error when the file loads. It can be repeated, once per executor.
Checking in CI
$ anvil sequences/board.yseq --validate
secuencia 'board' cargada (4 pasos en main, 1 subsecuencia(s) externa(s), 1 ejecutor(es))
'board' válida (4 paso(s) en main, 1 subsecuencia(s) externa(s))
$ echo $?
0
--validate needs no executor and no bench, and exits with 0 when the file
is valid, so it can guard every change to a sequence in CI. Remember from
chapter 7 what it cannot see: whether steps and their inputs exist. For that,
--validate --with-executors with the executors running — and, in 0.5.0, with
#70 in mind.
Exit codes
0 when the sequence passes. 1 for everything else: a fail, an error, a
file that does not load, a sequence that does not match its executors, an
executor that cannot be reached. A script can stop on the exit code, and read
the report to know which of those it was.
Following a run as it happens
--events writes one JSON line per event — a step starting, a step’s result —
to the error stream while the sequence runs. It is what the Sequence Editor
reads to light up the running step. --quiet does not silence it, and it
carries the same data as --json, instrument addresses included.
Not covered here
--process-model wraps a sequence in a process model that identifies the unit
and reports on it. The only process model in 0.5.0 relies on the engine’s
built-in demonstration steps, and it is not in the release archive, so this
book leaves it out.
12. Steps in Python and Rust
Everything from chapter 4 on is about the sequence, and the sequence does not
know what language a step is written in. It names the step and the executor.
To show that, this chapter writes board/measure_rail twice more — in Python
and in Rust — and runs it with a sequence that differs from
sequences/first.yseq only in its executors: line.
Writing the step is much the same in all three languages: the function’s signature is the catalog, a returned number is a measurement, and the limit lives in the sequence. What differs is how the executor is started, and that is what this chapter is about.
| C# | Python | Rust | |
|---|---|---|---|
| the executor is | your own program | a server from the repository, pointed at a folder | anvil-exec-wasm, next to your compiled modules |
| who starts it | you | you | Anvil, when a sequence uses it |
executor type | grpc | grpc | wasm |
| a step is | a method with [Step] | a function with @step | a function with #[step] |
Python
The Python executor is not in the release archive: it is in the repository you
cloned in chapter 2. It needs grpcio, and the gRPC code it imports has to be
generated once after cloning. From ~/anvil-book:
$ python3 -m venv .venv
$ .venv/bin/pip install grpcio grpcio-tools
$ cd anvil/executors/python
$ ../../../.venv/bin/python -m grpc_tools.protoc -I ../../crates/modelo --python_out=. --grpc_python_out=. ../../crates/modelo/paso.proto
$ cd ~/anvil-book
If python3 -m venv fails, your distribution may ship it as a separate
package — python3-venv on Debian and Ubuntu. That case is not verified
in this book.
A step is a decorated function. Create python_steps/board.py:
from anvil_step import step
@step
def measure_rail() -> float:
"""Measures the supply rail, in volts."""
return 4.98
The file name is the module, so this is board/measure_rail again. Returning a
float is a measurement, a bool is pass or fail, and Result.error(...)
reports what could not be judged, as in C#. An exception becomes error.
Start the executor in its own terminal and leave it running:
$ .venv/bin/python anvil/executors/python/anvil-exec-python --steps python_steps
module 'board' (/home/you/anvil-book/python_steps/board.py) sha256:831270a42ae7bb6a050f189a2dcf434cf8cc44ca68122871a74e84f3808009e6
python executor listening on 127.0.0.1:9101 — 1 step(s) from 1 module(s): board/measure_rail
It serves every module in the folder given to --steps, on port 9101 by
default. You never edit the server; to add steps, add files to the folder and
restart it.
name: python
executors:
- { name: py, type: grpc, host: 127.0.0.1, port: 9101 }
main:
- name: board/measure_rail
executor: py
limit: { type: range, min: 4.75, max: 5.25 }
$ anvil sequences/python.yseq 2>/dev/null
=== python: pass ===
[pass] board/measure_rail:
The Python executor’s own README, executors/python/README.md,
covers the rest: named outputs, references and options. It is still in Spanish.
Rust
Rust steps are compiled to WebAssembly modules, and the executor is a program
that ships in the release archive, anvil-exec-wasm. It serves every .wasm
file in the folder it sits in. You need a Rust toolchain and one extra target:
$ rustup target add wasm32-wasip2
Create board-wasm/Cargo.toml — the package name is the module name:
[package]
name = "board"
version = "0.1.0"
edition = "2021"
[dependencies]
anvil-step = { path = "../anvil/executors/rust/anvil-step" }
[lib]
crate-type = ["cdylib"]
and board-wasm/src/lib.rs:
#![allow(unused)]
fn main() {
use anvil_step::{step, Outcome};
/// Measures the supply rail, in volts.
#[step]
fn measure_rail() -> Outcome {
Outcome::measured(4.98)
}
anvil_step::export!();
}
Build it, and put the module next to a copy of the executor. That folder is the executor, and you can copy it to another machine as it is:
$ cargo build --target wasm32-wasip2 --manifest-path board-wasm/Cargo.toml
$ mkdir wasm-dept
$ cp anvil-v0.5.0-x86_64-linux-musl/anvil-exec-wasm board-wasm/target/wasm32-wasip2/debug/board.wasm wasm-dept/
The first build takes about a minute. Then:
name: wasm
executors:
- { name: rs, type: wasm, path: ../wasm-dept/anvil-exec-wasm }
main:
- name: board/measure_rail
executor: rs
limit: { type: range, min: 4.75, max: 5.25 }
$ anvil sequences/wasm.yseq 2>/dev/null
=== wasm: pass ===
[pass] board/measure_rail:
$ ./wasm-dept/anvil-exec-wasm --list
board sha256:2087caa515293f3a0cda2b14c78a09d6d9b2bfb98a5e2c63916c872cb1d807d5
/home/you/anvil-book/wasm-dept/board.wasm
board/measure_rail()
Measures the supply rail, in volts.
There was no executor to start: with type: wasm, Anvil starts
anvil-exec-wasm itself from path. That path is relative to the sequence
file, not to the folder you run anvil from — hence the ../.
anvil-exec-wasm --list shows each module with the SHA-256 of the file that
serves it.
The step-by-step Rust quick start
goes further, with two modules and optional inputs, and
executors/rust/README.md covers the SDK.
13. When something goes wrong
The executor is not running
The most common mistake: you run a sequence and forgot to start the executor
in the other terminal. In 0.5.0 Anvil does not say so clearly. It retries the
connection for several seconds, printing hundreds of lines of motor conectado
and conexión cerrada; esperando otra (“engine connected”, “connection
closed; waiting for another”), and then ends with:
$ anvil sequences/first.yseq 2>&1 | tail -n 2
conexión cerrada; esperando otra
no se pudo conectar a los ejecutores de pasos (embebido en 127.0.0.1:33973): WASI socket error: ErrorCode { code: 14, name: "connection-refused", message: "The TCP connection was forcefully rejected" }
“Could not connect to the step executors … connection refused”. Two things in
that message are misleading. It names embebido, the engine’s built-in
executor, and a port that is not the one you declared — but the executor that
refused is yours, bench on 9201
(#78). When you see connection-refused, check
first that every grpc executor in the sequence is running:
$ ss -ltn | grep 9201
prints a line when something is listening on that port, and nothing when not.
An old executor is still running
The opposite problem is worse, because nothing fails. If an executor from an earlier session is still listening on the port — started in a terminal you closed, or left behind by a crash — Anvil talks to it, and it answers with whatever code it was built from. You change a step, run the sequence, and see the old behaviour.
Before chasing a result that makes no sense, see who owns the port:
$ ss -ltnp | grep 9201
The process name and number are at the end of the line; stop it and start your executor again.
If you start a C# executor while another one holds the port, it prints its
usual anvil C# executor: … on 127.0.0.1:9201 line and then stops with
Unhandled exception. System.IO.IOException: Failed to bind to address http://127.0.0.1:9201: address already in use. and a stack trace. Believe the
exception, not the first line
(#79).
I changed a step and nothing changed
The executor serves the code it was started with. Stop it and start it again;
dotnet run rebuilds. The same goes for the Python executor. Rust modules are
read when Anvil starts the executor, so rebuild and copy the .wasm again.
The sequence does not match the executors
la secuencia no casa con lo que ofrecen los ejecutores is Anvil refusing to
run because the sequence asks for something an executor does not offer. The
lines below it are in English and say exactly what: a step the executor does
not serve (chapter 4), an input it does not take or a required one missing
(chapter 7), an output it does not return (chapter 7). Nothing ran.
Messages that name Spanish keys
Some load errors in 0.5.0 still name the Spanish spelling of a key — for
instance, a subsequence with its own executors: section is told to refer to
executors “by name with ejecutor:”. The key is executor:. The sequence
format itself is English only (#59).
The AppImage does not start
dlopen(): error loading libfuse.so.2 means FUSE 2 is not installed; see the
end of chapter 2.