RclexTesting.DSL.Topics (RclexTesting (Experimental) v0.12.11)

Copy Markdown View Source

Publish, observe, and assert ROS topic messages within a scenario.

Use watch/2 to subscribe to a topic and buffer incoming messages. publish/2 sends a message, while expect/2, refute_message/1, and related assertions inspect buffered messages with cursor-based semantics.

Options

Every function in this module accepts an optional trailing keyword list:

  • :namespace the namespace of the scenario node. Defaults to the namespace given to scenario/3, or "/".
  • :qos a Rclex.QoS profile. If not specified, Rclex.QoS.profile_default/0 is used.
  • :timeout how long to wait, in milliseconds. Defaults to 5000.

Any option other than :timeout is forwarded verbatim to the underlying Rclex call, so options not listed here are supported too.

Summary

Functions

Poll predicate every 50 ms until it returns truthy or timeout elapses.

Assert that a message matching predicate arrives on topic.

expect/2 with either an explicit message type or options.

expect/3 with both an explicit message type and options.

Assert that count messages arrive on topic.

Assert a sequence of matchers in order on topic, using cursor semantics.

Return all messages buffered for topic since the scenario began (or since watch/1,2 was called), in arrival order.

Publish msg on topic.

Assert that no message arrives on topic within timeout ms (default 200 ms).

refute_message/1 with a message type, a predicate, or options.

refute_message/2 with any two of a message type, a predicate, and options.

refute_message/3 with a message type, a predicate, and options.

Start buffering all messages arriving on topic.

Subscribe to topic and buffer all arriving messages.

watch/2 with both an explicit message type and options.

Functions

eventually(predicate, opts \\ [])

@spec eventually(
  (-> boolean()),
  keyword()
) :: :ok

Poll predicate every 50 ms until it returns truthy or timeout elapses.

eventually(fn -> length(messages("/sensor/data")) >= 5 end)
eventually(fn -> length(messages("/sensor/data")) >= 5 end, timeout: 10_000)

expect(topic, predicate)

@spec expect(String.t(), (struct() -> boolean())) :: struct()

Assert that a message matching predicate arrives on topic.

Polls the internal buffer until the predicate returns truthy or the default timeout (5000 ms) elapses.

expect "/robot/state", &(&1.state == :running)

expect(topic, msg_type, predicate)

@spec expect(String.t(), module(), (struct() -> boolean())) :: struct()
@spec expect(String.t(), (struct() -> boolean()), keyword()) :: struct()

expect/2 with either an explicit message type or options.

# explicit type — registers the type and starts watching in one call
expect "/robot/state", MyPkg.Msg.RobotState, &(&1.state == :running)

# options
expect "/robot/state", &(&1.state == :running), timeout: 10_000, qos: sensor_qos

opts

  • :timeout how long to wait, in milliseconds. Defaults to 5000.
  • :namespace the namespace of the scenario node. Defaults to the namespace given to scenario/3, or "/".
  • :qos a Rclex.QoS profile. If not specified, Rclex.QoS.profile_default/0 is used. Only applied when the subscription is started.

expect(topic, msg_type, predicate, opts)

@spec expect(String.t(), module(), (struct() -> boolean()), keyword()) :: struct()

expect/3 with both an explicit message type and options.

expect "/robot/state", MyPkg.Msg.RobotState, &(&1.state == :running), timeout: 10_000

expect_count(topic, count, opts \\ [])

@spec expect_count(String.t(), pos_integer(), keyword()) :: [struct()]

Assert that count messages arrive on topic.

Returns the list of matched messages (next count in arrival order from cursor).

messages = expect_count "/sensor/data", 3

opts

  • :timeout how long to wait, in milliseconds. Defaults to 5000.
  • :namespace the namespace of the scenario node. Defaults to the namespace given to scenario/3, or "/".
  • :qos a Rclex.QoS profile. If not specified, Rclex.QoS.profile_default/0 is used. Only applied when the subscription is started.

expect_sequence(topic, matchers, opts \\ [])

@spec expect_sequence(String.t(), [(struct() -> boolean())], keyword()) :: [struct()]

Assert a sequence of matchers in order on topic, using cursor semantics.

Each matcher in matchers is matched against the next unconsumed message. Returns the list of matched messages. opts apply to every step.

expect_sequence "/robot/state", [
  matches(%{mode: :idle}),
  matches(%{mode: :starting}),
  matches(%{mode: :running})
]

messages(topic)

@spec messages(String.t()) :: [struct()]

Return all messages buffered for topic since the scenario began (or since watch/1,2 was called), in arrival order.

publish(topic, msg, opts \\ [])

@spec publish(String.t(), struct(), keyword()) :: :ok

Publish msg on topic.

The message type is inferred from the struct module. A publisher is started lazily and reused across calls within the same scenario.

publish "/mission/start", %MissionStart{id: 42}
publish "/mission/start", %MissionStart{id: 42}, qos: Rclex.QoS.profile_sensor_data()

opts

  • :namespace the namespace of the scenario node. Defaults to the namespace given to scenario/3, or "/".
  • :qos a Rclex.QoS profile. If not specified, Rclex.QoS.profile_default/0 is used. Only applied when the publisher is started.

refute_message(topic)

@spec refute_message(String.t()) :: :ok

Assert that no message arrives on topic within timeout ms (default 200 ms).

The message type must have been registered by an earlier watch/2, publish/2, or expect/3 call.

refute_message "/robot/error"

refute_message(topic, msg_type)

@spec refute_message(String.t(), module() | (struct() -> boolean()) | keyword()) ::
  :ok

refute_message/1 with a message type, a predicate, or options.

# explicit type — registers the type and starts watching in one call
refute_message "/robot/error", MyPkg.Msg.Error

# predicate against the already-registered type
refute_message "/robot/error", &(&1.code == :fatal)

# options
refute_message "/robot/error", timeout: 500

opts

  • :timeout how long to wait, in milliseconds. Defaults to 200.
  • :namespace the namespace of the scenario node. Defaults to the namespace given to scenario/3, or "/".
  • :qos a Rclex.QoS profile. If not specified, Rclex.QoS.profile_default/0 is used. Only applied when the subscription is started.

refute_message(topic, msg_type, predicate)

@spec refute_message(
  String.t(),
  module() | (struct() -> boolean()),
  (struct() -> boolean()) | keyword()
) :: :ok

refute_message/2 with any two of a message type, a predicate, and options.

refute_message "/robot/error", MyPkg.Msg.Error, &(&1.code == :fatal)
refute_message "/robot/error", MyPkg.Msg.Error, timeout: 500
refute_message "/robot/error", &(&1.code == :fatal), timeout: 500

refute_message(topic, msg_type, predicate, opts)

@spec refute_message(String.t(), module(), (struct() -> boolean()), keyword()) :: :ok

refute_message/3 with a message type, a predicate, and options.

refute_message "/robot/error", MyPkg.Msg.Error, &(&1.code == :fatal), timeout: 500

watch(topic)

@spec watch(String.t()) :: :ok

Start buffering all messages arriving on topic.

The message type must have been registered via a prior watch/2 or publish/2 call. For the first subscription on a topic, prefer watch/2.

watch(topic, msg_type)

@spec watch(String.t(), module() | keyword()) :: :ok

Subscribe to topic and buffer all arriving messages.

Safe to call multiple times for the same topic; subsequent calls are no-ops. When msg_type is omitted, the type registered by an earlier watch/2 or publish/2 is reused.

watch "/robot/state", MyPkg.Msg.RobotState
watch "/robot/state", qos: Rclex.QoS.profile_sensor_data()

opts

  • :namespace the namespace of the scenario node. Defaults to the namespace given to scenario/3, or "/".
  • :qos a Rclex.QoS profile. If not specified, Rclex.QoS.profile_default/0 is used.

watch(topic, msg_type, opts)

@spec watch(String.t(), module(), keyword()) :: :ok

watch/2 with both an explicit message type and options.

watch "/robot/state", MyPkg.Msg.RobotState, namespace: "/robot1"