Skip to content

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 ​

  1. Press one of the buttons at the top of the library:

    ButtonMakesListens onWith one rule that works as it stands
    + HTTP APIAn HTTP API127.0.0.1:18080GET /health → 200 {"status":"ok"}
    + OSC deviceAn OSC device127.0.0.1:9100/ping → /pong with the count as an int
    + UDP deviceA UDP device127.0.0.1:7100a datagram containing PING → PONG 1, PONG 2, …
    + TCP deviceA TCP device127.0.0.1:7200a line containing PING → PONG
    + MQTT brokerAn MQTT broker127.0.0.1:1883a publish to lab/<name>/set → the same payload, retained, on lab/<name>/state

    When another emulator of the library already uses that port, the next free one is taken.

  2. Give it a Name (at most 120 characters).

  3. Set Listen on: IP:port. 127.0.0.1 answers this computer only; 0.0.0.0 answers the network as well.

  4. 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 ​

  1. 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.
  2. 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).
  3. Watch Received fill.
  4. 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:

CountWhat
RequestsEverything that arrived: requests, messages, lines.
No ruleWhat no rule took. An HTTP request without a route still gets its answer (see Requests no route takes); the others get none.
FailedExchanges where a reply could not be made or sent.
While downWhat arrived while the emulator was down. Shown when it has an outage set or something met it down. Never counted as No rule.
Not deliveredMQTT 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:

ColumnWhat
TimeWhen it arrived.
FromThe client's address.
RequestWhat arrived, in protocol notation: GET /users/7, /ping 1, POWER?.
RuleThe rule that took it (#2), or —.
ReplyWhat 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.
msFrom 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.

FieldWhat
MethodGET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, or Any. A GET route answers HEAD too.
PathStarts 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.
ConditionsEvery one must hold. Add one with + Condition.

Path examples:

PathTakesDoes 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:

WhereNameReads
HeaderA header name, any caseThe header's value; a header sent several times, its values joined with , .
QueryA query parameterIts value, decoded; the first, when it is repeated.
Body—The whole body as text.
JSONA JSON path, such as $.user.idThat 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).

FieldWhatDefault
Status100–599.200
FaultSomething other than an answer; see Faults.None — answer
Delay, msHow long to wait before answering, 0–60 000 ms.0
Jitter, msUp to this much longer, at random, 0–60 000 ms.0
WeightIts share when the route answers at random. Shown only then.1
HeadersUp to 32. Names may use parameters; values are templates.none
BodyA 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 responseRequests getFor
In sequence, then the lastThe first, the second, …, then the last one from then on: 500, 500, 200, 200, 200…Retries: fail twice, then work.
In turnThe first again after the last: 200, 500, 200, 500…A dependency that fails now and then, regularly.
At random, by weightEach 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:

PresetAdds
200 JSON200, {"ok":true}
201 Created201, {"id":"{{uuid}}"}, header Location: {{request.path}}/{{counter}}
404 Not found404, {"error":"not found"}
500 Server error500, {"error":"internal"}
503 Unavailable503, {"error":"unavailable"}, header Retry-After: 1
Slow — 2 s200, {"ok":true} after 2000 ms
No answerThe fault No answer
Connection closedThe fault Close the connection
Malformed JSON200, {"items":[{"id":1},{"id":2}]} with the fault Malformed body

Faults ​

FaultWhat the client meets
None — answerThe response.
No answerNothing. 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 connectionThe connection closes without an answer, after the delay.
Malformed bodyA 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 ​

TemplateIs
{{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.

FieldWhat
Address patternAn OSC 1.0 address pattern: * any characters, ? one, [a-z] a set, {a,b} either, each within one segment (see OSC).
Argument rulesUp to 16 conditions on the arguments, as in Wait for OSC (see Nodes).
ReplyOff: take the message and answer nothing.
Reply addressThe reply's address, a template.
Reply argumentsUp to 16 arguments, each a Type (int, float, str, long, double, bool, blob, nil) and a Value template.
Reply toEmpty: back to the sender's address and port. Otherwise IP:port.
Delay, ms, Jitter, ms0–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.

FieldWhat
MatchAny datagram, Contains text, Matches regex or Contains bytes (hex).
PatternThe text, regular expression or bytes to look for.
ReplyTake it without a reply, Text or Hex, then the reply itself as a template.
Reply toEmpty: back to the sender. Otherwise IP:port.
Delay, ms, Jitter, ms0–60 000 ms each.

A UDP or TCP reply can read:

TemplateIs
{{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.

FieldWhat
Message endWhat 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.
GreetingSent as a client connects; empty for none. It can read {{request.from}}.
Match, Pattern, ReplyAs for a UDP device.
Then close the connectionClose the connection after this rule's reply — QUIT, say.
Delay, ms, Jitter, ms0–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.

FieldWhat
User name, PasswordWhen 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.
RetainedUp to 64 messages (Topic, Payload, QoS) held from the start, as if published with retain: a client that subscribes gets them first.
Topic filterWhich topics a rule takes: + one level, # the rest — lab/+/set.
Match, PatternA condition on the payload, as for a UDP device.
ReplyOff: take the message and publish nothing more.
Reply topic, Reply payloadTemplates. The topic cannot hold + or #.
QoS, RetainOf the reply.
Delay, ms, Jitter, ms0–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:

FieldWhatDefault
Up for, msHow long it answers, 10–3 600 000 ms.10 000
Down for, msHow long it is down, 10–3 600 000 ms.3000
While downHTTP 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:

EmulatorMeets
HTTP503 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 deviceOpen connections are dropped within 0.1 s; new ones are closed as they arrive.
MQTT brokerEvery connection is dropped; new ones are refused (CONNACK return code 3, server unavailable).
OSC, UDP deviceNothing 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 ​

WhatLimitAt the limit
Routes or rules per emulator64Refused when checked.
Responses per route16Refused.
Conditions per route16Refused.
Headers per response32Refused.
Argument conditions, reply arguments (OSC)16 eachRefused.
Retained messages (MQTT)64Refused.
A body, reply or greeting as written256 KiBRefused.
A delay or a jitter60 000 msRefused.
HTTP request body1 MiB413.
HTTP connections at once512More are closed as they arrive.
HTTP request head30 sA client must send it within this.
TCP connections at once256More are closed as they arrive.
OSC and UDP replies waiting for their delay1024More are dropped and counted as Failed.
MQTT clients at once256More are closed as they arrive.
MQTT packet256 KiBThe client's connection ends.
MQTT subscriptions per client100More are refused.
MQTT retained topics1000 topics, 16 MiBA new retained message is routed, not retained.
MQTT messages waiting for one slow client1024 messages, 8 MiBIt misses them; counted as Not delivered.

Mock this ​

To make an emulator from a response that worked:

  1. On the HTTP screen, send a request and get a response — or use Send now on an HTTP node of an experiment.
  2. Press ⧉ Mock this beside the response. The Mock this response dialog shows the route it will make.
  3. In Add to, pick one of your HTTP emulators, or A new emulator.
  4. 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.

EmulatorListens onDoes
Demo API127.0.0.1:8080GET /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 device127.0.0.1:9100/ping → /pong with the count; /fader/* → /ack with the address it got; /cue/* taken without a reply.
Demo UDP device127.0.0.1:7100PING → PONG and the count; anything else → ACK and its size in bytes.
Demo TCP device127.0.0.1:7200Lines 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 broker127.0.0.1:1883Retains 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.

json
{
  "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 emulate runs emulators from files or from this library until Ctrl+C or --for, printing what they answer; see The command line.