Skip to content

Dataplane matchers

Asserting on packet results with raw proto accessors gets old fast:

ASSERT_EQ(response.possible_outcomes_size(), 1);
ASSERT_EQ(response.possible_outcomes(0).packets_size(), 1);
EXPECT_EQ(response.possible_outcomes(0).packets(0).dataplane_egress_port(), 1u);

The dataplane matchers let you say what you mean instead:

EXPECT_THAT(dataplane.InjectPacket({...}), IsOkAndHolds(ForwardsTo(1)));

They compose with gmock's standard vocabulary — AllOf, ElementsAre, Contains, and friends all work. Here's the full set at a glance:

Shorthands   ForwardsTo  Forwards  Drops
Outcomes     OutcomeIs  OutcomesAre  EachOutcome  AnyOutcome  Outcome
Packets      OnPort  HasPayload  OnPorts  Packets
Input        HasIngress
Extraction   PacketsByDataplanePort  PacketsByP4RuntimePort

The basics

Many tests only need the shorthands. They work on both InjectPacketResponse (from InjectPacket) and ProcessPacketResult (from ResultStream::Next):

#include "fourward_cc/dataplane_matchers.h"
#include "fourward_cc/dataplane_client.h"

using ::fourward::Drops;
using ::fourward::ForwardsTo;
using ::fourward::HasIngress;

// Inject and assert in one shot:
EXPECT_THAT(dataplane.InjectPacket({...}), IsOkAndHolds(ForwardsTo(1)));
EXPECT_THAT(dataplane.InjectPacket({...}), IsOkAndHolds(ForwardsTo("Ethernet0")));

// Multicast:
EXPECT_THAT(dataplane.InjectPacket({...}), IsOkAndHolds(ForwardsTo(1, 2)));

// Same for stream results:
EXPECT_THAT(stream.Next(),
            IsOkAndHolds(AllOf(ForwardsTo(1), HasIngress(0))));

// Or assign first when you need the value for follow-up work:
ASSERT_OK_AND_ASSIGN(InjectPacketResponse response,
                     dataplane.InjectPacket({...}));
EXPECT_THAT(response, Drops());

Ports can be dataplane numbers or P4Runtime IDs — pass them bare for convenience, or wrap them for explicitness:

ForwardsTo(1)                            // dataplane port
ForwardsTo(DataplanePort{1})             // same, explicit
ForwardsTo("Ethernet0")                  // P4Runtime port
ForwardsTo(P4RuntimePort{"Ethernet0"})   // same, explicit

Looking at individual packets

When you need more than "which port?", drop down to the packet-level matchers. These match on individual OutputPacket protos — use them inside OutcomeIs to assert on a single deterministic outcome:

using ::fourward::OutcomeIs;
using ::fourward::OnPort;
using ::fourward::HasPayload;

// Single packet on port 1:
EXPECT_THAT(response, OutcomeIs(OnPort(1)));

// Multicast — two output packets:
EXPECT_THAT(response, OutcomeIs(OnPort(1), OnPort(2)));

// Port and payload:
EXPECT_THAT(response, OutcomeIs(OnPort(1, expected_bytes)));

// With a payload matcher:
EXPECT_THAT(response, OutcomeIs(OnPort(1, HasPayload(EndsWith("hello")))));

HasPayload takes any Matcher<const std::string&>, so it plays nicely with Eq, StartsWith, ResultOf, and anything else gmock offers.

ForwardsTo(port) is just OutcomeIs(OnPort(port)), Forwards() is "forwarded to some port" (without caring which), and Drops() is OutcomeIs() (zero packets).

// Don't care which port — just that the packet wasn't dropped:
EXPECT_THAT(dataplane.InjectPacket({...}), IsOkAndHolds(Forwards()));

Grouping by port

OnPorts groups packets by egress port and applies per-group matchers — use it when a single EXPECT_THAT covers everything you need. When you need packets in variables for more involved follow-up, see Extracting packets by port below.

Use OnPorts directly inside OutcomeIs:

using ::fourward::OnPorts;

// 7 copies to port 5, 25 to port 42:
EXPECT_THAT(response, OutcomeIs(OnPorts({
    {DataplanePort{5}, SizeIs(7)},
    {DataplanePort{42}, SizeIs(25)},
})));

// Every output packet carries the same payload:
EXPECT_THAT(response, OutcomeIs(Packets(Each(HasPayload(expected)))));

// Match individual packets per port:
EXPECT_THAT(response, OutcomeIs(OnPorts({
    {DataplanePort{1}, UnorderedElementsAreArray({
        HasPayload(packet_a),
        HasPayload(packet_b),
    })},
    {DataplanePort{2}, ElementsAre(HasPayload(packet_c))},
})));

// Works with P4Runtime ports too:
EXPECT_THAT(response, OutcomeIs(OnPorts({
    {P4RuntimePort{"Ethernet0"}, SizeIs(1)},
    {P4RuntimePort{"Ethernet1"}, SizeIs(1)},
})));

Extracting packets by port

PacketsByDataplanePort and PacketsByP4RuntimePort are the variable-extraction counterpart to OnPorts — same grouping, but the result lands in a map you can index directly or match exhaustively with gmock's container matchers:

using ::fourward::PacketsByDataplanePort;

auto by_port = PacketsByDataplanePort(response);

// Index into individual ports:
EXPECT_THAT(by_port[1], SizeIs(2));
EXPECT_THAT(by_port[2], ElementsAre(HasPayload(expected_bytes)));

// Or match the whole map — exhaustive, no stray ports:
EXPECT_THAT(by_port, UnorderedElementsAreArray({
    Pair(1, UnorderedElementsAreArray({
        HasPayload(packet_a),
        HasPayload(packet_b),
    })),
    Pair(2, UnorderedElementsAreArray({
        HasPayload(packet_c),
    })),
}));
using ::fourward::PacketsByP4RuntimePort;

EXPECT_THAT(PacketsByP4RuntimePort(response), UnorderedElementsAreArray({
    Pair("Ethernet0", SizeIs(1)),
    Pair("Ethernet1", SizeIs(1)),
}));

Both functions fail the test if the response has more than one possible outcome. Indexing a port that received no packets returns an empty vector.

Handling multiple outcomes

P4 programs with action selectors can produce multiple possible outcomes — each representing one "possible world."

OutcomesAre pins the exact set of outcomes (order-independent). Each argument is a packet matcher for a single-packet outcome; use Outcome(...) to spell out a multi-packet outcome:

using ::fourward::OutcomesAre;
using ::fourward::Outcome;

// Action selector: forwards to port 1 or port 2.
EXPECT_THAT(response, OutcomesAre(OnPort(1), OnPort(2)));

// One outcome multicasts, the other drops:
EXPECT_THAT(response, OutcomesAre(
    Outcome(OnPort(1), OnPort(2)),
    Outcome()));

EachOutcome and AnyOutcome quantify when you don't need to pin the exact set:

using ::fourward::EachOutcome;
using ::fourward::AnyOutcome;

// No matter what the selector picks, the packet reaches port 1:
EXPECT_THAT(response, EachOutcome(OnPort(1)));

// At least one branch drops:
EXPECT_THAT(response, AnyOutcome(Packets(IsEmpty())));

Container-level matching with Packets(...)

When you need to match properties of the packet list rather than individual packets — size, port grouping, etc. — wrap the matcher in Packets(...):

using ::fourward::Packets;

// Exactly 7 output packets:
EXPECT_THAT(response, OutcomeIs(Packets(SizeIs(7))));

// At least one output packet on port 1:
EXPECT_THAT(response, OutcomeIs(Packets(Contains(OnPort(1)))));

Packets(...) works inside OutcomeIs, EachOutcome, and AnyOutcome.

Ingress port

HasIngress works on ProcessPacketResult (which carries the input packet) and supports both port types:

EXPECT_THAT(stream.Next(), IsOkAndHolds(HasIngress(0)));
EXPECT_THAT(stream.Next(), IsOkAndHolds(HasIngress("Ethernet0")));

Composing with packetlib

If your project uses packetlib, you can parse the raw output bytes into a structured Packet proto and assert on header fields — all within the same EXPECT_THAT. The bridge is gmock's ResultOf:

#include "packetlib/packetlib.h"
#include "gutil/proto_matchers.h"

using ::packetlib::ParsePacket;
using ::testing::ResultOf;

EXPECT_THAT(response, OutcomeIs(
    AllOf(OnPort(1),
          HasPayload(ResultOf(ParsePacket, Partially(EqualsProto(R"pb(
              headers { ethernet_header { ethertype: "0x0800" } }
              headers { ipv4_header { destination_ip: "10.0.0.1" } }
          )pb")))))));

If you find yourself writing HasPayload(ResultOf(ParsePacket, ...)) a lot, a one-liner in your project saves the boilerplate:

template <typename M>
auto HasParsedPayload(M m) {
  return HasPayload(ResultOf(packetlib::ParsePacket, std::move(m)));
}

// Then:
EXPECT_THAT(response, OutcomeIs(OnPort(1), HasParsedPayload(
    Partially(EqualsProto(R"pb(...)pb")))));

4ward doesn't depend on packetlib — HasPayload just hands its matcher whatever ResultOf returns, so any bytes → T parser works the same way.

Trace on failure

When an outcome-level matcher fails (ForwardsTo, Forwards, Drops, OutcomeIs, OutcomesAre, EachOutcome, AnyOutcome), the full simulator trace is automatically appended to the failure message — if the response carries one. This gives you immediate visibility into how the packet was processed without rerunning the test:

Expected: drop the packet
  Actual: (has 1 possible outcomes),
full trace:
events {
  table_lookup {
    table_name: "ingress.acl"
    hit: true
    ...
  }
}
...

No opt-in needed — the trace shows up whenever the response has one.

Bazel dependency

cc_test(
    name = "my_test",
    srcs = ["my_test.cc"],
    deps = [
        "@fourward//fourward_cc:dataplane_matchers",
        # ...
    ],
)