Cucumber.Messages (Cucumber v1.0.0)

View Source

Builders and NDJSON encoding for Cucumber Messages, the protocol every Cucumber implementation emits so formatters, report services, and the Cucumber Compatibility Kit can consume runs uniformly.

A message is an envelope: a single-key map whose key names the message type (mirroring the schema's oneof) and whose value is the payload, with camelCase keys matching the JSON schema. One envelope per line, JSON-encoded, makes the NDJSON stream.

Two groups of builders live here:

These builders are pure — id allocation, ordering, and NDJSON output are owned by Cucumber.RunCoordinator (the run-wide message sink) and driven by Cucumber.Messages.Emitter. Enable the stream with

config :cucumber, messages: "cucumber-messages.ndjson"

The file is written when the test suite finishes, via ExUnit.after_suite/1. Those callbacks fire on every ExUnit.run/1, so a project that invokes a nested ExUnit.run/1 mid-suite will flush (and close) the stream at that point rather than at the end of the real run.

Example

Ids must be unique across the whole stream, so when emitting several features, thread next_id from one compilation into the next (as Cucumber.Compiler.compile_features!/1 does):

{lines, _next_id} =
  Enum.flat_map_reduce(features, 0, fn feature, start_id ->
    compilation = Gherkin.Pickles.compile(feature, start_id)

    envelopes = [
      Cucumber.Messages.source(feature.file, feature.source),
      Cucumber.Messages.gherkin_document(
        feature.file,
        compilation.document,
        compilation.comments
      )
      | Enum.map(compilation.pickles, &Cucumber.Messages.pickle/1)
    ]

    {Enum.map(envelopes, &Cucumber.Messages.encode!/1), compilation.next_id}
  end)

Known deltas from the reference stream

testCase.testSteps entries do not carry stepMatchArgumentsLists (expression matching returns converted values, not source offsets), and parameterType/hook envelopes have no source line (their macros don't record one). The CCK approval harness (test/cucumber/cck_approval_test.exs) normalizes these; its samples table documents the two samples with further per-sample divergences.

Summary

Types

A single-key envelope map, e.g. %{pickle: %{...}}.

A %{seconds: integer, nanos: integer} map (timestamp or duration).

A step/hook result status accepted by test_step_result/3.

Functions

Builds an attachment envelope from a Cucumber.Attachment.

Converts a monotonic nanosecond difference into a duration.

Encodes an envelope as one NDJSON line (no trailing newline).

Builds a gherkinDocument envelope from a compiled feature node.

Builds the meta envelope describing this implementation and its host.

Builds a parameterType envelope from a custom parameter type definition.

Builds a pickle envelope from a Gherkin.Pickle.

Builds a source envelope carrying a feature file's raw text.

Builds a stepDefinition envelope from a step-registry entry.

Builds a testCase envelope binding a pickle to its executable test steps.

Builds a testCaseStarted envelope.

Builds a testRunStarted envelope.

Converts a System.system_time(:nanosecond) value into a timestamp.

Types

envelope()

@type envelope() :: %{required(atom()) => map()}

A single-key envelope map, e.g. %{pickle: %{...}}.

instant()

@type instant() :: %{seconds: integer(), nanos: integer()}

A %{seconds: integer, nanos: integer} map (timestamp or duration).

status()

@type status() ::
  :passed | :failed | :pending | :skipped | :undefined | :ambiguous | :unknown

A step/hook result status accepted by test_step_result/3.

Functions

attachment(attachment, ref)

@spec attachment(Cucumber.Attachment.t(), map() | nil) :: envelope()

Builds an attachment envelope from a Cucumber.Attachment.

ref carries :test_case_started_id/:test_step_id when the attachment was recorded during a step (both optional in the schema).

duration(nanoseconds)

@spec duration(integer()) :: instant()

Converts a monotonic nanosecond difference into a duration.

Negative differences are clamped at zero.

encode!(envelope)

@spec encode!(envelope()) :: String.t()

Encodes an envelope as one NDJSON line (no trailing newline).

gherkin_document(uri, feature_node, comments \\ [])

@spec gherkin_document(String.t(), map(), [map()]) :: envelope()

Builds a gherkinDocument envelope from a compiled feature node.

feature_node and comments are the document and comments produced by Gherkin.Pickles.compile/2.

hook(id, type, tag, name, source_reference)

@spec hook(
  String.t(),
  Cucumber.Hooks.hook_type(),
  String.t() | nil,
  String.t() | nil,
  map()
) ::
  envelope()

Builds a hook envelope.

type is a Cucumber.Hooks hook kind; tag and name may be nil.

meta()

@spec meta() :: envelope()

Builds the meta envelope describing this implementation and its host.

parameter_type(id, map)

@spec parameter_type(String.t(), %{name: String.t(), regexp: Regex.t()}) :: envelope()

Builds a parameterType envelope from a custom parameter type definition.

See Cucumber.ParameterTypes for how custom types are defined.

pickle(pickle)

@spec pickle(Gherkin.Pickle.t()) :: envelope()

Builds a pickle envelope from a Gherkin.Pickle.

source(uri, data)

@spec source(String.t(), String.t()) :: envelope()

Builds a source envelope carrying a feature file's raw text.

The media type follows the file extension: Markdown feature files (.feature.md) get the MDG media type, everything else plain Gherkin.

step_definition(id, arg, source_reference)

Builds a stepDefinition envelope from a step-registry entry.

key is a Cucumber.Discovery registry key; source_reference is a %{uri: ..., location: %{line: ...}} map.

test_case(id, pickle_id, test_steps, test_run_started_id)

@spec test_case(String.t(), String.t(), [map()], String.t() | nil) :: envelope()

Builds a testCase envelope binding a pickle to its executable test steps.

Each entry in test_steps is either a pickle step (%{id: ..., pickleStepId: ..., stepDefinitionIds: [...]}) or a scenario-hook step (%{id: ..., hookId: ...}), in execution order.

test_case_finished(test_case_started_id, timestamp, will_be_retried)

@spec test_case_finished(String.t(), instant(), boolean()) :: envelope()

Builds a testCaseFinished envelope.

will_be_retried marks a failed attempt that a retry follows.

test_case_started(id, test_case_id, attempt, timestamp)

@spec test_case_started(String.t(), String.t(), non_neg_integer(), instant()) ::
  envelope()

Builds a testCaseStarted envelope.

attempt is 0-based; retries increment it.

test_run_finished(success, timestamp, test_run_started_id)

@spec test_run_finished(boolean(), instant(), String.t() | nil) :: envelope()

Builds a testRunFinished envelope.

success is false when any scenario failed.

test_run_hook_finished(test_run_hook_started_id, result, timestamp)

@spec test_run_hook_finished(String.t(), map(), instant()) :: envelope()

Builds a testRunHookFinished envelope.

test_run_hook_started(id, test_run_started_id, hook_id, timestamp)

@spec test_run_hook_started(String.t(), String.t() | nil, String.t() | nil, instant()) ::
  envelope()

Builds a testRunHookStarted envelope.

Marks a before_all/after_all hook beginning execution.

test_run_started(id, timestamp)

@spec test_run_started(String.t(), instant()) :: envelope()

Builds a testRunStarted envelope.

test_step_finished(test_case_started_id, test_step_id, result, timestamp)

@spec test_step_finished(String.t(), String.t(), map(), instant()) :: envelope()

Builds a testStepFinished envelope.

Build result with test_step_result/3.

test_step_result(status, duration_ns, message \\ nil)

@spec test_step_result(status(), non_neg_integer(), String.t() | nil) :: map()

Builds a TestStepResult payload.

Used by test_step_finished/4 and test_run_hook_finished/3.

test_step_started(test_case_started_id, test_step_id, timestamp)

@spec test_step_started(String.t(), String.t(), instant()) :: envelope()

Builds a testStepStarted envelope.

timestamp(nanoseconds)

@spec timestamp(integer()) :: instant()

Converts a System.system_time(:nanosecond) value into a timestamp.