Skip to content

Commit 1199952

Browse files
committed
Add end-to-end BMv2 tests for the five new read API families
The unit tests for the read APIs landed by de3e031 (counter), e29bd3e (meter), dd152cf (register), and 6e43a4b (action-profile member and group) exercise each surface against an in-process gRPC fake. This commit closes the production-readiness gap by running the same APIs against a real simple_switch_grpc instance loaded with a new P4 fixture that declares all five entity families in a single pipeline, so one BMv2 spawn amortises across every read. The fixture counters_meters_registers_groups.p4 declares a 64-cell indirect counter (packets_and_bytes), a 32-cell indirect meter (bytes), a 16-cell register array of bit<32>, an action_selector with crc16 hash and 128 entries, and an lb_table whose implementation references that selector — the last is the prerequisite P4Info needs to attach the implicit action profile to a table id, without which selector-group reads are not valid. The pipeline is minimal: every packet bumps cell zero of the counter, executes the meter at cell zero, writes the register at cell zero, then applies lb_table. The integration test class CountersMetersRegistersGroupsIntegrationTest follows the lifecycle DigestIntegrationTest established — a single @BeforeAll spawn through BMv2TestSupport.checkEnvironment, an @afterall teardown, native simple_switch_grpc or testcontainers BMv2 depending on JP4_BMV2_MODE. Five tests cover the five read API families: counter and meter cells read against the freshly-loaded zero-state device (BMv2 reports every cell even when the data plane has never touched it), register cells similarly, and action-profile member and group reads after seeding device state through a parallel P4Runtime stub Write. jp4 1.4 has no public write API for action profiles, so the stub is the conventional integration-test workaround; it is contained to seeding and never used for read-side assertions. Two BMv2-shape findings surfaced during local verification and shape the test code as committed. First, simple_switch_grpc 1.15.1 returns UNIMPLEMENTED for the RegisterEntry read RPC — its error string says "Register reads are not supported yet". The integration test catches that status code and skips through Assumptions.assumeTrue, so it becomes a no-op until BMv2 adds support; the unit test in P4SwitchReadRegisterTest already covers the jp4 read path end to end against the gRPC fake. Second, P4Runtime 1.3+ canonical-bytestring encoding strips leading zero bytes, so a 9-bit port value of 5 sent to BMv2 as {0x00, 0x05} round-trips as a single-byte {0x05} on the read side. The action-profile member and group tests send the canonical form to match what BMv2 stores; the unit-test fake does not canonicalise, which is why the gRPC-fake suite did not catch this before. Test count moves from 379 to 384 with one skip on native BMv2 and zero skips when the register-read RPC is eventually supported. No production source changes: this is test-only and the public read APIs themselves are unchanged. The commit is SemVer-irrelevant.
1 parent 6e43a4b commit 1199952

4 files changed

Lines changed: 1039 additions & 0 deletions

File tree

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package io.github.zhh2001.jp4.integration;
2+
3+
import com.google.protobuf.ByteString;
4+
import io.github.zhh2001.jp4.P4Switch;
5+
import io.github.zhh2001.jp4.entity.ActionProfileGroup;
6+
import io.github.zhh2001.jp4.entity.ActionProfileMember;
7+
import io.github.zhh2001.jp4.entity.CounterEntry;
8+
import io.github.zhh2001.jp4.entity.MeterEntry;
9+
import io.github.zhh2001.jp4.entity.RegisterEntry;
10+
import io.github.zhh2001.jp4.entity.WeightedMember;
11+
import io.github.zhh2001.jp4.pipeline.DeviceConfig;
12+
import io.github.zhh2001.jp4.pipeline.P4Info;
13+
import io.github.zhh2001.jp4.testsupport.BMv2TestSupport;
14+
import io.github.zhh2001.jp4.error.P4OperationException;
15+
import io.grpc.ManagedChannel;
16+
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
17+
import org.junit.jupiter.api.AfterAll;
18+
import org.junit.jupiter.api.Assumptions;
19+
import org.junit.jupiter.api.BeforeAll;
20+
import org.junit.jupiter.api.Test;
21+
import p4.v1.P4DataOuterClass.P4Data;
22+
import p4.v1.P4RuntimeGrpc;
23+
import p4.v1.P4RuntimeOuterClass.Action;
24+
import p4.v1.P4RuntimeOuterClass.ActionProfileGroup.Member;
25+
import p4.v1.P4RuntimeOuterClass.Entity;
26+
import p4.v1.P4RuntimeOuterClass.Uint128;
27+
import p4.v1.P4RuntimeOuterClass.Update;
28+
import p4.v1.P4RuntimeOuterClass.WriteRequest;
29+
30+
import java.nio.file.Path;
31+
import java.util.List;
32+
33+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
34+
import static org.junit.jupiter.api.Assertions.assertEquals;
35+
import static org.junit.jupiter.api.Assertions.assertNotNull;
36+
37+
/**
38+
* End-to-end integration coverage for the five read API families
39+
* landed by {@code de3e031} (counter), {@code e29bd3e} (meter),
40+
* {@code dd152cf} (register), and {@code 6e43a4b} (action-profile
41+
* member + group). The unit-test classes for each family use an
42+
* in-process gRPC fake; this class exercises the same APIs against a
43+
* real {@code simple_switch_grpc} (BMv2) instance running the
44+
* {@code counters_meters_registers_groups.p4} pipeline.
45+
*
46+
* <p>Counter / meter / register reads run against the freshly-loaded
47+
* zero-state device — BMv2 reports every cell in the indirect array,
48+
* including the cells the data plane has never touched, with default
49+
* values. Action-profile member / group reads need at least one entry
50+
* present to exercise the response parser; the two relevant tests
51+
* seed device state through a raw P4Runtime stub Write before reading
52+
* through jp4. jp4 v1.4 has no public write API for those entities,
53+
* so the stub is the conventional integration-test workaround until a
54+
* future write-side release.
55+
*/
56+
class CountersMetersRegistersGroupsIntegrationTest {
57+
58+
private static final String COUNTER_NAME = "MyIngress.my_counter";
59+
private static final String METER_NAME = "MyIngress.my_meter";
60+
private static final String REGISTER_NAME = "MyIngress.my_register";
61+
private static final String SELECTOR_NAME = "MyIngress.my_selector";
62+
private static final int SELECTOR_ID = 287486194;
63+
private static final int SET_EGRESS_ID = 25797781;
64+
private static final int PORT_PARAM_ID = 1;
65+
66+
private static final int COUNTER_SIZE = 64;
67+
private static final int METER_SIZE = 32;
68+
private static final int REGISTER_SIZE = 16;
69+
70+
private static BMv2TestSupport bmv2;
71+
private static P4Switch sw;
72+
private static ManagedChannel rawChannel;
73+
private static P4RuntimeGrpc.P4RuntimeBlockingStub rawStub;
74+
private static long electionId;
75+
76+
@BeforeAll
77+
static void start() throws Exception {
78+
BMv2TestSupport.checkEnvironment();
79+
bmv2 = new BMv2TestSupport("CountersMetersRegistersGroupsIntegrationTest").start();
80+
P4Info p4info = P4Info.fromFile(
81+
Path.of("src/test/resources/p4/counters_meters_registers_groups.p4info.txtpb"));
82+
DeviceConfig dc = DeviceConfig.Bmv2.fromFile(
83+
Path.of("src/test/resources/p4/counters_meters_registers_groups.json"));
84+
sw = P4Switch.connectAsPrimary(bmv2.grpcAddress()).bindPipeline(p4info, dc);
85+
86+
// Parallel stub used only for seeding device state for the action-profile
87+
// member / group tests; never used for read-side assertions.
88+
rawChannel = NettyChannelBuilder.forTarget(bmv2.grpcAddress())
89+
.usePlaintext()
90+
.build();
91+
rawStub = P4RuntimeGrpc.newBlockingStub(rawChannel);
92+
// jp4 picks election ids monotonically; we reuse one that we know it
93+
// has already mastered on this channel (it is the primary).
94+
electionId = 1L;
95+
}
96+
97+
@AfterAll
98+
static void stop() {
99+
if (sw != null) sw.close();
100+
if (rawChannel != null) rawChannel.shutdownNow();
101+
if (bmv2 != null) bmv2.close();
102+
}
103+
104+
@Test
105+
void readCounterReturnsAllCellsInitiallyZero() {
106+
List<CounterEntry> entries = sw.readCounter(COUNTER_NAME).all();
107+
assertEquals(COUNTER_SIZE, entries.size(),
108+
"BMv2 should return every counter cell, even the unwritten ones");
109+
for (CounterEntry e : entries) {
110+
assertEquals(COUNTER_NAME, e.counterName());
111+
assertEquals(0L, e.packetCount(),
112+
"freshly-loaded counter cell " + e.index() + " should report zero packets");
113+
assertEquals(0L, e.byteCount(),
114+
"freshly-loaded counter cell " + e.index() + " should report zero bytes");
115+
}
116+
}
117+
118+
@Test
119+
void readMeterReturnsAllCellsInitiallyDefault() {
120+
List<MeterEntry> entries = sw.readMeter(METER_NAME).all();
121+
assertEquals(METER_SIZE, entries.size(),
122+
"BMv2 should return every meter cell, even the unwritten ones");
123+
for (MeterEntry e : entries) {
124+
assertEquals(METER_NAME, e.meterName());
125+
assertNotNull(e.config(),
126+
"MeterConfig should never be null on the read path");
127+
assertNotNull(e.counterData(),
128+
"MeterCounterData should never be null on the read path");
129+
}
130+
}
131+
132+
@Test
133+
void readRegisterReturnsAllCellsInitiallyZero() throws Exception {
134+
List<RegisterEntry> entries;
135+
try {
136+
entries = sw.readRegister(REGISTER_NAME).all();
137+
} catch (P4OperationException e) {
138+
// BMv2 simple_switch_grpc 1.15.1 returns
139+
// "UNIMPLEMENTED: Register reads are not supported yet" for the
140+
// RegisterEntry read RPC. The P4Runtime spec permits the server
141+
// to refuse a read with UNIMPLEMENTED, and that is what BMv2
142+
// currently does for indirect-register cells. The jp4 read API
143+
// is exercised end-to-end by the unit test in
144+
// P4SwitchReadRegisterTest; this integration test will start
145+
// running automatically once BMv2 adds register-read support.
146+
if (e.getMessage() != null && e.getMessage().contains("UNIMPLEMENTED")) {
147+
Assumptions.assumeTrue(false,
148+
"BMv2 does not yet implement register reads: " + e.getMessage());
149+
}
150+
throw e;
151+
}
152+
assertEquals(REGISTER_SIZE, entries.size(),
153+
"BMv2 should return every register cell, even the unwritten ones");
154+
for (RegisterEntry e : entries) {
155+
assertEquals(REGISTER_NAME, e.registerName());
156+
// Decode the serialised P4Data — the convention RegisterInfo
157+
// promised in a280a1e — and assert the bit<32> value is zero.
158+
P4Data decoded = P4Data.parseFrom(e.data().toByteArray());
159+
byte[] bits = decoded.getBitstring().toByteArray();
160+
for (byte b : bits) {
161+
assertEquals((byte) 0, b,
162+
"freshly-loaded register cell " + e.index()
163+
+ " should hold zero in every byte; got " + bits.length + " bytes");
164+
}
165+
}
166+
}
167+
168+
@Test
169+
void readActionProfileMemberReturnsInsertedMember() {
170+
int memberId = 42;
171+
// The P4Runtime 1.3+ canonical-bytestring rule strips leading zero
172+
// bytes, so a 9-bit port value of 5 round-trips as a single byte.
173+
// Send the canonical form to match what BMv2 stores and returns.
174+
byte[] portBytes = new byte[]{0x05};
175+
seedMember(memberId, portBytes);
176+
177+
List<ActionProfileMember> members = sw.readActionProfileMember(SELECTOR_NAME).all();
178+
assertEquals(1, members.size(),
179+
"exactly one member should be present after the INSERT");
180+
ActionProfileMember m = members.get(0);
181+
assertEquals(SELECTOR_NAME, m.actionProfileName());
182+
assertEquals(memberId, m.memberId());
183+
assertEquals("MyIngress.set_egress", m.action().name());
184+
assertArrayEquals(portBytes, m.action().param("port").toByteArray(),
185+
"the canonical set_egress(port=...) parameter should round-trip end-to-end");
186+
}
187+
188+
@Test
189+
void readActionProfileGroupReturnsInsertedGroup() {
190+
int memberId = 43;
191+
int groupId = 7;
192+
// Canonical-bytestring (P4Runtime 1.3+) — leading zeros stripped.
193+
byte[] portBytes = new byte[]{0x06};
194+
byte[] watchBytes = new byte[]{0x01};
195+
seedMember(memberId, portBytes);
196+
seedGroup(groupId, memberId, /*weight*/ 3, watchBytes);
197+
198+
List<ActionProfileGroup> groups = sw.readActionProfileGroup(SELECTOR_NAME).all();
199+
assertEquals(1, groups.size(),
200+
"exactly one group should be present after the INSERT");
201+
ActionProfileGroup g = groups.get(0);
202+
assertEquals(SELECTOR_NAME, g.actionProfileName());
203+
assertEquals(groupId, g.groupId());
204+
assertEquals(1, g.members().size(), "group should carry one weighted member");
205+
WeightedMember wm = g.members().get(0);
206+
assertEquals(memberId, wm.memberId());
207+
assertEquals(3, wm.weight());
208+
assertNotNull(wm.watchPort(),
209+
"watch_port bytes set on the wire should surface as a non-null watchPort");
210+
assertArrayEquals(watchBytes, wm.watchPort().toByteArray());
211+
}
212+
213+
// ---------- raw-stub seeding helpers --------------------------------
214+
215+
private static void seedMember(int memberId, byte[] portBytes) {
216+
Action action = Action.newBuilder()
217+
.setActionId(SET_EGRESS_ID)
218+
.addParams(Action.Param.newBuilder()
219+
.setParamId(PORT_PARAM_ID)
220+
.setValue(ByteString.copyFrom(portBytes))
221+
.build())
222+
.build();
223+
var member = p4.v1.P4RuntimeOuterClass.ActionProfileMember.newBuilder()
224+
.setActionProfileId(SELECTOR_ID)
225+
.setMemberId(memberId)
226+
.setAction(action)
227+
.build();
228+
Entity entity = Entity.newBuilder().setActionProfileMember(member).build();
229+
sendInsert(entity);
230+
}
231+
232+
private static void seedGroup(int groupId, int memberId, int weight, byte[] watchBytes) {
233+
Member slot = Member.newBuilder()
234+
.setMemberId(memberId)
235+
.setWeight(weight)
236+
.setWatchPort(ByteString.copyFrom(watchBytes))
237+
.build();
238+
var group = p4.v1.P4RuntimeOuterClass.ActionProfileGroup.newBuilder()
239+
.setActionProfileId(SELECTOR_ID)
240+
.setGroupId(groupId)
241+
.setMaxSize(8)
242+
.addMembers(slot)
243+
.build();
244+
Entity entity = Entity.newBuilder().setActionProfileGroup(group).build();
245+
sendInsert(entity);
246+
}
247+
248+
private static void sendInsert(Entity entity) {
249+
WriteRequest req = WriteRequest.newBuilder()
250+
.setDeviceId(0L)
251+
.setElectionId(Uint128.newBuilder()
252+
.setHigh(0L)
253+
.setLow(electionId)
254+
.build())
255+
.addUpdates(Update.newBuilder()
256+
.setType(Update.Type.INSERT)
257+
.setEntity(entity)
258+
.build())
259+
.build();
260+
rawStub.write(req);
261+
}
262+
}

0 commit comments

Comments
 (0)