Emulators
An emulator is Signal Lab playing the API, device or service your system talks to. It listens on one address and answers by rules: an HTTP API by routes, an OSC, UDP or TCP device by "on this, reply that", an MQTT broker as any broker does plus rules of its own. It can be slow, fail, or go down now and then, so you can test what your system does when its dependency misbehaves. Every exchange is counted, listed and sent to the Inspector.
An emulator is one document. The Emulators screen keeps a library of them; the same document runs inside an experiment as an Emulator node, from the command line with signallab emulate, and through the API and MCP, and answers the same way everywhere.
The screen
On the left is the library (Library): every emulator with its protocol and address, a pulsing dot and a request count on the ones running. On the right are the selected emulator's settings and rules, and under them what it has received (Live).
Making an emulator
Press one of the buttons at the top of the library:
Button Makes Listens on With one rule that works as it stands + HTTP API An HTTP API 127.0.0.1:18080GET /health→ 200{"status":"ok"}+ OSC device An OSC device 127.0.0.1:9100/ping→/pongwith the count as an int+ UDP device A UDP device 127.0.0.1:7100a datagram containing PING→PONG 1,PONG 2, …+ TCP device A TCP device 127.0.0.1:7200a line containing PING→PONG+ MQTT broker An MQTT broker 127.0.0.1:1883a publish to lab/<name>/set→ the same payload, retained, onlab/<name>/stateWhen another emulator of the library already uses that port, the next free one is taken.
Give it a Name (at most 120 characters).
Set Listen on:
IP:port.127.0.0.1answers this computer only;0.0.0.0answers the network as well.Change the rules (below), and say in Note what it stands in for.
Changes are saved on their own. Duplicate makes a copy on the next free port. Delete asks once more (Delete?), stops the emulator if it runs, and removes it from the library.
Rules are tried in order, first to last; the first that matches answers. Each rule's header shows a one-line summary; click it to open or fold the rule. The ↑ and ↓ buttons move a rule, × removes it.
Running it
- Select the emulator and press Start. Its port opens before the button comes back: a port already taken, or an emulator with a problem, is refused there with the reason.
- Point your system at it. For an HTTP API, Copy URL copies its address (
http://127.0.0.1:18080), and each route has a Copy the URL button for its own (not when its path holds a{{…}}template). - Watch Received fill.
- Press Stop, or stop its job from the console strip.
The state beside the buttons says Not running, where it answers, or that it is down.
An emulator keeps answering with the rules it was started with. When you change it while it runs, Restart appears: press it to start again with the rules as they are now. Until then the hit counts on the rules are hidden, as they belong to the old rules.
Take down makes a running emulator unavailable until you press Bring up: an HTTP request gets 503, a TCP device and an MQTT broker drop their connections and refuse new ones, an OSC or UDP device answers nothing. See Going down.
Two emulators of one transport cannot share a port: HTTP, TCP and MQTT emulators listen on TCP ports, OSC and UDP emulators on UDP ports. An HTTP API and an OSC device can both use port 8080; two HTTP APIs cannot. A second one on a taken port is refused when it starts.
TIP
In a browser connected to a server, the emulator runs on the server. One listening on 0.0.0.0 is reached by the server's name, and Copy URL copies that address; one on 127.0.0.1 answers only programs on the server itself.
What arrived
While it runs, Live counts:
| Count | What |
|---|---|
| Requests | Everything that arrived: requests, messages, lines. |
| No rule | What no rule took. An HTTP request without a route still gets its answer (see Requests no route takes); the others get none. |
| Failed | Exchanges where a reply could not be made or sent. |
| While down | What arrived while the emulator was down. Shown when it has an outage set or something met it down. Never counted as No rule. |
| Not delivered | MQTT only, when it happens: messages a client was too far behind to take. |
Each rule's header shows how many times it matched since the start.
Received lists the newest 300 exchanges, newest first:
| Column | What |
|---|---|
| Time | When it arrived. |
| From | The client's address. |
| Request | What arrived, in protocol notation: GET /users/7, /ping 1, POWER?. |
| Rule | The rule that took it (#2), or —. |
| Reply | What went back: 200 OK · 37 B, /pong 3, a payload; held or closed for a fault; the error when the reply failed; down when it arrived while down. |
| ms | From arrival until the reply left, its delay included. |
The ⌕ button on a row (Open in the Inspector) opens that exchange in the Inspector, when capture was on. When more than 200 exchanges arrive within a fifth of a second, the list skips some and says how many. The engine keeps the newest 500 exchanges of each running emulator, with what arrived, for the command line, the API and MCP.
HTTP API
An HTTP/1.1 server. Each request is answered by the first route that takes it.
Routes
A route takes a request when its method, its path and all its conditions match.
| Field | What |
|---|---|
| Method | GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, or Any. A GET route answers HEAD too. |
| Path | Starts with /. A segment :name takes any one segment, read as {{request.params.name}}; a last segment * takes everything below. A trailing / makes no difference; the query string is not part of the path. |
| Conditions | Every one must hold. Add one with + Condition. |
Path examples:
| Path | Takes | Does not take |
|---|---|---|
/health | /health, /health/ | /health/db, /Health |
/users/:id | /users/7 (params.id is 7), /users/a%20b (a b) | /users, /users/7/orders |
/files/* | /files, /files/a, /files/a/b/c | /file, /other/files/a |
A condition reads one part of the request (Where) and compares it:
| Where | Name | Reads |
|---|---|---|
| Header | A header name, any case | The header's value; a header sent several times, its values joined with , . |
| Query | A query parameter | Its value, decoded; the first, when it is repeated. |
| Body | — | The whole body as text. |
| JSON | A JSON path, such as $.user.id | That field of a JSON body. |
The comparisons are equals, does not equal, less than, at most, greater than, at least, contains, matches regex, is empty and is not empty. Numbers compare as numbers, text exactly. A header, parameter or field that is not there is empty. A comparison that cannot be made — text against a number — does not hold.
Responses
A route has one to 16 responses (Responses).
| Field | What | Default |
|---|---|---|
| Status | 100–599. | 200 |
| Fault | Something other than an answer; see Faults. | None — answer |
| Delay, ms | How long to wait before answering, 0–60 000 ms. | 0 |
| Jitter, ms | Up to this much longer, at random, 0–60 000 ms. | 0 |
| Weight | Its share when the route answers at random. Shown only then. | 1 |
| Headers | Up to 32. Names may use parameters; values are templates. | none |
| Body | A template, up to 256 KiB as written. | empty |
Without a Content-Type header, a body that is valid JSON goes as application/json and any other body as text/plain; charset=utf-8.
With two responses or more, Which response says which one a request gets:
| Which response | Requests get | For |
|---|---|---|
| In sequence, then the last | The first, the second, …, then the last one from then on: 500, 500, 200, 200, 200… | Retries: fail twice, then work. |
| In turn | The first again after the last: 200, 500, 200, 500… | A dependency that fails now and then, regularly. |
| At random, by weight | Each drawn by its weight. Weights 8 and 2 give the first about 80 % of the time. At least one weight must be above 0. | A realistic share of failures. |
Add a response adds a ready response to the route:
| Preset | Adds |
|---|---|
| 200 JSON | 200, {"ok":true} |
| 201 Created | 201, {"id":"{{uuid}}"}, header Location: {{request.path}}/{{counter}} |
| 404 Not found | 404, {"error":"not found"} |
| 500 Server error | 500, {"error":"internal"} |
| 503 Unavailable | 503, {"error":"unavailable"}, header Retry-After: 1 |
| Slow — 2 s | 200, {"ok":true} after 2000 ms |
| No answer | The fault No answer |
| Connection closed | The fault Close the connection |
| Malformed JSON | 200, {"items":[{"id":1},{"id":2}]} with the fault Malformed body |
Faults
| Fault | What the client meets |
|---|---|
| None — answer | The response. |
| No answer | Nothing. The request is held for up to 2 minutes, then the connection is closed — so the client's own timeout is what is tested. The delay does not apply. |
| Close the connection | The connection closes without an answer, after the delay. |
| Malformed body | A complete HTTP answer with the status and headers set, whose body stops halfway: JSON that does not parse. When the whole body was JSON, the content type still says application/json. |
Requests no route takes
Requests no route takes decides what a request that matches no route gets:
- 404 Not found — 404 with the body
{"error":"no_route"}; - This answer — a response you set, with everything a route's response has. Its
{{counter}}counts the requests no route took.
Either way the request counts as No rule.
What an HTTP reply can read
| Template | Is |
|---|---|
{{request.method}} | GET, POST, … |
{{request.path}} | The path, without the query. |
{{request.params.id}} | The path segment named :id. |
{{request.query.page}} | A query parameter, decoded. |
{{request.headers.x-key}} | A header; names in lower case. |
{{request.body}} | The body as text: its first 64 KiB. |
{{request.json.name}} | A field of a JSON body, when the body is JSON and within 64 KiB. |
{{request.from}} | The client's IP:port. |
A request body larger than 1 MiB gets 413 and is counted as Failed. A reply that cannot be made — a template naming something the request does not have — gets 500 with the error in its body, and is counted as Failed.
OSC device
Each message that arrives — each message of a bundle on its own — is answered by the first rule it matches. A datagram that is not OSC is counted as No rule.
| Field | What |
|---|---|
| Address pattern | An OSC 1.0 address pattern: * any characters, ? one, [a-z] a set, {a,b} either, each within one segment (see OSC). |
| Argument rules | Up to 16 conditions on the arguments, as in Wait for OSC (see Nodes). |
| Reply | Off: take the message and answer nothing. |
| Reply address | The reply's address, a template. |
| Reply arguments | Up to 16 arguments, each a Type (int, float, str, long, double, bool, blob, nil) and a Value template. |
| Reply to | Empty: back to the sender's address and port. Otherwise IP:port. |
| Delay, ms, Jitter, ms | 0–60 000 ms each. |
An argument's value is read as its type after the template is filled: {{request.args[0]}} echoes the first argument as a number when the type is a number. A bool takes true, 1, yes, on or false, 0, no, off; a blob takes hex bytes; an empty value is the type's zero.
Replies leave from the emulator's own port, so a client that listens on the port it sent from hears them.
An OSC reply can read {{request.address}}, {{request.args[0]}} and {{request.from}}.
UDP device
Each datagram is answered by the first rule it matches.
| Field | What |
|---|---|
| Match | Any datagram, Contains text, Matches regex or Contains bytes (hex). |
| Pattern | The text, regular expression or bytes to look for. |
| Reply | Take it without a reply, Text or Hex, then the reply itself as a template. |
| Reply to | Empty: back to the sender. Otherwise IP:port. |
| Delay, ms, Jitter, ms | 0–60 000 ms each. |
A UDP or TCP reply can read:
| Template | Is |
|---|---|
{{request.text}} | The payload as text. |
{{request.match}} | What matched: the text, a regular expression's first group (or the whole match), the bytes. |
{{request.hex}} | The payload as hex bytes, its first 1024. |
{{request.bytes}} | The payload's size. |
{{request.from}} | The sender's IP:port. |
A text reply is at most 65 507 bytes.
TCP device
A device that speaks lines on a TCP connection, as a projector or a matrix switcher does. Each message a client sends is answered by the first rule it matches; the reply goes back on the same connection.
| Field | What |
|---|---|
| Message end | What ends a message, and is added after each reply and the greeting: LF (\n) (a \r before it is dropped), CR LF (\r\n), CR (\r), or None — every chunk. Empty lines are skipped. |
| Greeting | Sent as a client connects; empty for none. It can read {{request.from}}. |
| Match, Pattern, Reply | As for a UDP device. |
| Then close the connection | Close the connection after this rule's reply — QUIT, say. |
| Delay, ms, Jitter, ms | 0–60 000 ms each. |
A message longer than 64 KiB without its delimiter is taken as it stands.
MQTT broker
A small MQTT 3.1.1 broker on plain TCP. It does what a broker does: clients connect, subscribe with + and #, publish at QoS 0, 1 and 2, retained messages and last wills work, and a second connection with a client's id takes over from the first. Sessions are always clean: a client asking to keep its session gets a fresh one, and nothing is queued for a client that is away.
On top of that, every message published to it is checked against the rules: the first that matches also publishes a reply — a device reporting what it did.
| Field | What |
|---|---|
| User name, Password | When a user name is set, a client must connect with it and the password; empty: anyone may connect. A password without a user name is refused, as MQTT 3.1.1 cannot carry one. |
| Retained | Up to 64 messages (Topic, Payload, QoS) held from the start, as if published with retain: a client that subscribes gets them first. |
| Topic filter | Which topics a rule takes: + one level, # the rest — lab/+/set. |
| Match, Pattern | A condition on the payload, as for a UDP device. |
| Reply | Off: take the message and publish nothing more. |
| Reply topic, Reply payload | Templates. The topic cannot hold + or #. |
| QoS, Retain | Of the reply. |
| Delay, ms, Jitter, ms | 0–60 000 ms each. |
An MQTT reply can read {{request.topic}}, {{request.levels[1]}} (the topic's levels, from 0), {{request.payload}}, {{request.json.state}}, {{request.match}}, {{request.qos}}, {{request.retain}}, {{request.client}} (the client id) and {{request.from}}.
Templates in replies
Replies are written in the same template language as experiments, so a field means the same thing here and there. A reply can read:
request— what arrived, as listed for each protocol above;{{counter}}— how many messages this rule has taken since the emulator started, this one included;- the generators —
{{uuid}},{{now.iso}}, random values and the rest; random ones are drawn from the emulator's seed; - parameters, when the emulator runs in an experiment or is started with
signallab emulate --param.
A reply never reads secrets, and an unknown name is an error, not an empty text.
Some fields are fixed when the emulator starts, before anything arrives: a path, a condition, an address pattern, a payload pattern, a topic filter, Reply to, a header's name, retained messages and the broker's login. They take text and parameters only, no request and no generators.
The seed drives the random order of responses, the jitter and the random generators. On the Emulators screen each start takes a new seed; an experiment uses the run's seed, and signallab emulate --seed takes one you give.
Going down
To test what your system does when a dependency flaps, tick Goes down now and then:
| Field | What | Default |
|---|---|---|
| Up for, ms | How long it answers, 10–3 600 000 ms. | 10 000 |
| Down for, ms | How long it is down, 10–3 600 000 ms. | 3000 |
| While down | HTTP only: what a request meets while it is down. | 503 Unavailable |
The schedule starts when the emulator starts and repeats: up, down, up, down… While it is down:
| Emulator | Meets |
|---|---|
| HTTP | 503 Unavailable: 503 with Retry-After set to the seconds until it is back (at least 1). Close the connection: the connection closes without an answer. No answer: held for up to 2 minutes, then closed. |
| TCP device | Open connections are dropped within 0.1 s; new ones are closed as they arrive. |
| MQTT broker | Every connection is dropped; new ones are refused (CONNACK return code 3, server unavailable). |
| OSC, UDP device | Nothing is answered. |
What arrives while it is down counts as While down, not as No rule, and its rules are not asked.
Take down does the same on demand, whatever the schedule says, until you press Bring up; HTTP then meets 503 without Retry-After. In an experiment, the Emulator down/up node does it at a step of the run (see Nodes and Faults).
Problems
While you edit, the emulator is checked a moment after each change, and a problem shows under its buttons before you press Start. A problem names where it is — the rule, the response or the retained message, and the field — and what is wrong: a path without its /, a regular expression that does not compile, a reply template naming something other than request, parameters and generators, a value out of range. Start refuses an emulator with a problem.
Limits
| What | Limit | At the limit |
|---|---|---|
| Routes or rules per emulator | 64 | Refused when checked. |
| Responses per route | 16 | Refused. |
| Conditions per route | 16 | Refused. |
| Headers per response | 32 | Refused. |
| Argument conditions, reply arguments (OSC) | 16 each | Refused. |
| Retained messages (MQTT) | 64 | Refused. |
| A body, reply or greeting as written | 256 KiB | Refused. |
| A delay or a jitter | 60 000 ms | Refused. |
| HTTP request body | 1 MiB | 413. |
| HTTP connections at once | 512 | More are closed as they arrive. |
| HTTP request head | 30 s | A client must send it within this. |
| TCP connections at once | 256 | More are closed as they arrive. |
| OSC and UDP replies waiting for their delay | 1024 | More are dropped and counted as Failed. |
| MQTT clients at once | 256 | More are closed as they arrive. |
| MQTT packet | 256 KiB | The client's connection ends. |
| MQTT subscriptions per client | 100 | More are refused. |
| MQTT retained topics | 1000 topics, 16 MiB | A new retained message is routed, not retained. |
| MQTT messages waiting for one slow client | 1024 messages, 8 MiB | It misses them; counted as Not delivered. |
Mock this
To make an emulator from a response that worked:
- On the HTTP screen, send a request and get a response — or use Send now on an HTTP node of an experiment.
- Press ⧉ Mock this beside the response. The Mock this response dialog shows the route it will make.
- In Add to, pick one of your HTTP emulators, or A new emulator.
- Press Add the route. The Emulators screen opens on that emulator.
The route answers the request's method and path (without the query) with the response's status, headers and body. Headers that belong to the one exchange (Content-Length, Date, Server, ETag and the like) are left out, and the body is sent as it was, even if it holds {{. A new emulator holds only this route. Added to an existing emulator, the route goes first, so it answers before a broader route; a running one picks it up when you press Restart.
From an experiment, a URL written with templates becomes a pattern: its base ({{api}}) is dropped, a segment that is one template (/orders/{{order_id}}) becomes :order_id, and a segment only partly templated ends the path with *.
The starter set
The first time Signal Lab finds no emulator library, it writes five, all on this computer. Their names and notes are written in the language of the interface at that moment.
| Emulator | Listens on | Does |
|---|---|---|
| Demo API | 127.0.0.1:8080 | GET /health → {"status":"ok","time":…}; GET /users/:id → a user with that id; POST /users → 201 with a Location; GET /slow → after 1500 ms; /flaky → 503, 503, then 200 from then on. |
| Demo OSC device | 127.0.0.1:9100 | /ping → /pong with the count; /fader/* → /ack with the address it got; /cue/* taken without a reply. |
| Demo UDP device | 127.0.0.1:7100 | PING → PONG and the count; anything else → ACK and its size in bytes. |
| Demo TCP device | 127.0.0.1:7200 | Lines ending in CR LF. Greets with READY; POWER? → POWER=ON; POWER ON or POWER OFF → OK ON / OK OFF; QUIT → BYE, then hangs up. |
| Demo MQTT broker | 127.0.0.1:1883 | Retains online on lab/status; ON or OFF published to lab/<name>/set → the same, retained, on lab/<name>/state. |
The starter Is the service up? signal of the signal library asks http://127.0.0.1:8080/, the Demo API's address: it has no route for /, so it gets 404.
The library file
The library is emulators.json in the data folder (see Files); hover the count under the list to see its path. It is written whole 0.7 s after the last change, through a temporary file, so a failed write leaves the previous one. If the file cannot be read, the list shows the error with the path, line and column, and the file is left as it is: fix it and press Reload the file. Press Reload the file too after editing it by hand. With no file, the starter set is written again.
{
"version": 1,
"emulators": [
{
"id": "orders-api",
"note": "Stands in for the orders service.",
"emulator": {
"name": "Orders API",
"bind": "127.0.0.1:18080",
"protocol": "http",
"routes": [
{ "method": "GET", "path": "/orders/:id",
"responses": [{ "body": "{\"id\":\"{{request.params.id}}\",\"state\":\"open\"}" }] },
{ "method": "POST", "path": "/orders", "order": "sequence",
"responses": [{ "status": 503 }, { "status": 201, "body": "{\"id\":\"{{uuid}}\"}" }] }
],
"outage": { "up_ms": 20000, "down_ms": 2000, "fault": "unavailable" }
}
}
]
}The emulator object alone is a document signallab emulate reads too.
In experiments and scripts
- In an experiment, an Emulator node opens its emulator before the first step and answers until the run ends; what it received is counted in the report. An HTTP emulator there is also what Wait for HTTP request (Nodes) listens to, and an OSC or UDP emulator shares its port with the run's waits. Two emulators of one transport in one experiment cannot share a port. See Nodes and Faults.
signallab emulateruns emulators from files or from this library until Ctrl+C or--for, printing what they answer; see The command line.
Related
- Inspector — every exchange, decoded.
- Impairment — a bad network between your system and an emulator.
- Data and templates