Skip to content

Protocol reference

This is an RFC-style reference for the Hegel protocol. It is intended to be detailed enough that you could implement a compatible Hegel server or client only by referencing this document.

  • Server: The server process, implementing the core functionality of a property-based testing library. Hegel-core is an example of a server.
  • Client: The client library, implementing the user interface of a property-based testing library. Hegel-rust is an example of a client.
  • Packet: The unit of data sent in the protocol. A packet is always associated with a stream. A packet is comprised of a 31-bit integer ID, and 1 bit indicating whether it is a reply.
  • Request packet: A packet with the reply bit set to 0. All packets are either request packets or reply packets. Note that not every request packet expects a reply packet in response.
  • Reply packet: A packet with the reply bit set to 1. Each reply packet is associated with a specific request packet. All packets are either request packets or reply packets.
  • Stream: A unique identifier used to associate logically-related packets together. All packets are associated with a stream.
  • Control stream: The stream used for high-level packets, for example because no other stream has been established yet. The control stream has id 0.
  • Test case: A single execution of the test function with the concrete set of values that were generated for it during its execution.
  • Test run: The full lifecycle of a test function, including executing multiple test cases and shrinking any failures.
  • Connection: A generic connection allowing packets to be sent between a client and a server. The protocol is agnostic to the details of this connection and does not specify what transport layer it uses or how it is established. Connections are intended to be created once per process and shared between test runs, though this is not a requirement. A single test run uses the same connection for all its test cases. Multiple test runs are permitted in parallel on a single connection.

A packet consists of a 20-byte header, a variable-length payload, and a 1-byte terminator.

0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Magic (0x4845474C) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum (CRS32) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Stream id |S|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|R| Message id |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payload length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payload (variable length) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Term. (0x0A) |
+-+-+-+-+-+-+-+-+

The first five fields comprise the header. Each field is an unsigned 32-bit big-endian integer:

  • Magic: The constant 0x4845474C. This is ASCII for "HEGL".
  • Checksum: Defined as crS32(H), where H is the packet with the terminator removed and the checksum field set to 0.
  • Stream ID: The stream this packet is associated with. The S (source) bit is 1 for streams created by the client, and 0 for streams created by the server1.
  • Message ID: The id of the message associated with this packet. The R (reply) bit is set iff this packet is a reply to a request packet. The message id of a reply packet is the same as the message id of the corresponding request packet, but with the R bit set2.
  • Payload length: The length of the payload, in bytes.

The header is followed by the variable-length payload field, and then a single terminator byte (0x0A).

The encoding of the payload varies, and is documented for individual packets. We encode the payload of many packets in the protocol with CBOR.

The protocol is agnostic of the transport layer used to send and receive packets3. We will assume here that a connection has been established between the server and the client and do not specify how that connection is established.

After the connection has been established, the client is expected to initiate a handshake over that connection. A handshake must be initiated before any other communication. A handshake must not be initiated more than once per connection.

Client                            Server
    |                                  |
 1  |---[control]-- handshake -------->|
 1R |<--[control]-- handshake_reply ---|
    |                                  |
handshake
reply bit0
encodingASCII
payloadhegel_handshake_start
handshake_reply
reply bit1
encodingASCII
payloadHegel/{version}, where version is the protocol version.

After completing the handshake, the client may at any time choose to start a test run.

Client                           Server
    |                                 |
 1  |---[control]-- run_test -------->|
 1R |<--[control]-- run_test_reply ---|
    |                                 |
run_test
reply bit0
encodingCBOR
fieldscommand"run_test"
typetext
requiredyes
stream_idA new stream id chosen by the client that will be used for this test run. Note that because the client is creating this stream, stream_id must have its |S| bit set to 1, i.e. must be odd.
typeinteger
requiredyes
test_casesThe maximum number of test cases to execute.
typeinteger
requiredyes
seedRandom seed.
typeinteger
derandomizeIf true, and seed is not set, derive a deterministic seed from database_key.
typeboolean
defaultfalse
database_keyStable database key for this test.
typebytes
databasePath to the test case database directory, or null to disable the database.
typetext | null
suppress_health_checkArray of health check names to suppress. Valid names are "test_cases_too_large", "filter_too_much", "too_slow", and "large_initial_test_case".
typetext[]
default[]
run_test_reply
reply bit1
encodingCBOR
payloadtrue

Once a test run has been started, the server initiates the execution of test cases. The client uses the stream established in the run_test packet for packets related to this test run.

Client                                       Server
    |                                             |
 1  |<--[S1]-- test_case -------------------------|
 1R |---[S1]-- test_case_reply ------------------>|
    |                                             |
 2  |---[S2]-- command -------------------------->|
 2R |<--[S2]-- (command reply, if appropriate) ---|
    |---[S2]-- ... ------------------------------>|
    |                                             |
 3  |---[S2]-- mark_complete -------------------->|
 3R |<--[S2]-- mark_complete_reply ---------------|
    |                                             |
 4  |---[S2]-- stream_close --------------------->|
    |                                             |
    :       ... repeat for each test case         :
    |                                             |

In the test_case packet, it establishes a new stream (S2) which is used for packets for this test case.

test_case
reply bit0
encodingCBOR
fieldsevent"test_case"
typetext
requiredyes
stream_idThe stream id for this test case.
typeinteger
requiredyes
is_finalfalse during normal execution, true during final replays.
typeboolean
requiredyes
test_case_reply
reply bit1
encodingCBOR
payloadnull

During the test case, the client may send test case commands to the server. These command packets form the main interaction between the client and the server during the test case and are used to request generated values, mark spans of choices, etc.

After some number of test cases, the server will consider the test run to be over. This might be because we hit the test_cases setting (communicated in the initial run_test packet), or because a test case found an error in the test function. Whenever the server decides the test run is finished, it sends a mark_complete command on the test case stream with information describing the status of the test case.

mark_complete
reply bit0
encodingCBOR
fieldscommand"mark_complete"
typetext
requiredyes
status"VALID" (test case passed), "INVALID" (test case rejected, for example by via assume(false)), or "INTERESTING" (test case failed).
typetext
requiredyes
originDescription of the failure location. Should be set when status is "INTERESTING".
typetext
mark_complete_reply
reply bit1
encodingCBOR
fieldsresultnull
typenull
requiredyes

All commands are request packets sent by the client on the test case stream. The server replies with the corresponding reply packet.

Instead of the documented reply packets below, the server might also return command_error_reply to any command.

command_error_reply
reply bit1
encodingCBOR
fieldserrorThe error message.
typetext
requiredyes
typeThe error type (for example, "ValueError").
typetext
requiredyes

Generate a value from a schema.

generate
reply bit0
encodingCBOR
fieldscommand"generate"
typetext
requiredyes
schemaA generator schema.
typemap
requiredyes
generate_reply
reply bit1
encodingCBOR
fieldsresultThe generated value.
typeany
requiredyes

Mark the start of a span of choices.

start_span
reply bit0
encodingCBOR
fieldscommand"start_span"
typetext
requiredyes
labelIdentifies the span type.
typeinteger
default0
start_span_reply
reply bit1
encodingCBOR
fieldsresultnull
typenull
requiredyes

Mark the end of a span of choices.

stop_span
reply bit0
encodingCBOR
fieldscommand"stop_span"
typetext
requiredyes
discardIf true, this span is excluded from shrinking.
typeboolean
defaultfalse
stop_span_reply
reply bit1
encodingCBOR
fieldsresultnull
typenull
requiredyes

Record an observation for targeted property-based testing.

target
reply bit0
encodingCBOR
fieldscommand"target"
typetext
requiredyes
valueThe observation value.
typefloat
requiredyes
labelIdentifies the observation.
typeinteger
requiredyes
target_reply
reply bit1
encodingCBOR
fieldsresultnull
typenull
requiredyes

Create a new collection. Collections are used for the sizing of variable-length collections.

new_collection
reply bit0
encodingCBOR
fieldscommand"new_collection"
typetext
requiredyes
min_sizeMinimum number of elements.
typeinteger
default0
max_sizeMaximum number of elements.
typeinteger | null
defaultunbounded
new_collection_reply
reply bit1
encodingCBOR
fieldsresultThe collection id as an integer.
typetext
requiredyes

Ask whether the collection should produce another element.

collection_more
reply bit0
encodingCBOR
fieldscommand"collection_more"
typetext
requiredyes
collection_idThe collection id.
typetext
requiredyes
collection_more_reply
reply bit1
encodingCBOR
fieldsresulttrue if another element should be produced, false if the collection is done.
typeboolean
requiredyes

Indicate that the most recently produced element was not added to the collection.

collection_reject
reply bit0
encodingCBOR
fieldscommand"collection_reject"
typetext
requiredyes
collection_idThe collection id.
typetext
requiredyes
collection_reject_reply
reply bit1
encodingCBOR
fieldsresultnull
typenull
requiredyes

Create a new variable pool, which is a collection of identifiers intended to point to values that were generated or otherwise produced during testing. This is primarily useful for stateful testing, but can be used in normal tests too.

new_pool
reply bit0
encodingCBOR
fieldscommand"new_pool"
typetext
requiredyes
new_pool_reply
reply bit1
encodingCBOR
fieldsresultThe pool id (0-indexed).
typeinteger
requiredyes

Add a new variable to a pool.

pool_add
reply bit0
encodingCBOR
fieldscommand"pool_add"
typetext
requiredyes
pool_idThe pool to add to.
typeinteger
requiredyes
pool_add_reply
reply bit1
encodingCBOR
fieldsresultThe variable id.
typeinteger
requiredyes

Draw a variable from a pool. The server selects which variable to return.

pool_generate
reply bit0
encodingCBOR
fieldscommand"pool_generate"
typetext
requiredyes
pool_idThe pool to draw from.
typeinteger
requiredyes
consumeIf true, the variable is removed from the pool after being returned.
typeboolean
defaultfalse
pool_generate_reply
reply bit1
encodingCBOR
fieldsresultThe variable id. If the pool is empty, the server marks the test case as invalid.
typeinteger
requiredyes

After all test cases have been executed and shrinking is complete, the server sends a test_done event on the test stream.

Client                        Server
    |                              |
 1  |<--[S1]-- test_done ----------|
 1R |---[S1]-- test_done_reply --->|
    |                              |
test_done
reply bit0
encodingCBOR
fieldsevent"test_done"
typetext
requiredyes
results.passedWhether the test passed.
typeboolean
requiredyes
results.test_casesTotal number of test cases executed.
typeinteger
requiredyes
results.valid_test_casesNumber of valid test cases.
typeinteger
requiredyes
results.invalid_test_casesNumber of invalid test cases.
typeinteger
requiredyes
results.interesting_test_casesNumber of interesting (failing) test cases.
typeinteger
requiredyes
results.seedThe random seed that was used, as a string.
typetext
requiredyes
results.flakyPresent if the test was detected to be flaky.
typetext
results.health_check_failurePresent if a health check failed.
typetext
results.errorPresent if there was an error. This could be a usage error (e.g. invalid arguments) or indicate a server bug (e.g. unexpected exception raised).
typetext
test_done_reply
reply bit1
encodingCBOR
payloadtrue

After test_done, the server sends a test_case packet with is_final set to true for each interesting test case. The intent is for the client to use this to replay the minimal failing test case. Each final replay follows the standard test case flow.

For clarity at runtime, stream can be marked as closed when they are no longer needed.

stream_close
reply bit0
encodingraw
payloadThe single byte 0xFE. The message id of this packet is always (1 << 31) - 1.

Fields in bold are required.

Some fields list their type as schema. This means it accepts any generator schema. For example, {"type": "integer"} is a valid schema, so {"type": "one_of", "generators": [{"type": "integer"}]} is as well.

constantGenerate value.
valueThe value to return.
typeany
requiredyes
sampled_fromGenerate one of values.
valuesArray of concrete values to sample from. Must be non-empty
typeany[]
requiredyes
one_ofGenerate a value drawn from one of generators.
generatorsArray of generators. Must be non-empty.
typeschema[]
requiredyes
nullGenerate null.
booleanGenerate either true or false.
integerGenerate an integer.
min_valueMinimum value, inclusive.
typeinteger
defaultunbounded
max_valueMaximum value, inclusive.
typeinteger
defaultunbounded
floatGenerate a float.
min_valueMinimum value.
typefloat
defaultunbounded
max_valueMaximum value.
typefloat
defaultunbounded
allow_nanWhether NaN can be generated.
typeboolean
defaulttrue if neither min_value nor max_value is set, false otherwise
allow_infinityWhether +Infinity and -Infinity can be generated.
typeboolean
defaulttrue if either min_value or max_value is unset, false otherwise
widthFloat width. Either 32 or 64.
typeinteger
default64
exclude_minExclude the minimum value.
typeboolean
defaultfalse
exclude_maxExclude the maximum value.
typeboolean
defaultfalse
stringGenerate a Unicode string. Returned as a custom CBOR tag 6, with a payload equivalent to the UTF-8 representation of the string except that surrogate code points are allowed.
min_sizeMinimum length, in the number of code points.
typeinteger
default0
max_sizeMaximum length, in the number of code points.
typeinteger
defaultunbounded
binaryGenerate a byte string.
min_sizeMinimum length, in bytes.
typeinteger
default0
max_sizeMaximum length, in bytes.
typeinteger
defaultunbounded
regexGenerate a string that matches the given pattern regular expression.
patternThe regular expression to match.
typestring
requiredyes
fullmatchIf true, pattern must match the entire string. If false, pattern may match a substring.
typeboolean
defaultfalse
listGenerate a list of values from the elements generator.
elementsThe elements generator.
typeschema
requiredyes
min_sizeMinimum number of elements.
typeinteger
default0
max_sizeMaximum number of elements.
typeinteger
defaultunbounded
uniqueIf true, all generated list elements will be distinct.
typeboolean
defaultfalse
dictGenerate a map with keys drawn from keys and values drawn from values. Returns in the format [[key1, value1], ...].
keysThe keys generator.
typeschema
requiredyes
valuesThe values generator.
typeschema
requiredyes
min_sizeMinimum number of map entries.
typeinteger
default0
max_sizeMaximum number of map entries.
typeinteger
defaultunbounded
tupleGenerate a fixed-length array, where each element is drawn from the corresponding generator at that position.
elementsA list of generators of the same length as the desired tuple.
typeschema[]
requiredyes
emailGenerate an email address string, according to RFC 5322 Section 3.4.1.
urlGenerate an http/https URL string, according to RFC 3986.
domainGenerate a fully qualified domain name string, according to RFC 1035.
max_lengthMaximum length of the domain name.
typeinteger
default255
ipv4Generate an IPv4 address string.
ipv6Generate an IPv6 address string.
dateGenerate an ISO 8601 date string. For example: "2024-03-15".
timeGenerate an ISO 8601 time string. For example: "14:30:00".
datetimeGenerate an ISO 8601 datetime string. For example: "2024-03-15T14:30:00".

We use the following conventions for the sequence diagrams in this document:

Client Server
| |
1 |---[S1]-- a ---------------------------------->|
1R |<--[S1]-- a_reply -----------------------------|
| |
2 |<--[S2]-- b -----------------------------------|
2R |---[S2]-- b_reply ---------------------------->|
| |

Each named arrow --[C]-- name --> represents a packet, associated with stream C, being sent from either the client to the server (-->) or the server to the client (<--). name is purely illustrative and is not part of the spec.

Each packet is associated with an auto-incrementing number on the left. n indicates a request packet, and nR indicates a reply packet responding to packet n. Not all request packets expect a reply packet. These packet numbers are illustrative and not part of the spec, except for where they associate a specific request packet with a specific reply packet.

  1. The benefit of the S bit is to allow both the client and server to create streams without coordinating with each other.

  2. The benefit of the message id is to support out-of-order replies that are associated with the same stream.

  3. For example, hegel-core uses process stdin and stdout as its transport layer.