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 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 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[]
phasesOnly run the given phases. Valid phases are "generate", "shrink", "reuse", "target", "explicit", and "explain".
typetext[]
defaultall phases
report_multiple_failuresIf true, the server will report all the distinct bugs it finds during a test run. Otherwise, the server will report only the bug with the simplest failing test case.
typeboolean
defaulttrue
failure_blobIf set, the server runs exactly one test case, corresponding to the test case encoded in this blob. The standard phases are skipped. The blob here corresponds to the blob returned by test_done.results.failure_blobs.
typebytes
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.

When the client decides the test case is complete , it sends a mark_complete command on the test case stream with information describing the final status of the test case. The server replies with a mark_complete_reply packet.

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
originA unique identifier for the failure. Should only be set when status is "INTERESTING".

The server treats each unique origin as a separate failure, and will shrink and report each independently.

The granularity of origin is up to the client. For example, if the client decides to report failure_line_number as its origin, any failure on that line, including those with a different exception type, will be treated by the server as the same failure. (If the client did want these to be treated as different failures, it might report (failure_line_number, exception_type) as its origin).

Be careful not to include a string representation of the input in origin. If origin depends on the specific inputs given by the server, each shrink attempt will be treated as a unique failure, which is almost certainly not what you want.
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 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.

When the server decides the test run is finished, it sends a test_done packet 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.failure_blobsOne entry per interesting test case, in the same order as the failures are reported. Each entry encodes its failing test case as an opaque blob of bytes. A blob can be sent as run_test.failure_blob to replay that specific failure.
typebytes[]
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 the test_done packet, 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
one_ofGenerate a value drawn from one of generators. Returned as a 2-element list [index, value], where index is the 0-based index into generators and value is the value drawn from generators[index].
generatorsArray of generators. Must be non-empty.
typeschema[]
requiredyes
booleanGenerate either true or false.
pProbability of generating true. Must be in the range [0, 1].
typefloat
default0.5
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
fractionGenerate a rational number.

Returned as a 2-element array [numerator, denominator], where both elements are integers and denominator > 0.
min_valueMinimum value, inclusive.
typeinteger | float
defaultunbounded
max_valueMaximum value, inclusive.
typeinteger | float
defaultunbounded
max_denominatorMaximum denominator. Must be positive.
typeinteger
defaultunbounded
complexGenerate a complex number.

Returned as a 2-element array [real, imaginary], where both elements are floats.
min_magnitudeMinimum magnitude (abs(z)) of generated values.
typefloat
default0
max_magnitudeMaximum magnitude (abs(z)) of generated values.
typefloat
defaultunbounded
allow_nanWhether NaN can be generated. Applies to both the real and imaginary part.
typeboolean
defaulttrue if min_magnitude is 0 and max_magnitude is unset, false otherwise
allow_infinityWhether +Infinity and -Infinity can be generated. Applies to both the real and imaginary part.
typeboolean
defaulttrue if max_magnitude is unset, false otherwise
allow_subnormalWhether subnormal numbers can be generated. Applies to both the real and imaginary part.
typeboolean
defaulttrue
widthTotal width of the complex number in bits. One of 32, 64, or 128.

For example, width=128 indicates both the real and imaginary parts are 64-bit.
typeinteger
default128
stringGenerate a Unicode string. Returned as a custom CBOR tag 91, with a payload equivalent to the UTF-8 representation of the string except that surrogate code points are allowed.

To exclude surrogates, for example because the client's programming language does not allow UTF-8 strings with surrogate code points, set exclude_categories to ["Cs"].

Characters are generated as follows:
  • When no filtering rules are specified, any character can be generated.
  • If min_codepoint or max_codepoint is specified, then only characters having a code point in that range will be generated.
  • If categories is specified, then only characters from those Unicode categories will be generated. This is a further restriction, characters must also satisfy min_codepoint and max_codepoint.
  • If exclude_categories is specified, then any character from those categories will not be generated. It is invalid to pass both categories and exclude_categories; these arguments are alternative ways to specify the same thing.
  • If include_characters is specified, then any additional characters in that list will also be generated.
  • If exclude_characters is specified, then any characters in that list will not be generated. It is invalid to have any overlap between include_characters and exclude_characters.
If codec is specified, only characters encodable in that codec will be generated.
min_sizeMinimum length, in the number of code points.
typeinteger
default0
max_sizeMaximum length, in the number of code points.
typeinteger
defaultunbounded
min_codepointMinimum Unicode code point, inclusive.
typeinteger
defaultunbounded
max_codepointMaximum Unicode code point, inclusive.
typeinteger
defaultunbounded
categoriesRestrict generated characters to the given Unicode general categories. Each category can be either a one-letter major category (e.g. "L"), or a two-letter minor category (e.g. "Lu").
typetext[]
defaultno restriction
exclude_categoriesExclude characters in the given Unicode general categories.
typetext[]
defaultno restriction
include_charactersCharacters that should always be allowed, even if they would otherwise be excluded by the other filters.
typetext
defaultno restriction
exclude_charactersCharacters that should never be generated.
typetext
defaultno restriction
codecIf set, only characters encodable in the given codec will be generated. hegel-core accepts any name from Python's codecs module (e.g. "ascii", "utf-8", "latin-1").
typetext
defaultno restriction
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
alphabetThese fields have the same meaning as the corresponding fields in string. The only difference is the default: if alphabet is not passed, it defaults here to {"codec": "utf-8"}.
typeobject
defaultDefaults to {"codec": "utf-8"}.
min_codepointSame meaning as min_codepoint in string.
typeinteger
defaultunbounded
max_codepointSame meaning as max_codepoint in string.
typeinteger
defaultunbounded
categoriesSame meaning as categories in string.
typetext[]
defaultno restriction
exclude_categoriesSame meaning as exclude_categories in string.
typetext[]
defaultno restriction
include_charactersSame meaning as include_characters in string.
typetext
defaultno restriction
exclude_charactersSame meaning as exclude_characters in string.
typetext
defaultno restriction
codecSame meaning as codec in string.
typetext
defaultno restriction
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
ip_addressGenerate an IP address string.
versionThe IP version. Must be 4 or 6.
typeinteger
requiredyes
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".
uuidGenerate a UUID string. For example: "550e8400-e29b-41d4-a716-446655440000".
versionIf set, generates a UUID of the corresponding RFC 4122 version. Must be one of 1, 2, 3, 4, 5.
typeinteger
defaultany version

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.