[PATCH can-next v2 6/9] selftests: can: add J1939 wire-format tests against CAN_RAW
From: Oleksij Rempel
Date: Wed Nov 04 2026 - 11:26:09 EST
Check J1939 single frames against a CAN_RAW socket in both directions,
for PDU1 (with the DP bit set) and PDU2: the CAN id and payload of a
J1939 send, and the payload, source address and PGN that the J1939
socket gets for an injected frame.
The base test sends between two J1939 sockets, so a CAN id encoding bug
and a matching decoding bug in the kernel cancel out there. CAN_RAW does
not use the J1939 code, so it checks each direction on its own.
Signed-off-by: Oleksij Rempel <o.rempel@xxxxxxxxxxxxxx>
---
Q (sashiko): does the test check the priority of a received frame, or
the SCM_J1939_PRIO cmsg?
A: Neither. It checks the priority of sent frames only; the cmsg
metadata of received messages is not tested yet.
Q (sashiko): can the TX tests see a frame from another node?
A: No. They still read the first frame, but j1939_lib.sh runs them
only on a vcan it creates, where no other node sends.
Changes in v2:
- drop the Assisted-by trailer; the cover letter says how an LLM was used
- wrap at 80 columns and fix the checkpatch warnings (Marc)
- name all constants, avoid casts, return bool from helpers named
*_ok() and fill test payloads with fill_pattern()
- check that a sent frame is an extended data frame (sashiko, Marc)
- check the full PGN of a sent frame (sashiko); use a PDU1 PGN with DP
set
- check the source address and PGN of a received frame (sashiko)
- say in the commit message what CAN_RAW checks that the base test
cannot (sashiko)
- keep the Makefile and .gitignore lists sorted; the wrapper no longer
sets J1939_TEST_PROG
---
tools/testing/selftests/net/can/.gitignore | 1 +
tools/testing/selftests/net/can/Makefile | 1 +
tools/testing/selftests/net/can/test_j1939_wire.c | 186 +++++++++++++++++++++
tools/testing/selftests/net/can/test_j1939_wire.sh | 4 +
4 files changed, 192 insertions(+)
diff --git a/tools/testing/selftests/net/can/.gitignore b/tools/testing/selftests/net/can/.gitignore
index c9b50d4865c4a1f7523b96516669d7fc6e0d151c..d83fd28a50766c25069c60d10deb76e5722bba5b 100644
--- a/tools/testing/selftests/net/can/.gitignore
+++ b/tools/testing/selftests/net/can/.gitignore
@@ -2,4 +2,5 @@
test_j1939_addr
test_j1939_base
test_j1939_opts
+test_j1939_wire
test_raw_filter
diff --git a/tools/testing/selftests/net/can/Makefile b/tools/testing/selftests/net/can/Makefile
index d72095ad7d849d8a35c20e9c5bdaa81bc194bc64..5262f0b8f73f80f524c32665c1fc08a7a0ec7e78 100644
--- a/tools/testing/selftests/net/can/Makefile
+++ b/tools/testing/selftests/net/can/Makefile
@@ -10,6 +10,7 @@ TEST_PROGS := \
test_j1939_addr.sh \
test_j1939_base.sh \
test_j1939_opts.sh \
+ test_j1939_wire.sh \
test_raw_filter.sh \
# end of TEST_PROGS
diff --git a/tools/testing/selftests/net/can/test_j1939_wire.c b/tools/testing/selftests/net/can/test_j1939_wire.c
new file mode 100644
index 0000000000000000000000000000000000000000..ad542ed241c0b0e59c40cc49bccc418031273002
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_wire.c
@@ -0,0 +1,186 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause)
+/*
+ * J1939 wire-format selftests: a J1939 socket against CAN_RAW.
+ *
+ * Run by test_j1939_wire.sh, see j1939_tests.h.
+ */
+#include <errno.h>
+
+#include "kselftest_harness.h"
+#include "j1939_tests.h"
+
+/* arbitrary addresses and PGNs; only PDU class and DP bit matter */
+#define SA_LOCAL 0x90 /* the J1939 socket under test */
+#define SA_PEER 0x20 /* the CAN_RAW peer */
+/* a destination-specific PGN with DP set, and a broadcast PGN */
+#define PGN_PDU1 (J1939_PGN_DP | 0x0ab00)
+#define PGN_PDU2 0x0feee
+_Static_assert(J1939_IS_PDU1(PGN_PDU1) && !J1939_IS_PDU1(PGN_PDU2),
+ "PGN_PDU1 must be PDU1 and PGN_PDU2 must be PDU2");
+
+#define PRIO J1939_PRIO_DEFAULT
+
+FIXTURE(j1939_wire)
+{
+ int ifindex;
+ int j1939;
+ int raw;
+};
+
+FIXTURE_SETUP(j1939_wire)
+{
+ self->j1939 = -1;
+ self->raw = -1;
+
+ J1939_SETUP_IFINDEX(self);
+
+ J1939_SETUP_RAW(self, J1939_RAW_RCVBUF_DEFAULT);
+}
+
+FIXTURE_TEARDOWN(j1939_wire)
+{
+ if (self->j1939 >= 0)
+ close(self->j1939);
+ if (self->raw >= 0)
+ close(self->raw);
+}
+
+/*
+ * Inject one CAN frame from the raw peer and check that the J1939 socket
+ * receives the same CAN_MAX_DLEN-byte payload, from SA_PEER and with PGN @pgn.
+ * Opens the J1939 RX socket into self->j1939 (the fixture teardown closes it).
+ * @bcast selects a broadcast (PDU2) socket and frame; @pgn and @da address the
+ * injected frame.
+ */
+static bool rx_single_frame_ok(FIXTURE_DATA(j1939_wire) *self, bool bcast,
+ pgn_t pgn, uint8_t da,
+ const uint8_t payload[CAN_MAX_DLEN])
+{
+ struct sockaddr_can src;
+
+ if (bcast)
+ self->j1939 = j1939_open_flags(self->ifindex, J1939_NO_NAME,
+ SA_LOCAL, pgn, J1939_OPEN_BCAST);
+ else
+ self->j1939 = j1939_open(self->ifindex, SA_LOCAL, pgn);
+ if (self->j1939 < 0)
+ return false;
+
+ if (!send_raw8_ok(self->raw, PRIO, pgn, da, SA_PEER, payload))
+ return false;
+
+ if (!recvfrom_verify_ok(self->j1939, payload, CAN_MAX_DLEN,
+ J1939_RECV_TIMEOUT_MS, &src)) {
+ fprintf(stderr, "# payload missing or not the one sent\n");
+ return false;
+ }
+
+ if (src.can_addr.j1939.addr != SA_PEER ||
+ src.can_addr.j1939.pgn != pgn) {
+ fprintf(stderr,
+ "# from SA 0x%02x PGN 0x%05x, want 0x%02x 0x%05x\n",
+ src.can_addr.j1939.addr, src.can_addr.j1939.pgn,
+ SA_PEER, pgn);
+ return false;
+ }
+ return true;
+}
+
+/*
+ * Send one CAN_MAX_DLEN-byte payload from the J1939 socket to the raw peer and
+ * return the CAN frame seen on the wire via *frame, so each caller can assert
+ * the id fields it cares about. Opens the J1939 TX socket into self->j1939
+ * (the fixture teardown closes it) and connects it to (@da, @pgn); @bcast
+ * opens a broadcast (PDU2) socket.
+ */
+static bool tx_to_raw_send_recv_ok(FIXTURE_DATA(j1939_wire) *self, bool bcast,
+ pgn_t pgn, uint8_t da,
+ const uint8_t payload[CAN_MAX_DLEN],
+ struct can_frame *frame)
+{
+ if (bcast)
+ self->j1939 = j1939_open_flags(self->ifindex, J1939_NO_NAME,
+ SA_LOCAL, J1939_NO_PGN,
+ J1939_OPEN_BCAST);
+ else
+ self->j1939 = j1939_open(self->ifindex, SA_LOCAL, J1939_NO_PGN);
+ if (self->j1939 < 0)
+ return false;
+
+ if (!j1939_connect_ok(self->j1939, self->ifindex, da, pgn))
+ return false;
+
+ if (!send_all_ok(self->j1939, payload, CAN_MAX_DLEN))
+ return false;
+
+ return recv_frame_ok(self->raw, frame, J1939_RECV_TIMEOUT_MS);
+}
+
+/* J1939 TX -> CAN_RAW: destination-specific (PDU1) single frame. */
+TEST_F(j1939_wire, tx_pdu1_to_raw)
+{
+ uint8_t payload[CAN_MAX_DLEN];
+ struct can_frame frame;
+
+ fill_pattern(payload, sizeof(payload));
+ ASSERT_TRUE(tx_to_raw_send_recv_ok(self, false, PGN_PDU1, SA_PEER,
+ payload, &frame));
+ ASSERT_TRUE(j1939_is_data_frame(frame.can_id))
+ TH_LOG("CAN id 0x%08x is not a 29-bit data frame",
+ frame.can_id);
+
+ EXPECT_EQ(j1939_id_prio(frame.can_id), PRIO);
+ EXPECT_EQ(j1939_id_sa(frame.can_id), SA_LOCAL);
+ EXPECT_EQ(j1939_id_ps(frame.can_id), SA_PEER);
+ /* PGN, with the PS (the destination) masked off */
+ EXPECT_EQ(j1939_id_pgn(frame.can_id) & J1939_PGN_PDU1_MAX, PGN_PDU1);
+
+ ASSERT_EQ(frame.can_dlc, sizeof(payload));
+ EXPECT_EQ(memcmp(frame.data, payload, sizeof(payload)), 0);
+}
+
+/* CAN_RAW -> J1939 RX: destination-specific (PDU1) single frame. */
+TEST_F(j1939_wire, rx_pdu1_from_raw)
+{
+ uint8_t payload[CAN_MAX_DLEN];
+
+ fill_pattern(payload, sizeof(payload));
+ ASSERT_TRUE(rx_single_frame_ok(self, false, PGN_PDU1, SA_LOCAL,
+ payload));
+}
+
+/* J1939 TX -> CAN_RAW: broadcast (PDU2) single frame. */
+TEST_F(j1939_wire, tx_pdu2_to_raw)
+{
+ uint8_t payload[CAN_MAX_DLEN];
+ struct can_frame frame;
+
+ fill_pattern(payload, sizeof(payload));
+ ASSERT_TRUE(tx_to_raw_send_recv_ok(self, true, PGN_PDU2, J1939_NO_ADDR,
+ payload, &frame));
+ ASSERT_TRUE(j1939_is_data_frame(frame.can_id))
+ TH_LOG("CAN id 0x%08x is not a 29-bit data frame",
+ frame.can_id);
+
+ EXPECT_EQ(j1939_id_prio(frame.can_id), PRIO);
+ EXPECT_EQ(j1939_id_sa(frame.can_id), SA_LOCAL);
+ EXPECT_EQ(j1939_id_pgn(frame.can_id), PGN_PDU2);
+
+ ASSERT_EQ(frame.can_dlc, sizeof(payload));
+ EXPECT_EQ(memcmp(frame.data, payload, sizeof(payload)), 0);
+}
+
+/* CAN_RAW -> J1939 RX: broadcast (PDU2) single frame. */
+TEST_F(j1939_wire, rx_pdu2_from_raw)
+{
+ uint8_t payload[CAN_MAX_DLEN];
+
+ fill_pattern(payload, sizeof(payload));
+ ASSERT_TRUE(rx_single_frame_ok(self, true, PGN_PDU2, J1939_NO_ADDR,
+ payload));
+}
+
+int main(int argc, char **argv)
+{
+ return j1939_test_main(argc, argv);
+}
diff --git a/tools/testing/selftests/net/can/test_j1939_wire.sh b/tools/testing/selftests/net/can/test_j1939_wire.sh
new file mode 100755
index 0000000000000000000000000000000000000000..61f65365ec568f928b58bc4f892f97e9ec887027
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_wire.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+source "$(dirname "$0")/j1939_lib.sh"
--
2.47.3