mirror of
https://github.com/logicog/RTLPlayground.git
synced 2026-10-29 17:54:14 +08:00
Merge branch 'main' into fix/cpu-vlan-init
This commit is contained in:
191 files changed
+4925
-1825
No files matched your search
+12
-1
@@ -31,7 +31,9 @@ TESTS = $(BUILDDIR)/test_cmd_editor \
|
||||
$(BUILDDIR)/test_tick_gate \
|
||||
$(BUILDDIR)/test_cpu_vlan_full \
|
||||
$(BUILDDIR)/test_cpu_vlan_restricted \
|
||||
$(BUILDDIR)/test_cpu_vlan_8372
|
||||
$(BUILDDIR)/test_cpu_vlan_8372\
|
||||
$(BUILDDIR)/test_nic_rx \
|
||||
$(BUILDDIR)/test_lacp
|
||||
|
||||
# The table tests link the register mock and one environment file; stub/
|
||||
# stands in for the two headers a firmware build generates.
|
||||
@@ -54,9 +56,18 @@ $(BUILDDIR)/test_port_tables: test_port_tables.c $(TABLE_ENV) hw_mock.h | $(BUIL
|
||||
$(BUILDDIR)/test_tick_gate: test_tick_gate.c support.c $(SRCROOT)/cmd_editor.c | $(BUILDDIR)
|
||||
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
||||
|
||||
# Exercise the production RX ring reader; only the hardware DMA edge is mocked.
|
||||
# Discard unused TX functions so this RX fixture needs no TX register mocks.
|
||||
$(BUILDDIR)/test_nic_rx: test_nic_rx.c $(SRCROOT)/rtl837x_nic.c $(SRCROOT)/rtl837x_nic.h | $(BUILDDIR)
|
||||
$(CC) $(CFLAGS) -ffunction-sections -fdata-sections $(filter %.c,$^) -o $@ $(LDFLAGS) -Wl,--gc-sections
|
||||
|
||||
$(BUILDDIR)/test_page_json: test_page_json.c $(TABLE_ENV) hw_mock.h | $(BUILDDIR)
|
||||
$(CC) $(CFLAGS) $(filter %.c,$^) -o $@ $(LDFLAGS)
|
||||
|
||||
# rtl837x_lacp.c is compiled UNMODIFIED from the firmware tree.
|
||||
$(BUILDDIR)/test_lacp: test_lacp.c $(SRCROOT)/rtl837x_lacp.c | $(BUILDDIR)
|
||||
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
||||
|
||||
# httpd/httpd.c and uip/uip.c are compiled UNMODIFIED from the firmware tree;
|
||||
# this test carries the environment below them itself.
|
||||
$(BUILDDIR)/test_httpd_tx: test_httpd_tx.c $(SRCROOT)/httpd/httpd.c $(SRCROOT)/uip/uip.c | $(BUILDDIR)
|
||||
|
||||
@@ -48,6 +48,7 @@ fires — so it drops straight into CI.
|
||||
| `test_port_tables` | `rtl837x_port.c` | VLAN entry layout and round trip, PVID register sharing, static multicast and management entries, per-port flush, trunk membership and hash seed |
|
||||
| `test_page_json` | `httpd/page_impl.c` + `rtl837x_port.c` | `/vlan.json`, `/vlanlist`, `/l2.json` (walk, wrap marker, paging inside `outbuf`), 64-bit counters in `/status.json` and `/counters.json` |
|
||||
| `test_httpd_tx` | `httpd/httpd.c` + `uip/uip.c` | a GET of a static file against a client that moves its receive window: ACK accounting, continuation chunks out of flash, retransmission |
|
||||
| `test_lacp` | `rtl837x_lacp.c` | state machines against a simulated 802.3ad partner: convergence, mis-cabling, expiry, re-election, two groups, delivery by trap |
|
||||
|
||||
## Adding a test for another module
|
||||
1. Write `test_<module>.c` with `main()` driving the module's entry points and
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "syslog.h"
|
||||
#include "rtl837x_phy.h"
|
||||
#include "support.h"
|
||||
#include "rtl837x_lacp.h"
|
||||
|
||||
/* ---- console: everything lands in out_buf (support.c) ---- */
|
||||
void print_byte(uint8_t v)
|
||||
@@ -133,3 +134,15 @@ uint8_t stp_prio = 0x80, stp_root_port = 0xff, stp_rstp = 1, stp_txhold = 6;
|
||||
uint8_t sfp_buf[16];
|
||||
bool sfp_read_block(uint8_t slot, uint8_t reg, uint8_t len) { (void)slot; (void)reg; (void)len; return false; }
|
||||
void print_phys_port(uint8_t port) { itoa_short(machine.log_to_phys_port[port]); }
|
||||
|
||||
uint8_t lacpEnabled;
|
||||
uint8_t lacp_actor_state[10];
|
||||
uint8_t lacp_partner_state[10];
|
||||
uint8_t lacp_rx_state[10];
|
||||
uint8_t lacp_partner_sys[10][MAC_ADDR_LEN];
|
||||
uint16_t lacp_rx_count[10];
|
||||
uint8_t lacp_port_lag[10];
|
||||
uint16_t lacp_lag_ports[LACP_NUM_LAGS];
|
||||
uint8_t lacp_agg_sys[LACP_NUM_LAGS][MAC_ADDR_LEN];
|
||||
uint8_t lacp_agg_valid[LACP_NUM_LAGS];
|
||||
uint16_t lacp_members_last[LACP_NUM_LAGS];
|
||||
@@ -112,7 +112,7 @@ void read_reg_timer(uint32_t *tmr) { *tmr = 0; }
|
||||
|
||||
/* uip-conf.h asks uIP not to define the packet buffer: on the switch it lives
|
||||
* at a fixed XDATA address, so the bench provides the storage itself. */
|
||||
u8_t uip_buf[UIP_BUFSIZE + 2];
|
||||
u8_t uip_buf[UIP_CONF_BUFFER_ALLOC_SIZE];
|
||||
|
||||
uint16_t strlen_x(const char *s) { return (uint16_t)strlen(s); }
|
||||
|
||||
@@ -148,6 +148,7 @@ void send_mirror(void) { }
|
||||
void send_mtu(void) { }
|
||||
void send_config(void) { }
|
||||
void send_cmd_log(void) { }
|
||||
void send_lacp(void) { }
|
||||
void send_lag(void) { }
|
||||
void send_stp(void) { }
|
||||
void send_stp_counters(void) { }
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "rtl837x_common.h"
|
||||
#include "machine.h"
|
||||
#include "uip.h"
|
||||
#include "rtl837x_regs.h"
|
||||
#include "rtl837x_lacp.h"
|
||||
|
||||
/* ---------- platform mocks ---------- */
|
||||
|
||||
uint8_t uip_buf[UIP_CONF_BUFFER_ALLOC_SIZE];
|
||||
uint16_t uip_len;
|
||||
struct uip_eth_addr uip_ethaddr = { .addr = {0x02,0x11,0x22,0x33,0x44,0x55} };
|
||||
const struct machine machine = { .min_port = 0, .max_port = 7,
|
||||
.log_to_phys_port = {5, 1, 2, 3, 4, 6, 7, 8, 9} };
|
||||
struct machine_runtime machine_detected = { .isRTL8373 = 1 };
|
||||
uint8_t lacpEnabled;
|
||||
uint16_t management_vlan; /* mocked; slow-protocol path zeroes it per-frame */
|
||||
|
||||
static int verbose = 0;
|
||||
void print_string(const char *s) { if (verbose) fputs(s, stdout); }
|
||||
void print_byte(uint8_t b) { if (verbose) printf("%02x", b); }
|
||||
void print_short(uint16_t v) { if (verbose) printf("%04x", v); }
|
||||
void write_char(char c) { if (verbose) putchar(c); }
|
||||
|
||||
/* Trunk programming mock: record last mask per LAG + call count.
|
||||
* hw_members mirrors LAG 0 so the single-aggregator scenarios read naturally. */
|
||||
static uint16_t hw_members_lag[4];
|
||||
static int hw_set_calls;
|
||||
#define hw_members hw_members_lag[0]
|
||||
void port_lag_members_set(uint8_t lag, uint16_t members)
|
||||
{
|
||||
if (lag < 4)
|
||||
hw_members_lag[lag] = members;
|
||||
hw_set_calls++;
|
||||
}
|
||||
|
||||
/* Reserved-multicast action mock: delivery of a LACPDU to the CPU rests on
|
||||
* the action of the slow-protocols entry alone, so record what the engine
|
||||
* sets. The real trap_rma_set() touches only the action field. */
|
||||
static uint16_t hw_rma_reg;
|
||||
static uint8_t hw_rma_act;
|
||||
static int hw_rma_calls;
|
||||
void trap_rma_set(uint16_t reg, uint8_t act);
|
||||
void trap_rma_set(uint16_t reg, uint8_t act)
|
||||
{
|
||||
hw_rma_reg = reg;
|
||||
hw_rma_act = act;
|
||||
hw_rma_calls++;
|
||||
}
|
||||
|
||||
/* ---------- on-wire mirrors (independent re-statement of the layout) ---------- */
|
||||
|
||||
struct wire_info {
|
||||
uint16_t sys_prio;
|
||||
uint8_t sys[MAC_ADDR_LEN];
|
||||
uint16_t key;
|
||||
uint16_t port_prio;
|
||||
uint16_t port;
|
||||
uint8_t state;
|
||||
uint8_t reserved[3];
|
||||
};
|
||||
|
||||
struct wire_pdu_out { /* what lacp_send() writes at uip_buf+12 */
|
||||
uint8_t dst[MAC_ADDR_LEN];
|
||||
uint8_t src[MAC_ADDR_LEN];
|
||||
struct rtl_tag rtl_tag;
|
||||
uint16_t ethertype;
|
||||
uint8_t subtype;
|
||||
uint8_t version;
|
||||
uint8_t tlv_actor, actor_len;
|
||||
struct wire_info actor;
|
||||
uint8_t tlv_partner, partner_len;
|
||||
struct wire_info partner;
|
||||
uint8_t tlv_collector, collector_len;
|
||||
uint16_t collector_max_delay;
|
||||
uint8_t collector_reserved[12];
|
||||
uint8_t tlv_terminator, terminator_len;
|
||||
uint8_t terminator_reserved[50];
|
||||
};
|
||||
|
||||
struct wire_pdu_in { /* forwarded form: the hardware inserts a VLAN tag */
|
||||
uint8_t dst[MAC_ADDR_LEN];
|
||||
uint8_t src[MAC_ADDR_LEN];
|
||||
struct rtl_tag rtl_tag;
|
||||
struct vlan_tag vlan_tag;
|
||||
uint16_t ethertype;
|
||||
uint8_t subtype;
|
||||
uint8_t version;
|
||||
uint8_t tlv_actor, actor_len;
|
||||
struct wire_info actor;
|
||||
uint8_t tlv_partner, partner_len;
|
||||
struct wire_info partner;
|
||||
};
|
||||
|
||||
_Static_assert(sizeof(struct wire_info) == 18, "info TLV body must be 18");
|
||||
_Static_assert(sizeof(struct wire_pdu_out) == 132, "TX pdu must be 132");
|
||||
_Static_assert(sizeof(struct rtl_tag) == 8, "rtl tag must be 8");
|
||||
|
||||
/* ---------- TX capture ---------- */
|
||||
|
||||
static struct wire_pdu_out last_tx[10];
|
||||
static int tx_count[10];
|
||||
|
||||
void tcpip_output(void)
|
||||
{
|
||||
struct wire_pdu_out *p = (struct wire_pdu_out *)&uip_buf[RTL_FRAME_DESC_SIZE];
|
||||
uint16_t pmask = HTONS(p->rtl_tag.pmask);
|
||||
for (int port = 0; port < 10; port++) {
|
||||
if (pmask & (1u << port)) {
|
||||
last_tx[port] = *p;
|
||||
tx_count[port]++;
|
||||
}
|
||||
}
|
||||
uip_len = 0;
|
||||
}
|
||||
|
||||
/* ---------- simulated partner ---------- */
|
||||
|
||||
/* Deliver a LACPDU into the switch on `port`, from partner system `sys`.
|
||||
* pstate = partner's Actor_State flags. If `echo`, the partner echoes the
|
||||
* switch's last-seen actor block back (fresh view) - required for quiescence. */
|
||||
static void partner_frame_full(uint8_t port, const uint8_t sys[MAC_ADDR_LEN], uint8_t pstate,
|
||||
int echo, uint16_t len, int tagged)
|
||||
{
|
||||
struct wire_pdu_in in;
|
||||
memset(&in, 0, sizeof(in));
|
||||
|
||||
in.dst[0]=0x01; in.dst[1]=0x80; in.dst[2]=0xc2; in.dst[5]=0x02;
|
||||
memcpy(in.src, sys, MAC_ADDR_LEN);
|
||||
in.rtl_tag.tag = HTONS(RTL_FRAME_TAG_ID);
|
||||
in.rtl_tag.pmask = HTONS(port); /* RX: 4-bit ingress port */
|
||||
in.vlan_tag.svlan = HTONS(0x8100); /* the tag's TPID, which marks the body offset */
|
||||
in.ethertype = HTONS(0x8809);
|
||||
in.subtype = 0x01;
|
||||
in.version = 0x01;
|
||||
|
||||
in.tlv_actor = 0x01; in.actor_len = 0x14;
|
||||
in.actor.sys_prio = HTONS(0x8000);
|
||||
memcpy(in.actor.sys, sys, MAC_ADDR_LEN);
|
||||
in.actor.key = HTONS(0x0011);
|
||||
in.actor.port_prio = HTONS(0x00ff);
|
||||
in.actor.port = HTONS((uint16_t)port + 101);
|
||||
in.actor.state = pstate;
|
||||
|
||||
in.tlv_partner = 0x02; in.partner_len = 0x14;
|
||||
if (echo) { /* fresh view of us, from our last TX */
|
||||
in.partner.sys_prio = last_tx[port].actor.sys_prio;
|
||||
memcpy(in.partner.sys, last_tx[port].actor.sys, MAC_ADDR_LEN);
|
||||
in.partner.key = last_tx[port].actor.key;
|
||||
in.partner.port = last_tx[port].actor.port;
|
||||
in.partner.state = last_tx[port].actor.state;
|
||||
} /* else: zeros = stale view */
|
||||
|
||||
memcpy(uip_buf, &in, sizeof(in));
|
||||
if (!tagged) { /* trapped: no VLAN tag, so the body sits earlier */
|
||||
uint8_t *body = uip_buf + RTL_FRAME_DESC_SIZE + RTL_TAG_SIZE;
|
||||
memmove(body, body + VLAN_TAG_SIZE,
|
||||
sizeof(in) - (RTL_FRAME_DESC_SIZE + RTL_TAG_SIZE + VLAN_TAG_SIZE));
|
||||
len -= VLAN_TAG_SIZE;
|
||||
}
|
||||
uip_len = len;
|
||||
lacp_in();
|
||||
}
|
||||
|
||||
static void partner_frame_len(uint8_t port, const uint8_t sys[MAC_ADDR_LEN], uint8_t pstate, int echo, uint16_t len)
|
||||
{
|
||||
partner_frame_full(port, sys, pstate, echo, len, 1);
|
||||
}
|
||||
|
||||
static void partner_frame(uint8_t port, const uint8_t sys[MAC_ADDR_LEN], uint8_t pstate, int echo)
|
||||
{
|
||||
partner_frame_full(port, sys, pstate, echo, sizeof(struct wire_pdu_in), 1);
|
||||
}
|
||||
|
||||
/* The same LACPDU as the reserved-multicast trap delivers it. */
|
||||
static void partner_frame_trap(uint8_t port, const uint8_t sys[MAC_ADDR_LEN], uint8_t pstate, int echo)
|
||||
{
|
||||
partner_frame_full(port, sys, pstate, echo, sizeof(struct wire_pdu_in), 0);
|
||||
}
|
||||
|
||||
/* Advance time by n system ticks; the LACP timers step every fourth one */
|
||||
volatile uint32_t ticks;
|
||||
static void step(int n) { while (n--) { ticks++; lacp_timers(); } }
|
||||
|
||||
/* ---------- scenario runner ---------- */
|
||||
|
||||
static int failures;
|
||||
#define CHECK(cond, name) do { \
|
||||
if (cond) printf("PASS %s\n", name); \
|
||||
else { printf("FAIL %s\n", name); failures++; } \
|
||||
} while (0)
|
||||
|
||||
#define P_ACT LACP_STATE_ACTIVITY
|
||||
#define P_AGG LACP_STATE_AGGREGATION
|
||||
#define P_SYNC LACP_STATE_SYNC
|
||||
#define P_COL LACP_STATE_COLLECTING
|
||||
#define P_DIST LACP_STATE_DISTRIBUTING
|
||||
#define P_TO LACP_STATE_TIMEOUT /* partner requests short (fast) timeout */
|
||||
|
||||
static const uint8_t SYS_A[MAC_ADDR_LEN] = {0x02,0xaa,0xaa,0xaa,0xaa,0x01};
|
||||
static const uint8_t SYS_B[MAC_ADDR_LEN] = {0x02,0xbb,0xbb,0xbb,0xbb,0x02};
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
verbose = (argc > 1 && !strcmp(argv[1], "-v"));
|
||||
|
||||
/* Boot init first: on host, zeroed globals would otherwise read as "every
|
||||
* port belongs to LAG 0" (the firmware calls this from main() the same way) */
|
||||
lacp_init();
|
||||
|
||||
/* T1: enable announces on every port with sane field contents */
|
||||
lacp_lag_set(0, 0x00ff);
|
||||
step(8);
|
||||
int all_tx = 1, sane = 1;
|
||||
for (int i = machine.min_port; i <= machine.max_port; i++) {
|
||||
if (!tx_count[i]) all_tx = 0;
|
||||
if (last_tx[i].ethertype != HTONS(0x8809) || last_tx[i].subtype != 1
|
||||
|| last_tx[i].dst[5] != 0x02
|
||||
|| !(last_tx[i].actor.state & (P_ACT|P_AGG|P_TO)))
|
||||
sane = 0;
|
||||
}
|
||||
CHECK(all_tx && sane, "T1 enable: LACPDU announced on all ports, well-formed");
|
||||
|
||||
/* T2: partner without SYNC -> actor SYNC only, no members yet */
|
||||
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO, 1);
|
||||
partner_frame(1, SYS_A, P_ACT|P_AGG|P_TO, 1);
|
||||
step(8);
|
||||
CHECK((lacp_actor_state[0] & P_SYNC) && !(lacp_actor_state[0] & (P_COL|P_DIST))
|
||||
&& hw_members == 0,
|
||||
"T2 partner not in sync: actor SYNC only, trunk empty");
|
||||
|
||||
/* T3: partner in sync on ports 0,1 -> full converge, members 0x0003 */
|
||||
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
partner_frame(1, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(8);
|
||||
CHECK(hw_members == 0x0003
|
||||
&& (lacp_actor_state[0] & (P_SYNC|P_COL|P_DIST)) == (P_SYNC|P_COL|P_DIST)
|
||||
&& (lacp_actor_state[1] & (P_SYNC|P_COL|P_DIST)) == (P_SYNC|P_COL|P_DIST),
|
||||
"T3 convergence: both ports collecting/distributing, trunk 0x0003");
|
||||
|
||||
/* T3b: partner-block echo must be VERBATIM. A Linux 802.3ad partner accepts
|
||||
* our SYNC only if our Partner TLV mirrors its actor identity exactly; a
|
||||
* hardcoded sys_prio/port_prio (instead of the recorded value) makes Linux
|
||||
* clear partner-SYNC and the bond never distributes. Verify we echo the
|
||||
* partner's own priorities back, not a constant. */
|
||||
CHECK(HTONS(last_tx[0].partner.sys_prio) == 0x8000
|
||||
&& HTONS(last_tx[0].partner.port_prio) == 0x00ff
|
||||
&& HTONS(last_tx[0].partner.key) == 0x0011
|
||||
&& HTONS(last_tx[0].partner.port) == (uint16_t)0 + 101,
|
||||
"T3b partner echo: recorded partner priorities/key/port echoed verbatim");
|
||||
|
||||
/* T4: mis-cabling - port 2 sees a DIFFERENT system: must stay out */
|
||||
partner_frame(2, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(8);
|
||||
CHECK(hw_members == 0x0003 && !(lacp_actor_state[2] & P_SYNC),
|
||||
"T4 mis-cabling: port with different partner system stays out");
|
||||
|
||||
/* T5: register-write economy: stable state must not rewrite the trunk */
|
||||
int calls_before = hw_set_calls;
|
||||
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
partner_frame(1, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(64);
|
||||
CHECK(hw_set_calls == calls_before,
|
||||
"T5 stable state: no redundant trunk register writes");
|
||||
|
||||
/* T6: stale echo triggers retransmit (update_NTT) */
|
||||
int txc = tx_count[0];
|
||||
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 0 /* stale view of us */);
|
||||
step(8);
|
||||
CHECK(tx_count[0] > txc, "T6 update_NTT: stale partner view causes retransmit");
|
||||
|
||||
/* T7: expiry - partner silent past the short timeout: trunk drains,
|
||||
* aggregator identity is released */
|
||||
step(4 * LACP_SHORT_TIMEOUT + 64); /* past the short timeout */
|
||||
CHECK(hw_members == 0 && lacp_agg_valid[0] == 0,
|
||||
"T7 expiry: members drop to 0 and aggregator is released");
|
||||
|
||||
/* T8: re-convergence with a NEW partner system after release */
|
||||
/* (needs a fresh announce first so the echo below carries current state) */
|
||||
step(4 * 0x0100 + 8); /* let periodic TX refresh last_tx */
|
||||
partner_frame(3, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
partner_frame(3, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(8);
|
||||
CHECK(hw_members == 0x0008 && (lacp_actor_state[3] & P_COL),
|
||||
"T8 re-election: new partner system forms a fresh aggregate");
|
||||
|
||||
/* T9: lacp off clears the trunk */
|
||||
lacp_off();
|
||||
CHECK(hw_members == 0 && lacpEnabled == 0, "T9 disable: trunk cleared");
|
||||
|
||||
/* ---- per-LAG scenarios: two independent aggregators ---- */
|
||||
|
||||
/* T10: lag 1 lacp {0,1} + lag 2 lacp {2,3}, both to the SAME partner
|
||||
* system (worst case for sibling scoping): each converges into its own
|
||||
* hardware trunk, and the engine reports enabled. */
|
||||
lacp_lag_set(1, 0x0003); /* ports 0,1 */
|
||||
lacp_lag_set(2, 0x000c); /* ports 2,3 */
|
||||
step(8); /* announce */
|
||||
for (int r = 0; r < 2; r++) { /* two rounds so echoes carry fresh state */
|
||||
for (int p = 0; p < 4; p++)
|
||||
partner_frame(p, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(8);
|
||||
}
|
||||
CHECK(lacpEnabled == 1
|
||||
&& hw_members_lag[1] == 0x0003 && hw_members_lag[2] == 0x000c,
|
||||
"T10 two LAGs: independent aggregates converge on their own trunks");
|
||||
|
||||
/* T11: removing one LAG releases only its trunk; the other keeps running */
|
||||
lacp_lag_set(1, 0);
|
||||
CHECK(hw_members_lag[1] == 0 && hw_members_lag[2] == 0x000c
|
||||
&& lacpEnabled == 1,
|
||||
"T11 remove one LAG: its trunk drains, the other LAG unaffected");
|
||||
|
||||
/* T12: a port in no LACP LAG ignores LACPDUs entirely */
|
||||
int rx5_before = tx_count[5];
|
||||
partner_frame(5, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(8);
|
||||
CHECK(!(lacp_actor_state[5] & P_SYNC) && hw_members_lag[2] == 0x000c,
|
||||
"T12 unassigned port: LACPDU ignored, no state change");
|
||||
(void)rx5_before;
|
||||
|
||||
/* T13: removing the last LACP LAG shuts the engine down */
|
||||
lacp_lag_set(2, 0);
|
||||
CHECK(lacpEnabled == 0 && hw_members_lag[2] == 0
|
||||
&& hw_rma_reg == RTL837X_RMA2_CONF && hw_rma_act == RTL837X_RMA_ACT_DROP,
|
||||
"T13 last LAG removed: engine off, trunk drained, address dropped");
|
||||
|
||||
/* T14: while the engine runs, the slow-protocols address is trapped to
|
||||
* the CPU. The trap keys on the address alone, so delivery needs no L2
|
||||
* entry and nothing written per VLAN or per PVID. */
|
||||
hw_rma_calls = 0;
|
||||
lacp_lag_set(0, 0x0003);
|
||||
CHECK(hw_rma_calls == 1 && hw_rma_reg == RTL837X_RMA2_CONF
|
||||
&& hw_rma_act == RTL837X_RMA_ACT_TRAP,
|
||||
"T14 engine on traps 01:80:c2:00:00:02 to the CPU");
|
||||
|
||||
/* T15: a trapped LACPDU arrives without the VLAN tag a forwarded one
|
||||
* carries, so its body sits four bytes earlier. Both forms must parse. */
|
||||
uint16_t rx_before = lacp_rx_count[0];
|
||||
lacp_partner_state[0] = 0; /* all three are written past the ethertype gate */
|
||||
memset(lacp_partner_sys[0], 0, MAC_ADDR_LEN);
|
||||
partner_frame_trap(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
int trap_ok = lacp_rx_count[0] == (uint16_t)(rx_before + 1)
|
||||
&& lacp_partner_state[0] == (P_ACT|P_AGG|P_TO|P_SYNC)
|
||||
&& !memcmp(lacp_partner_sys[0], SYS_A, MAC_ADDR_LEN);
|
||||
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
CHECK(trap_ok && lacp_rx_count[0] == (uint16_t)(rx_before + 2),
|
||||
"T15 trapped LACPDU parses like a forwarded one");
|
||||
|
||||
/* T16: the partner changes one cable at a time. Port 1 starts hearing B
|
||||
* while port 0 still hears A, then A goes quiet and B appears on port 0
|
||||
* too. The aggregator must let go of A once no port hears it, even though
|
||||
* port 1 was CURRENT the whole time, or the LAG never aggregates with B. */
|
||||
lacp_lag_set(0, 0);
|
||||
lacp_lag_set(0, 0x0003);
|
||||
step(8);
|
||||
for (int r = 0; r < 3; r++) {
|
||||
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
partner_frame(1, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(8);
|
||||
}
|
||||
for (int t = 0; t < 40; t++) { /* port 1 moved to B, port 0 still on A */
|
||||
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
partner_frame(1, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(4 * 40);
|
||||
}
|
||||
int stayed_out = hw_members == 0x0001 && !(lacp_actor_state[1] & P_SYNC);
|
||||
for (int t = 0; t < 60; t++) /* port 0 unplugged from A, past the short timeout */
|
||||
{ partner_frame(1, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1); step(4 * 40); }
|
||||
for (int t = 0; t < 40; t++) { /* port 0 now on B as well */
|
||||
partner_frame(0, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
partner_frame(1, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(4 * 40);
|
||||
}
|
||||
CHECK(stayed_out && hw_members == 0x0003 && !memcmp(lacp_agg_sys[0], SYS_B, MAC_ADDR_LEN),
|
||||
"T16 re-cabling one port at a time: aggregator moves from A to B");
|
||||
|
||||
/* T17: a group that already has a STATIC trunk is put under LACP with the
|
||||
* same ports (the LAG page's Static -> LACP switch sends only "lag N lacp").
|
||||
* The static trunk must go: until the partner converges the ports are not
|
||||
* an aggregate, and after a reboot the config replays only the LACP line. */
|
||||
hw_members_lag[1] = 0x0030; /* static lag 2: ports 4,5 */
|
||||
lacp_lag_set(1, 0x0030);
|
||||
step(8);
|
||||
CHECK(hw_members_lag[1] == 0 && lacp_members_last[1] == 0 && lacp_lag_ports[1] == 0x0030,
|
||||
"T17 static trunk taken over: cleared while the group negotiates");
|
||||
lacp_lag_set(1, 0);
|
||||
|
||||
/* T18: a port named in a second group moves there. The first group's
|
||||
* candidate set drops it, so taking the second group out later leaves no
|
||||
* orphan that group 1 still shows as a candidate but never runs LACP on. */
|
||||
lacp_lag_set(1, 0x0030); /* ports 4,5 */
|
||||
step(8);
|
||||
for (int r = 0; r < 2; r++) {
|
||||
partner_frame(4, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
partner_frame(5, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(8);
|
||||
}
|
||||
int converged = hw_members_lag[1] == 0x0030;
|
||||
lacp_lag_set(2, 0x0060); /* ports 5,6: port 5 moves from group 2 to 3 */
|
||||
int moved = lacp_lag_ports[1] == 0x0010 && lacp_port_lag[5] == 2
|
||||
&& hw_members_lag[1] == 0x0010;
|
||||
lacp_lag_set(2, 0);
|
||||
int no_orphan = lacp_port_lag[5] == LACP_LAG_NONE && lacp_lag_ports[1] == 0x0010;
|
||||
lacp_lag_set(3, 0x0010); /* the last port leaves group 2 as well */
|
||||
CHECK(converged && moved && no_orphan && lacp_lag_ports[1] == 0 && lacp_agg_valid[1] == 0,
|
||||
"T18 port moves between groups: old group drops it from candidates and trunk");
|
||||
lacp_lag_set(3, 0);
|
||||
|
||||
/* T20: taking a group out of LACP leaves nothing of the partner behind
|
||||
* for "lacp show" to report as if the port still heard it. */
|
||||
lacp_lag_set(1, 0x0030);
|
||||
step(8);
|
||||
partner_frame(4, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(8);
|
||||
int had_partner = lacp_rx_state[4] == LACP_RX_CURRENT && lacp_rx_count[4] > 0;
|
||||
lacp_lag_set(1, 0);
|
||||
CHECK(had_partner && lacp_partner_state[4] == 0 && lacp_rx_state[4] == LACP_RX_INITIALIZE
|
||||
&& lacp_rx_count[4] == 0 && !memcmp(lacp_partner_sys[4], "\0\0\0\0\0\0", MAC_ADDR_LEN),
|
||||
"T20 release: partner state, rx state, counter and system cleared");
|
||||
|
||||
/* T21: applying the same candidate set again (the LAG page resends it
|
||||
* with every Apply) must not tear the trunk down and renegotiate. */
|
||||
lacp_lag_set(1, 0x0030);
|
||||
step(8);
|
||||
for (int r = 0; r < 2; r++) {
|
||||
partner_frame(4, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
partner_frame(5, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(8);
|
||||
}
|
||||
int before = hw_set_calls;
|
||||
lacp_lag_set(1, 0x0030);
|
||||
CHECK(hw_members_lag[1] == 0x0030 && hw_set_calls == before
|
||||
&& (lacp_actor_state[4] & P_COL),
|
||||
"T21 same ports applied again: trunk and state untouched");
|
||||
lacp_lag_set(1, 0);
|
||||
|
||||
/* T22: a runt on the slow-protocols address must not be parsed as a
|
||||
* LACPDU; the partner block would be read from stale buffer bytes. */
|
||||
lacp_lag_set(1, 0x0030);
|
||||
step(8);
|
||||
partner_frame_len(4, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1, 40); /* cut before the partner block */
|
||||
CHECK(lacp_rx_count[4] == 0 && lacp_rx_state[4] == LACP_RX_INITIALIZE,
|
||||
"T22 truncated LACPDU ignored: the port does not record it");
|
||||
lacp_lag_set(1, 0);
|
||||
|
||||
/* T23: the partner asks for the long timeout (Linux's default slow rate).
|
||||
* current_while is armed from OUR LACP_Timeout, which is short, so a
|
||||
* partner that stops speaking LACP with the link up is dropped after the
|
||||
* short timeout, not after two minutes. */
|
||||
lacp_lag_set(1, 0x0030);
|
||||
step(8);
|
||||
for (int r = 0; r < 2; r++) {
|
||||
partner_frame(4, SYS_A, P_ACT|P_AGG|P_SYNC, 1);
|
||||
partner_frame(5, SYS_A, P_ACT|P_AGG|P_SYNC, 1);
|
||||
step(8);
|
||||
}
|
||||
int slow_converged = hw_members_lag[1] == 0x0030;
|
||||
step(4 * LACP_SHORT_TIMEOUT + 64);
|
||||
CHECK(slow_converged && hw_members_lag[1] == 0,
|
||||
"T23 partner on slow rate goes quiet: dropped after our short timeout");
|
||||
lacp_lag_set(1, 0);
|
||||
|
||||
/* T24: a LACPDU carrying our own system id (two ports of one group
|
||||
* cabled to each other) must not be taken for a partner. */
|
||||
lacp_lag_set(1, 0x0030);
|
||||
step(8);
|
||||
partner_frame(4, uip_ethaddr.addr, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
CHECK(lacp_rx_count[4] == 0 && lacp_rx_state[4] == LACP_RX_INITIALIZE,
|
||||
"T24 own system id: LACPDU ignored");
|
||||
lacp_lag_set(1, 0);
|
||||
|
||||
printf("\n%s (%d failure%s)\n", failures ? "SANDBOX: FAILURES" : "SANDBOX: ALL PASS",
|
||||
failures, failures == 1 ? "" : "s");
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "rtl837x_common.h"
|
||||
#include "rtl837x_regs.h"
|
||||
#include "rtl837x_nic.h"
|
||||
|
||||
uint8_t rx_headers[16];
|
||||
|
||||
/* Model the DMA behavior reported on SWTGW218AS: a transfer starting inside
|
||||
* the RX ring wraps at its end. An initial address outside the ring instead
|
||||
* reads flat ASIC memory, including stale TX bytes at 0x2800. */
|
||||
static uint8_t asic[0x8000];
|
||||
static struct {
|
||||
uint8_t before[16];
|
||||
uint8_t data[UIP_CONF_BUFFER_ALLOC_SIZE];
|
||||
uint8_t after[16];
|
||||
} destination;
|
||||
|
||||
struct dma_request {
|
||||
uint8_t *buffer;
|
||||
uint16_t ring;
|
||||
uint8_t words;
|
||||
};
|
||||
static struct dma_request requests[8];
|
||||
static unsigned request_count, fail_request;
|
||||
static int failures;
|
||||
|
||||
static void check(bool condition, const char *what)
|
||||
{
|
||||
if (!condition) {
|
||||
printf(" FAIL: %s\n", what);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
bool nic_rx_dma(uint8_t *buffer, uint16_t ring, uint8_t words)
|
||||
{
|
||||
size_t bytes = (size_t)words * 8;
|
||||
unsigned call = ++request_count;
|
||||
|
||||
if (call <= sizeof(requests) / sizeof(requests[0]))
|
||||
requests[call - 1] = (struct dma_request){ buffer, ring, words };
|
||||
if (call == fail_request || !words || (size_t)ring + bytes > sizeof(asic))
|
||||
return false;
|
||||
if (ring < RTL837X_NIC_RX_RING_BYTES) {
|
||||
for (size_t i = 0; i < bytes; i++)
|
||||
buffer[i] = asic[((size_t)ring + i) % RTL837X_NIC_RX_RING_BYTES];
|
||||
} else {
|
||||
memcpy(buffer, asic + ring, bytes);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void fixture_reset(void)
|
||||
{
|
||||
memset(asic, 0xd7, sizeof(asic));
|
||||
memset(&destination, 0xa5, sizeof(destination));
|
||||
memset(rx_headers, 0x6c, sizeof(rx_headers));
|
||||
memset(requests, 0, sizeof(requests));
|
||||
request_count = 0;
|
||||
fail_request = 0;
|
||||
}
|
||||
|
||||
static bool filled(const uint8_t *data, size_t len, uint8_t value)
|
||||
{
|
||||
while (len--)
|
||||
if (*data++ != value)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool guards_intact(void)
|
||||
{
|
||||
return filled(destination.before, sizeof(destination.before), 0xa5)
|
||||
&& filled(destination.after, sizeof(destination.after), 0xa5);
|
||||
}
|
||||
|
||||
static bool requests_start_in_rx_ring(void)
|
||||
{
|
||||
for (unsigned i = 0; i < request_count; i++) {
|
||||
if (i >= sizeof(requests) / sizeof(requests[0]) || !requests[i].words
|
||||
|| (requests[i].ring & 7)
|
||||
|| requests[i].ring >= RTL837X_NIC_RX_RING_BYTES)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ring_write(uint16_t offset, const uint8_t *data, size_t len)
|
||||
{
|
||||
for (size_t i = 0; i < len; i++)
|
||||
asic[((size_t)offset + i) % RTL837X_NIC_RX_RING_BYTES] = data[i];
|
||||
}
|
||||
|
||||
static void put_descriptor(uint16_t offset, uint16_t len)
|
||||
{
|
||||
uint8_t descriptor[8] = { 0x03, 0x00, 0xb0, 0xc8, len, len >> 8, 0, 0 };
|
||||
ring_write(offset, descriptor, sizeof(descriptor));
|
||||
}
|
||||
|
||||
static void expect_descriptor(uint16_t len)
|
||||
{
|
||||
uint8_t descriptor[8] = { 0x03, 0x00, 0xb0, 0xc8, len, len >> 8, 0, 0 };
|
||||
check(!memcmp(rx_headers, descriptor, sizeof(descriptor)), "the first eight descriptor bytes stay intact");
|
||||
}
|
||||
|
||||
static void pattern(uint8_t *data, size_t len)
|
||||
{
|
||||
for (size_t i = 0; i < len; i++)
|
||||
data[i] = (uint8_t)(i * 37 + (i >> 8) + 0x13);
|
||||
}
|
||||
|
||||
static void t_last_descriptor(void)
|
||||
{
|
||||
static const uint8_t root_bpdu[68] = {
|
||||
0x01, 0x80, 0xc2, 0, 0, 0, 0xd0, 0xea, 0x11, 0x12, 0x34, 0x56,
|
||||
0x88, 0x99, 0x04, 0, 0, 0, 0, 8,
|
||||
0, 0x27, 0x42, 0x42, 3, 0, 0, 2, 2, 0x3c,
|
||||
0x10, 0, 0xd0, 0xea, 0x11, 0x12, 0x34, 0x56, 0, 0, 0, 0,
|
||||
0x10, 0, 0xd0, 0xea, 0x11, 0x12, 0x34, 0x56, 0x80, 0x0a,
|
||||
0, 0, 0x14, 0, 2, 0, 0x0f, 0, 0
|
||||
};
|
||||
uint8_t stale_tx[72] = { 0 };
|
||||
uint16_t last = RTL837X_NIC_RX_RING_BYTES - 8;
|
||||
|
||||
printf("[test] the last RX descriptor wraps its BPDU payload before DMA\n");
|
||||
fixture_reset();
|
||||
put_descriptor(last, sizeof(root_bpdu));
|
||||
ring_write(0, root_bpdu, sizeof(root_bpdu));
|
||||
memcpy(stale_tx, root_bpdu, sizeof(root_bpdu));
|
||||
stale_tx[17] = 0x20;
|
||||
stale_tx[19] = 1; /* retained TX destination mask, not ingress port 8 */
|
||||
memcpy(asic + RTL837X_NIC_RX_RING_BYTES, stale_tx, sizeof(stale_tx));
|
||||
check(nic_rx_header(last), "the final descriptor is readable");
|
||||
expect_descriptor(sizeof(root_bpdu));
|
||||
check(nic_rx_packet(destination.data, last + 8), "a payload address at the RX ring limit wraps to zero");
|
||||
check(!memcmp(destination.data, root_bpdu, sizeof(root_bpdu)), "the complete upstream BPDU is read instead of stale TX bytes");
|
||||
check(destination.data[19] == 8, "the upstream ingress tag survives the ring boundary");
|
||||
check(request_count == 2 && requests[0].ring == last && requests[0].words == 1
|
||||
&& requests[1].ring == 0 && requests[1].words == 9,
|
||||
"descriptor and rounded payload DMA both start in the RX ring");
|
||||
check(requests_start_in_rx_ring() && guards_intact(), "DMA starts inside the RX ring and preserves destination guards");
|
||||
expect_descriptor(sizeof(root_bpdu));
|
||||
}
|
||||
|
||||
static void t_payload_layouts(void)
|
||||
{
|
||||
static const struct {
|
||||
uint16_t descriptor, len;
|
||||
} cases[] = {
|
||||
{ 0x0100, 8 }, { 0x0100, 16 }, { 0x0100, 64 },
|
||||
{ 0x0100, 68 }, { 0x0100, 72 },
|
||||
{ RTL837X_NIC_RX_RING_BYTES - 16, 8 },
|
||||
{ RTL837X_NIC_RX_RING_BYTES - 16, 16 },
|
||||
{ RTL837X_NIC_RX_RING_BYTES - 16, 68 },
|
||||
{ RTL837X_NIC_RX_RING_BYTES - 72, 64 },
|
||||
{ RTL837X_NIC_RX_RING_BYTES - 48, 68 }
|
||||
};
|
||||
uint8_t frame[80];
|
||||
|
||||
printf("[test] payload DMA handles ordinary, exact-boundary, and crossing frames\n");
|
||||
pattern(frame, sizeof(frame));
|
||||
for (unsigned i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
||||
size_t rounded = (cases[i].len + 7u) & ~7u;
|
||||
fixture_reset();
|
||||
put_descriptor(cases[i].descriptor, cases[i].len);
|
||||
ring_write(cases[i].descriptor + 8, frame, rounded);
|
||||
check(nic_rx_header(cases[i].descriptor), "the descriptor transfer completes");
|
||||
check(nic_rx_packet(destination.data, cases[i].descriptor + 8), "the payload transfer completes");
|
||||
check(!memcmp(destination.data, frame, rounded), "every payload and permitted padding byte reaches the correct destination offset");
|
||||
check(filled(destination.data + rounded, sizeof(destination.data) - rounded, 0xa5), "DMA does not write beyond the rounded frame");
|
||||
check(request_count == 2, "the payload uses one DMA request even when crossing the ring end");
|
||||
check(requests_start_in_rx_ring() && guards_intact(), "crossing DMA starts inside the RX ring and preserves destination guards");
|
||||
expect_descriptor(cases[i].len);
|
||||
}
|
||||
}
|
||||
|
||||
static void t_maximum_frames(void)
|
||||
{
|
||||
uint8_t frame[(UIP_CONF_BUFFER_SIZE + 7u) & ~7u];
|
||||
static const uint16_t lengths[] = { 1, 7, 1549, 1550, 1551, 1552, 1553, 1554, 1555, 1556 };
|
||||
|
||||
printf("[test] rounded maximum frames fit without overwriting their descriptor\n");
|
||||
pattern(frame, sizeof(frame));
|
||||
for (unsigned layout = 0; layout < 3; layout++) {
|
||||
uint16_t descriptor = layout == 0 ? 0x0100
|
||||
: RTL837X_NIC_RX_RING_BYTES - (layout == 1 ? 16 : 8);
|
||||
for (unsigned i = 0; i < sizeof(lengths) / sizeof(lengths[0]); i++) {
|
||||
uint16_t len = lengths[i];
|
||||
size_t rounded = (len + 7u) & ~7u;
|
||||
fixture_reset();
|
||||
put_descriptor(descriptor, len);
|
||||
ring_write(descriptor + 8, frame, rounded);
|
||||
check(nic_rx_header(descriptor), "the maximum-frame descriptor transfer completes");
|
||||
check(nic_rx_packet(destination.data, descriptor + 8), "all supported frame lengths fit the destination");
|
||||
check(!memcmp(destination.data, frame, rounded), "the full rounded frame is preserved");
|
||||
check(filled(destination.data + rounded, sizeof(destination.data) - rounded, 0xa5), "rounded DMA writes no bytes beyond the available destination");
|
||||
check(request_count == 2, "maximum frames use a single payload DMA request");
|
||||
check(requests_start_in_rx_ring() && guards_intact(), "rounded DMA starts inside the RX ring and preserves destination guards");
|
||||
expect_descriptor(len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void t_invalid_frames(void)
|
||||
{
|
||||
static const uint16_t invalid_lengths[] = { 0, UIP_CONF_BUFFER_SIZE + 1, 0xffff };
|
||||
static const uint16_t invalid_pointers[] = { 1, 7, RTL837X_NIC_RX_RING_BYTES + 8, 0xfff8 };
|
||||
|
||||
printf("[test] invalid lengths and DMA addresses are rejected before transfer\n");
|
||||
for (unsigned i = 0; i < sizeof(invalid_lengths) / sizeof(invalid_lengths[0]); i++) {
|
||||
fixture_reset();
|
||||
rx_headers[4] = invalid_lengths[i];
|
||||
rx_headers[5] = invalid_lengths[i] >> 8;
|
||||
check(!nic_rx_packet(destination.data, 0x0100), "unsupported or overflowing lengths are rejected");
|
||||
check(!request_count && filled(destination.data, sizeof(destination.data), 0xa5) && guards_intact(),
|
||||
"invalid frame lengths cause no DMA or destination changes");
|
||||
}
|
||||
for (unsigned i = 0; i < sizeof(invalid_pointers) / sizeof(invalid_pointers[0]); i++) {
|
||||
fixture_reset();
|
||||
rx_headers[4] = 68;
|
||||
rx_headers[5] = 0;
|
||||
check(!nic_rx_header(invalid_pointers[i]), "an invalid descriptor address is rejected");
|
||||
check(!nic_rx_packet(destination.data, invalid_pointers[i]), "an invalid payload address is rejected");
|
||||
check(!request_count && guards_intact(), "invalid addresses never reach DMA");
|
||||
}
|
||||
fixture_reset();
|
||||
check(!nic_rx_header(RTL837X_NIC_RX_RING_BYTES) && !request_count,
|
||||
"a descriptor at the ring limit is rejected rather than wrapped");
|
||||
}
|
||||
|
||||
static void t_dma_failure(void)
|
||||
{
|
||||
uint8_t frame[UIP_CONF_BUFFER_ALLOC_SIZE];
|
||||
static const uint16_t lengths[] = { 68, UIP_CONF_BUFFER_SIZE };
|
||||
|
||||
printf("[test] failed descriptor and single-payload DMA transfers propagate failure\n");
|
||||
fixture_reset();
|
||||
fail_request = 1;
|
||||
check(!nic_rx_header(0x0100) && request_count == 1, "descriptor DMA failure reaches the caller");
|
||||
pattern(frame, sizeof(frame));
|
||||
for (unsigned layout = 0; layout < 3; layout++) {
|
||||
uint16_t descriptor = layout == 0 ? 0x0100
|
||||
: RTL837X_NIC_RX_RING_BYTES - (layout == 1 ? 16 : 8);
|
||||
for (unsigned i = 0; i < sizeof(lengths) / sizeof(lengths[0]); i++) {
|
||||
uint16_t len = lengths[i];
|
||||
fixture_reset();
|
||||
put_descriptor(descriptor, len);
|
||||
ring_write(descriptor + 8, frame, (len + 7u) & ~7u);
|
||||
check(nic_rx_header(descriptor), "the descriptor precedes the injected payload failure");
|
||||
fail_request = 2;
|
||||
check(!nic_rx_packet(destination.data, descriptor + 8), "payload DMA failure reaches the caller");
|
||||
check(request_count == fail_request, "a failed payload DMA is not retried or split");
|
||||
check(filled(destination.data, sizeof(destination.data), 0xa5) && guards_intact(),
|
||||
"an injected failure before DMA preserves the destination and guards");
|
||||
expect_descriptor(len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
t_last_descriptor();
|
||||
t_payload_layouts();
|
||||
t_maximum_frames();
|
||||
t_invalid_frames();
|
||||
t_dma_failure();
|
||||
if (failures) {
|
||||
printf("%d NIC RX check(s) failed\n", failures);
|
||||
return 1;
|
||||
}
|
||||
puts("all NIC RX checks passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -185,6 +185,23 @@ static void t_l2_page(void)
|
||||
CHECK(balanced(), "every page is valid JSON");
|
||||
}
|
||||
|
||||
static void t_status_macs(void)
|
||||
{
|
||||
const char *p1, *end, *mac;
|
||||
|
||||
printf("[test] /status.json reports the MAC addresses learned on each port\n");
|
||||
hw_reset(); out_reset();
|
||||
hw_reg_set(RTL837X_L2_LRN_PORT_CONSTRT_CNT + (1 << 2), 0x21);
|
||||
send_status();
|
||||
p1 = strstr(body(), "\"logPort\":1,");
|
||||
end = p1 ? strchr(p1, '}') : NULL;
|
||||
mac = p1 ? strstr(p1, "\"macs\":\"0x21\"") : NULL;
|
||||
CHECK(mac && mac < end, "port 1 shows its learned count");
|
||||
CHECK(has("\"macs\":\"0x0\""), "a port without learned addresses shows 0");
|
||||
CHECK(slen <= TCP_OUTBUF_SIZE, "fits the buffer");
|
||||
CHECK(balanced(), "valid JSON");
|
||||
}
|
||||
|
||||
static void t_status_counters(void)
|
||||
{
|
||||
printf("[test] /status.json prints 64-bit packet counters in full\n");
|
||||
@@ -211,6 +228,7 @@ int main(void)
|
||||
t_l2();
|
||||
t_l2_page();
|
||||
t_status_counters();
|
||||
t_status_macs();
|
||||
printf("\n%d checks, %d failed\n", tests_run, tests_failed);
|
||||
return tests_failed ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile the current firmware execute_config() with mocked flash/tokenizer edges."""
|
||||
import pathlib, subprocess
|
||||
root=pathlib.Path(__file__).resolve().parent.parent
|
||||
src=(root/'cmd_parser.c').read_text()
|
||||
start=src.index('void execute_config(void) __banked')
|
||||
end=src.index('// Execute multiple commands',start)
|
||||
body=src[start:end]
|
||||
out=root/'test/build';out.mkdir(exist_ok=True)
|
||||
harness=r"""
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#define __xdata
|
||||
#define __banked
|
||||
#define CONFIG_START 0
|
||||
#define CONFIG_LEN 1024
|
||||
#define FLASH_READ_BURST_SIZE 256
|
||||
#define CMD_BUF_SIZE 64
|
||||
#define ERR_OK 0
|
||||
#define NUL 0
|
||||
#define PASSWORD "test-default"
|
||||
uint8_t flash_buf[256],cmd_buffer[64],save_cmd,err_status;
|
||||
char passwd[32],image[1024],seen[16][64];int count;
|
||||
struct {uint32_t addr;uint16_t len;} flash_region;
|
||||
void strtox(char*d,const char*s){strcpy(d,s);}
|
||||
void flash_read_bulk(uint8_t*d){memcpy(d,image+flash_region.addr,256);}
|
||||
void print_string(const char*s){(void)s;}
|
||||
void print_string_x(uint8_t*s){(void)s;}
|
||||
void write_char(char c){(void)c;}
|
||||
void clear_command_history(void){}
|
||||
void cmd_tokenize(void){err_status=cmd_buffer[0]=='#'?1:0;}
|
||||
void cmd_parser(void){assert(count<16);strcpy(seen[count++],(char*)cmd_buffer);}
|
||||
"""
|
||||
tests=r"""
|
||||
void run(const char*s){memset(image,0,sizeof image);strncpy(image,s,sizeof image-1);count=0;execute_config();assert(save_cmd==1);}
|
||||
int main(void){
|
||||
run("one\ntwo\n");assert(count==2&&!strcmp(seen[1],"two"));
|
||||
run("one\rtwo\r");assert(count==2&&!strcmp(seen[0],"one"));
|
||||
run("one\r\ntwo\r\n");assert(count==2&&!strcmp(seen[1],"two"));
|
||||
run("\r\n# ignored\r\none\r\n\r\ntwo");assert(count==2&&!strcmp(seen[0],"one"));
|
||||
run("one\0ignored");assert(count==1&&!strcmp(seen[0],"one"));
|
||||
char longline[300];memset(longline,'x',sizeof longline);strcpy(longline+280,"\r\nok\r\n");run(longline);assert(count==1&&!strcmp(seen[0],"ok"));
|
||||
/* CR at the end of a flash burst, LF at the start of the next. */
|
||||
char boundary[300];memset(boundary,'\n',255);strcpy(boundary+255,"\r\nok\r\n");run(boundary);assert(count==1&&!strcmp(seen[0],"ok"));
|
||||
puts("startup CR/LF/CRLF, comments, NUL, overlong and burst-boundary cases passed");return 0;
|
||||
}
|
||||
"""
|
||||
(out/'startup_crlf.c').write_text(harness+body+tests)
|
||||
subprocess.run(['gcc','-std=gnu11','-Wall','-Wextra','-Werror','-g','-fsanitize=address,undefined','-fno-omit-frame-pointer','-no-pie',str(out/'startup_crlf.c'),'-o',str(out/'test_startup_crlf')],check=True)
|
||||
subprocess.run([str(out/'test_startup_crlf')],check=True)
|
||||
Reference in new issue
Block a user