|
| 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