Line data Source code
1 : /**
2 : * @file WIBEthPacketBuilder_test.cxx
3 : *
4 : * Byte-exact verification of the WIBEth packet builder against a reference
5 : * header assembled from the protocol definitions rather than from the builder:
6 : * the full 42-byte Ethernet/IPv4/UDP layout including checksum, payload
7 : * equality outside the patched DAQ header, input immutability, patch
8 : * semantics, and construction into a misaligned destination.
9 : */
10 :
11 : #include "dpdklibs/wibeth/WIBEthPacketBuilder.hpp"
12 : #include "fdreadoutlibs/DUNEWIBEthTypeAdapter.hpp"
13 :
14 : #define BOOST_TEST_MODULE WIBEthPacketBuilder_test // NOLINT
15 :
16 : #include "boost/test/unit_test.hpp"
17 :
18 : #include <array>
19 : #include <cstddef>
20 : #include <cstdint>
21 : #include <cstring>
22 : #include <vector>
23 :
24 : using namespace dunedaq::dpdklibs;
25 :
26 : namespace {
27 :
28 : // Reference configuration, each field in the notation it is normally quoted
29 : // in. golden_config() hands these to the builder; expected_header() lays the
30 : // same values out on the wire. The two paths share no code.
31 : constexpr std::array<std::uint8_t, 6> kDstMacBytes = { 0x6c, 0xb3, 0x11, 0x79, 0x81, 0x00 };
32 : constexpr std::array<std::uint8_t, 6> kSrcMacBytes = { 0x6c, 0xb3, 0x11, 0x79, 0xb8, 0x6c };
33 : constexpr std::array<std::uint8_t, 4> kSrcIpBytes = { 192, 168, 100, 1 };
34 : constexpr std::array<std::uint8_t, 4> kDstIpBytes = { 192, 168, 100, 2 };
35 : constexpr std::uint16_t kSrcPort = 55677;
36 : constexpr std::uint16_t kDstPort = 55678;
37 : constexpr std::uint16_t kPacketId = 42;
38 :
39 : rte_ether_addr
40 6 : make_mac(const std::array<std::uint8_t, 6>& bytes)
41 : {
42 6 : rte_ether_addr addr{};
43 42 : for (std::size_t i = 0; i < bytes.size(); ++i) {
44 36 : addr.addr_bytes[i] = bytes[i];
45 : }
46 6 : return addr;
47 : }
48 :
49 : // The DAQ header is at Ethernet offset 42 and is not aligned for its 64-bit
50 : // words. Read it through an aligned copy.
51 : dunedaq::detdataformats::DAQEthHeader
52 6 : read_daq_header(const std::uint8_t* frame_start)
53 : {
54 6 : dunedaq::detdataformats::DAQEthHeader header;
55 6 : std::memcpy(&header, frame_start, sizeof(header));
56 6 : return header;
57 : }
58 :
59 : // Ones'-complement sum of 16-bit big-endian words, written independently of
60 : // rte_ipv4_cksum, which the builder uses. Over a valid IPv4 header including
61 : // its checksum field the sum is 0xffff; over the same header with that field
62 : // zero, the complement of the sum is the checksum.
63 : std::uint16_t
64 2 : ones_complement_sum(const std::uint8_t* data, std::size_t bytes)
65 : {
66 2 : std::uint32_t sum = 0;
67 22 : for (std::size_t i = 0; i + 1 < bytes; i += 2) {
68 20 : sum += static_cast<std::uint32_t>((data[i] << 8) | data[i + 1]);
69 : }
70 4 : while ((sum >> 16) != 0) {
71 2 : sum = (sum & 0xffffU) + (sum >> 16);
72 : }
73 2 : return static_cast<std::uint16_t>(sum);
74 : }
75 :
76 : wibeth::PacketConfig
77 3 : golden_config()
78 : {
79 3 : wibeth::PacketConfig cfg;
80 3 : cfg.dst_mac = make_mac(kDstMacBytes);
81 3 : cfg.src_mac = make_mac(kSrcMacBytes);
82 3 : cfg.src_ip = wibeth::parse_ipv4_addr("192.168.100.1");
83 3 : cfg.dst_ip = wibeth::parse_ipv4_addr("192.168.100.2");
84 3 : cfg.src_port = kSrcPort;
85 3 : cfg.dst_port = kDstPort;
86 3 : cfg.packet_id = kPacketId;
87 3 : return cfg;
88 : }
89 :
90 : // The 42-byte header golden_config() must produce, assembled field by field
91 : // from the protocol definitions: Ethernet II, IPv4 (RFC 791), UDP (RFC 768).
92 : // Sizes are literals, byte order is explicit, and the checksum comes from the
93 : // routine above. No builder constant, no DPDK helper and no function under
94 : // test appears here, so this is an independent reference rather than a
95 : // restatement of the implementation.
96 : std::array<std::uint8_t, 42>
97 1 : expected_header()
98 : {
99 1 : constexpr std::uint16_t kIPv4HeaderLen = 20;
100 1 : constexpr std::uint16_t kUDPHeaderLen = 8;
101 1 : constexpr std::uint16_t kWIBEthFrameLen = 7200;
102 1 : constexpr std::size_t kIPv4Offset = 14;
103 1 : constexpr std::size_t kChecksumOffset = kIPv4Offset + 10;
104 :
105 1 : std::array<std::uint8_t, 42> header{};
106 1 : std::size_t at = 0;
107 : // at() rather than operator[]: a layout that writes past the end must throw,
108 : // not corrupt the reference.
109 42 : const auto put8 = [&](std::uint8_t value) { header.at(at++) = value; };
110 10 : const auto put16 = [&](std::uint16_t value) {
111 9 : put8(static_cast<std::uint8_t>(value >> 8));
112 9 : put8(static_cast<std::uint8_t>(value & 0xffU));
113 10 : };
114 5 : const auto put_bytes = [&](const auto& bytes) {
115 24 : for (auto byte : bytes) {
116 20 : put8(byte);
117 : }
118 5 : };
119 :
120 : // Ethernet II: destination MAC, source MAC, EtherType 0x0800 for IPv4.
121 1 : put_bytes(kDstMacBytes);
122 1 : put_bytes(kSrcMacBytes);
123 1 : put16(0x0800);
124 :
125 : // IPv4: version 4 with a 5-word header, no DSCP/ECN, total length,
126 : // identification, no flags or fragment offset, TTL 64, protocol 17 (UDP),
127 : // checksum zero for now, source address, destination address.
128 1 : put8(0x45);
129 1 : put8(0x00);
130 1 : put16(static_cast<std::uint16_t>(kIPv4HeaderLen + kUDPHeaderLen + kWIBEthFrameLen));
131 1 : put16(kPacketId);
132 1 : put16(0x0000);
133 1 : put8(64);
134 1 : put8(17);
135 1 : put16(0x0000);
136 1 : put_bytes(kSrcIpBytes);
137 1 : put_bytes(kDstIpBytes);
138 :
139 : // UDP: source port, destination port, datagram length, checksum unused.
140 1 : put16(kSrcPort);
141 1 : put16(kDstPort);
142 1 : put16(static_cast<std::uint16_t>(kUDPHeaderLen + kWIBEthFrameLen));
143 1 : put16(0x0000);
144 :
145 1 : BOOST_REQUIRE_EQUAL(at, header.size());
146 :
147 : // The checksum covers the IPv4 header with its own field zero, which is the
148 : // state written above.
149 1 : const std::uint16_t checksum = static_cast<std::uint16_t>(~ones_complement_sum(&header[kIPv4Offset], kIPv4HeaderLen));
150 1 : header.at(kChecksumOffset) = static_cast<std::uint8_t>(checksum >> 8);
151 1 : header.at(kChecksumOffset + 1) = static_cast<std::uint8_t>(checksum & 0xffU);
152 :
153 1 : return header;
154 : }
155 :
156 : } // namespace
157 :
158 : BOOST_AUTO_TEST_SUITE(WIBEthPacketBuilder_test)
159 :
160 2 : BOOST_AUTO_TEST_CASE(GoldenHeaderPayloadAndInputImmutability)
161 : {
162 1 : dunedaq::fdreadoutlibs::types::DUNEWIBEthTypeAdapter frame{};
163 1 : std::memset(frame.data, 0xab, sizeof(frame.data));
164 :
165 1 : auto* wibeth_frame = reinterpret_cast<dunedaq::fddetdataformats::WIBEthFrame*>(frame.data);
166 1 : wibeth_frame->daq_header.det_id = 0;
167 1 : wibeth_frame->daq_header.crate_id = 1;
168 1 : wibeth_frame->daq_header.slot_id = 1;
169 1 : wibeth_frame->daq_header.stream_id = 0;
170 1 : wibeth_frame->daq_header.seq_id = 0;
171 1 : wibeth_frame->daq_header.block_length = 0;
172 1 : wibeth_frame->daq_header.timestamp = 123456789;
173 :
174 : // Copy the input so it can be compared after construction.
175 1 : std::vector<std::uint8_t> input_snapshot(frame.data, frame.data + wibeth::kWIBEthFrameBytes);
176 :
177 1 : wibeth::HeaderPatch patch;
178 1 : patch.set_det_id = true;
179 1 : patch.det_id = 3;
180 1 : patch.set_seq_id = true;
181 1 : patch.seq_id = 17;
182 1 : patch.force_block_length = true;
183 :
184 1 : std::vector<std::uint8_t> packet_bytes(wibeth::kEthernetPacketBytes, 0);
185 1 : wibeth::construct_packet(packet_bytes.data(), frame.data, golden_config(), patch);
186 :
187 : // Compare the 42-byte header element-wise so a mismatch reports its offset
188 : // and value.
189 1 : const auto expected = expected_header();
190 1 : BOOST_CHECK_EQUAL_COLLECTIONS(
191 : packet_bytes.begin(), packet_bytes.begin() + expected.size(), expected.begin(), expected.end());
192 :
193 : // Patched DAQ header fields.
194 1 : const auto out_header = read_daq_header(packet_bytes.data() + wibeth::kPacketHeaderBytes);
195 1 : BOOST_CHECK_EQUAL(out_header.det_id, 3);
196 1 : BOOST_CHECK_EQUAL(out_header.crate_id, 1);
197 1 : BOOST_CHECK_EQUAL(out_header.slot_id, 1);
198 1 : BOOST_CHECK_EQUAL(out_header.stream_id, 0);
199 1 : BOOST_CHECK_EQUAL(out_header.seq_id, 17);
200 1 : BOOST_CHECK_EQUAL(out_header.block_length, wibeth::kWIBEthBlockLength);
201 1 : BOOST_CHECK_EQUAL(out_header.timestamp, 123456789);
202 :
203 : // Complete payload equality outside the patched 16-byte DAQ header.
204 1 : BOOST_CHECK_EQUAL(std::memcmp(packet_bytes.data() + wibeth::kPacketHeaderBytes + wibeth::kDAQEthHeaderBytes,
205 : input_snapshot.data() + wibeth::kDAQEthHeaderBytes,
206 : wibeth::kWIBEthFrameBytes - wibeth::kDAQEthHeaderBytes),
207 : 0);
208 :
209 : // The input frame must not have been modified.
210 1 : BOOST_CHECK_EQUAL(std::memcmp(frame.data, input_snapshot.data(), wibeth::kWIBEthFrameBytes), 0);
211 1 : }
212 :
213 2 : BOOST_AUTO_TEST_CASE(IPv4ChecksumVerifiedIndependently)
214 : {
215 : // A second address pair, to check checksum validity beyond the single
216 : // reference vector.
217 1 : dunedaq::fdreadoutlibs::types::DUNEWIBEthTypeAdapter frame{};
218 1 : std::memset(frame.data, 0x5a, sizeof(frame.data));
219 :
220 1 : wibeth::PacketConfig cfg;
221 1 : cfg.src_ip = wibeth::parse_ipv4_addr("10.73.139.16");
222 1 : cfg.dst_ip = wibeth::parse_ipv4_addr("10.73.139.17");
223 1 : cfg.packet_id = 7;
224 :
225 1 : std::vector<std::uint8_t> packet_bytes(wibeth::kEthernetPacketBytes, 0);
226 1 : wibeth::construct_packet(packet_bytes.data(), frame.data, cfg);
227 :
228 1 : const std::uint8_t* ipv4_header = packet_bytes.data() + wibeth::kEthernetHeaderBytes;
229 1 : BOOST_CHECK_EQUAL(ones_complement_sum(ipv4_header, 20), 0xffff);
230 1 : }
231 :
232 2 : BOOST_AUTO_TEST_CASE(PatchDisabledPreservesFrameVerbatim)
233 : {
234 1 : dunedaq::fdreadoutlibs::types::DUNEWIBEthTypeAdapter frame{};
235 1 : std::memset(frame.data, 0x77, sizeof(frame.data));
236 :
237 1 : wibeth::HeaderPatch patch;
238 1 : patch.set_det_id = false;
239 1 : patch.set_seq_id = false;
240 1 : patch.force_block_length = false;
241 :
242 1 : std::vector<std::uint8_t> packet_bytes(wibeth::kEthernetPacketBytes, 0);
243 1 : wibeth::construct_packet(packet_bytes.data(), frame.data, wibeth::PacketConfig{}, patch);
244 :
245 1 : BOOST_CHECK_EQUAL(
246 : std::memcmp(packet_bytes.data() + wibeth::kPacketHeaderBytes, frame.data, wibeth::kWIBEthFrameBytes), 0);
247 1 : }
248 :
249 2 : BOOST_AUTO_TEST_CASE(SequenceIdWrapsModulo4096)
250 : {
251 1 : dunedaq::fdreadoutlibs::types::DUNEWIBEthTypeAdapter frame{};
252 1 : std::memset(frame.data, 0, sizeof(frame.data));
253 1 : wibeth::PacketConfig cfg;
254 :
255 1 : wibeth::HeaderPatch patch;
256 1 : patch.set_seq_id = true;
257 :
258 1 : std::vector<std::uint8_t> packet_bytes(wibeth::kEthernetPacketBytes, 0);
259 :
260 1 : patch.seq_id = 4095;
261 1 : wibeth::construct_packet(packet_bytes.data(), frame.data, cfg, patch);
262 1 : BOOST_CHECK_EQUAL(read_daq_header(packet_bytes.data() + wibeth::kPacketHeaderBytes).seq_id, 4095);
263 :
264 1 : patch.seq_id = 4096;
265 1 : wibeth::construct_packet(packet_bytes.data(), frame.data, cfg, patch);
266 1 : BOOST_CHECK_EQUAL(read_daq_header(packet_bytes.data() + wibeth::kPacketHeaderBytes).seq_id, 0);
267 1 : }
268 :
269 2 : BOOST_AUTO_TEST_CASE(DetIdUpperBoundary63)
270 : {
271 1 : dunedaq::fdreadoutlibs::types::DUNEWIBEthTypeAdapter frame{};
272 1 : std::memset(frame.data, 0, sizeof(frame.data));
273 1 : wibeth::PacketConfig cfg;
274 :
275 1 : wibeth::HeaderPatch patch;
276 1 : patch.set_det_id = true;
277 1 : patch.det_id = 63; // largest value the 6-bit field can carry
278 :
279 1 : std::vector<std::uint8_t> packet_bytes(wibeth::kEthernetPacketBytes, 0);
280 1 : wibeth::construct_packet(packet_bytes.data(), frame.data, cfg, patch);
281 1 : BOOST_CHECK_EQUAL(read_daq_header(packet_bytes.data() + wibeth::kPacketHeaderBytes).det_id, 63);
282 1 : }
283 :
284 2 : BOOST_AUTO_TEST_CASE(DetIdTruncatesToSixBits)
285 : {
286 : // The patch stores det_id in the 6-bit DAQEthHeader field, so 64 wraps to 0.
287 : // The plugin rejects det_id > 63 at configuration. Direct users of the
288 : // builder get this truncation.
289 1 : dunedaq::fdreadoutlibs::types::DUNEWIBEthTypeAdapter frame{};
290 1 : std::memset(frame.data, 0, sizeof(frame.data));
291 1 : wibeth::PacketConfig cfg;
292 :
293 1 : wibeth::HeaderPatch patch;
294 1 : patch.set_det_id = true;
295 1 : patch.det_id = 64;
296 :
297 1 : std::vector<std::uint8_t> packet_bytes(wibeth::kEthernetPacketBytes, 0);
298 1 : wibeth::construct_packet(packet_bytes.data(), frame.data, cfg, patch);
299 1 : BOOST_CHECK_EQUAL(read_daq_header(packet_bytes.data() + wibeth::kPacketHeaderBytes).det_id, 0);
300 1 : }
301 :
302 2 : BOOST_AUTO_TEST_CASE(ConstructsIntoMisalignedDestination)
303 : {
304 1 : dunedaq::fdreadoutlibs::types::DUNEWIBEthTypeAdapter frame{};
305 1 : std::memset(frame.data, 0xcd, sizeof(frame.data));
306 :
307 1 : wibeth::HeaderPatch patch;
308 1 : patch.set_det_id = true;
309 1 : patch.det_id = 5;
310 1 : patch.set_seq_id = true;
311 1 : patch.seq_id = 9;
312 :
313 : // Offset the destination by one byte so no field is naturally aligned.
314 : // Construction reaches caller storage only through memcpy, so the result must
315 : // be byte-identical to an aligned construction.
316 1 : std::vector<std::uint8_t> aligned(wibeth::kEthernetPacketBytes, 0);
317 1 : wibeth::construct_packet(aligned.data(), frame.data, golden_config(), patch);
318 :
319 1 : std::vector<std::uint8_t> raw(wibeth::kEthernetPacketBytes + 1, 0);
320 1 : std::uint8_t* dst = raw.data() + 1;
321 1 : wibeth::construct_packet(dst, frame.data, golden_config(), patch);
322 :
323 1 : BOOST_CHECK_EQUAL(std::memcmp(dst, aligned.data(), wibeth::kEthernetPacketBytes), 0);
324 1 : BOOST_CHECK_EQUAL(raw.back(), 0xcd);
325 :
326 1 : const auto out_header = read_daq_header(dst + wibeth::kPacketHeaderBytes);
327 1 : BOOST_CHECK_EQUAL(out_header.det_id, 5);
328 1 : BOOST_CHECK_EQUAL(out_header.seq_id, 9);
329 1 : BOOST_CHECK_EQUAL(out_header.block_length, wibeth::kWIBEthBlockLength);
330 1 : }
331 :
332 : BOOST_AUTO_TEST_SUITE_END()
|