From 5620e950ce0148bd28ca37ec5df4527cbf240ded Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:55:30 +0200 Subject: [PATCH] test: simulate the register file and the table engine, and test the tables The switch is reached through four register functions and the SFR_DATA registers, so a flat register file behind them is enough to run the table code on the host. A few addresses are live: writing TBL_CTRL with EXECUTE performs the VLAN or L2 operation at once and clears the bit, STAT_GET fetches a counter, L2_TBL_FLUSH_CTRL drops entries. Nothing ever reports busy, so a polling loop runs once and the tests say nothing about what happens when hardware stalls; that question was settled elsewhere. The header states the VLAN and L2 entry layouts on its own, so a test can hold what rtl837x_port.c writes against a second reading of the same bits rather than against itself. The first suite does that for the VLAN entry, the PVID register two ports share, the static multicast and management entries, the per-port flush and the trunk registers. Two stub headers stand in for the ones a firmware build generates, so the harness needs no SDCC build first. --- .gitignore | 4 + test/Makefile | 15 +- test/README.md | 11 ++ test/env_tables.c | 123 +++++++++++++++ test/hw_mock.c | 342 ++++++++++++++++++++++++++++++++++++++++ test/hw_mock.h | 59 +++++++ test/stub/html_data.h | 9 ++ test/stub/version.h | 5 + test/test_port_tables.c | 181 +++++++++++++++++++++ 9 files changed, 747 insertions(+), 2 deletions(-) create mode 100644 test/env_tables.c create mode 100644 test/hw_mock.c create mode 100644 test/hw_mock.h create mode 100644 test/stub/html_data.h create mode 100644 test/stub/version.h create mode 100644 test/test_port_tables.c diff --git a/.gitignore b/.gitignore index 3ccb2819..f5c8e8c7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ version.h tools/httpd_sim tools/injector tools/fileadder + +# host-test stand-ins for the two generated headers +!test/stub/html_data.h +!test/stub/version.h diff --git a/test/Makefile b/test/Makefile index 550cea3b..f23afd08 100644 --- a/test/Makefile +++ b/test/Makefile @@ -12,9 +12,10 @@ CC = gcc SRCROOT = .. # -include shim first so __xdata/__code/__banked vanish before any firmware header. CFLAGS = -std=gnu11 -g -O1 -Wall -Wextra -Wno-unused-parameter -Wno-unknown-pragmas \ + -Wno-pointer-sign \ -DRTLP_HOST_TEST \ -fsanitize=address,undefined -fno-omit-frame-pointer \ - -include sdcc_shim.h -I. -I$(SRCROOT) -I$(SRCROOT)/httpd -I$(SRCROOT)/uip + -include sdcc_shim.h -Istub -I. -I$(SRCROOT) -I$(SRCROOT)/httpd -I$(SRCROOT)/uip ifeq ($(findstring clang,$(shell $(CC) --version 2>/dev/null)),) CFLAGS += -Wno-builtin-declaration-mismatch endif @@ -23,7 +24,13 @@ LDFLAGS = -fsanitize=address,undefined BUILDDIR = build # One binary per subsystem under test. -TESTS = $(BUILDDIR)/test_cmd_editor +TESTS = $(BUILDDIR)/test_cmd_editor \ + $(BUILDDIR)/test_port_tables + +# The table tests link the register mock and one environment file; stub/ +# stands in for the two headers a firmware build generates. +TABLE_ENV = support.c hw_mock.c env_tables.c $(SRCROOT)/cmd_editor.c \ + $(SRCROOT)/rtl837x_port.c $(SRCROOT)/httpd/page_impl.c all: run @@ -34,6 +41,10 @@ $(BUILDDIR): $(BUILDDIR)/test_cmd_editor: test_cmd_editor.c support.c $(SRCROOT)/cmd_editor.c | $(BUILDDIR) $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) +# rtl837x_port.c and httpd/page_impl.c are compiled UNMODIFIED from the firmware tree. +$(BUILDDIR)/test_port_tables: test_port_tables.c $(TABLE_ENV) hw_mock.h | $(BUILDDIR) + $(CC) $(CFLAGS) $(filter %.c,$^) -o $@ $(LDFLAGS) + run: $(TESTS) @fail=0; for t in $(TESTS); do \ echo "=== $$t ==="; \ diff --git a/test/README.md b/test/README.md index 8c6df84e..4f19c22f 100644 --- a/test/README.md +++ b/test/README.md @@ -19,6 +19,16 @@ fires — so it drops straight into CI. - **`sdcc_shim.h`** (force-included) erases SDCC 8051 keywords (`__xdata`, `__code`, `__banked`, …) so firmware sources compile under host gcc. It changes no logic — the code under test is byte-for-byte the firmware source. +- **`hw_mock.c` / `hw_mock.h`** simulate the ASIC edge: a flat register file + behind `reg_read`/`reg_write` and the `SFR_DATA_*` registers, with a table + engine that performs a VLAN or L2 operation the moment `TBL_CTRL` is written, + fetches a MIB counter on `STAT_GET` and drops entries on `L2_TBL_FLUSH_CTRL`. + Nothing ever reports busy, so a polling loop runs once. The header states the + VLAN and L2 entry layouts independently of the firmware, so a test can hold + what `rtl837x_port.c` writes against an independent statement of the layout. +- **`env_tables.c`** carries the globals and leaf calls those two modules link + against; **`stub/`** stands in for the two headers only a firmware build + generates, so the harness needs no SDCC build first. - **`support.c` / `support.h`** mock the hardware edges: the 16-byte serial ring (`sbuf`), the command/history buffers, and the character-output sink (`write_char` etc.). Buffers are sized **exactly** as on target, so ASan @@ -35,6 +45,7 @@ fires — so it drops straight into CI. | Test binary | TU under test | Findings exercised | |-------------|---------------|--------------------| | `test_cmd_editor` | `cmd_editor.c` | **C4** — full-line hang + `cmd_buffer` 1-byte overflow; basic entry & backspace regressions | +| `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 | ## Adding a test for another module 1. Write `test_.c` with `main()` driving the module's entry points and diff --git a/test/env_tables.c b/test/env_tables.c new file mode 100644 index 00000000..4d31fb65 --- /dev/null +++ b/test/env_tables.c @@ -0,0 +1,123 @@ +/* + * env_tables.c - what rtl837x_port.c and httpd/page_impl.c link against + * besides the register mock: the console, the buffers page_impl writes into, + * the machine description and leaf calls into subsystems not under test. + * Buffers keep their firmware sizes so AddressSanitizer sees the same bounds + * the 8051 has. + */ +#include +#include + +#include "rtl837x_common.h" +#include "rtl837x_port.h" +#include "machine.h" +#include "uip.h" +#include "rtl837x_flash.h" +#include "syslog.h" +#include "rtl837x_phy.h" +#include "support.h" + +/* ---- console: everything lands in out_buf (support.c) ---- */ +void print_byte(uint8_t v) +{ + static const char h[] = "0123456789abcdef"; + write_char(h[(v >> 4) & 0xf]); + write_char(h[v & 0xf]); +} +void print_short(uint16_t v) { print_byte(v >> 8); print_byte(v); } +void print_long(uint32_t v) { print_short(v >> 16); print_short(v); } +void print_reg(uint16_t v) { print_short(v); } +void itoa_short(uint16_t v) +{ + char b[6]; int n = 0; + do { b[n++] = '0' + v % 10; v /= 10; } while (v); + while (n) write_char(b[--n]); +} +void dbg_string(char *p) { (void)p; } +void dbg_short(uint16_t v) { (void)v; } +void dbg_char(char c) { (void)c; } +void dbg_byte(uint8_t v) { (void)v; } + +/* ---- copied verbatim from rtlplayground.c ---- */ +uint16_t strtox(uint8_t *dst, const char *s) +{ + uint8_t *b = dst; + while (*s) + *dst++ = *s++; + *dst = 0; + return dst - b; +} + +const uint16_t bit_mask[16] = { + 0x0001, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040, 0x0080, + 0x0100, 0x0200, 0x0400, 0x0800, 0x1000, 0x2000, 0x4000, 0x8000 +}; +const uint8_t hex_chars[] = "0123456789abcdef"; +uint8_t *hex = (uint8_t *)hex_chars; + +/* ---- a 9-port RTL8373 whose logical ports map to themselves ---- */ +const struct machine machine = { + .machine_name = "HOSTTEST", + .min_port = 0, + .max_port = 8, + .n_sfp = 0, + .log_to_phys_port = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, + .phys_to_log_port = { 0, 1, 2, 3, 4, 5, 6, 7, 8 }, +}; +struct machine_runtime machine_detected = { .isRTL8373 = 1 }; + +/* ---- firmware state the modules read or write ---- */ +uint8_t outbuf[TCP_OUTBUF_SIZE]; +uint16_t slen; +uint16_t management_vlan = 1; +uint16_t cont_len; +uint32_t cont_addr; +uint8_t vlan_names[VLAN_NAMES_SIZE]; +uint16_t vlan_ptr; +uint8_t sfp_pins_last = 0xff; +uint8_t sfp_options[2]; +char sfp_module_vendor[2][17]; +char sfp_module_model[2][17]; +char sfp_module_serial[2][17]; +char hostname[24] = "hosttest"; +char port_names[9][PORT_NAME_SIZE]; +struct flash_region_t flash_region; +struct syslog_state syslog_state; +bool stp_enabled; + +uip_ipaddr_t uip_hostaddr, uip_draddr, uip_netmask; +struct uip_eth_addr uip_ethaddr = { .addr = { 0x02, 0x11, 0x22, 0x33, 0x44, 0x55 } }; + +/* ---- leaf calls into subsystems not under test ---- */ +void flash_read_bulk(uint8_t *dst) { (void)dst; } +char *get_flash_size_str(void) { return (char *)"2M"; } +uint8_t sfp_read_reg(uint8_t slot, uint8_t reg) { (void)slot; (void)reg; return 0; } +bool gpio_pin_test(uint8_t pin) { (void)pin; return false; } +void phy_read(uint8_t phy_id, uint8_t dev_id, uint16_t reg) { (void)phy_id; (void)dev_id; (void)reg; } +void phy_write(uint8_t phy_id, uint8_t dev_id, uint16_t reg, uint16_t val) { (void)phy_id; (void)dev_id; (void)reg; (void)val; } +void phy_reset(uint8_t port) { (void)port; } +uint8_t stp_port_role(uint8_t port) { (void)port; return 0; } +uint8_t stp_port_state(uint8_t port) { (void)port; return 3; } + +/* ---- STP state send_stp() prints; the tree is not under test here ---- */ +#include "rtl837x_stp.h" +struct bridge root_bridge; +struct bridge stp_dbridge[STP_ENTITIES]; +uint16_t stp_bpdu_age[STP_ENTITIES]; +uint16_t stp_dpid[STP_ENTITIES]; +uint16_t stp_lag_mask[STP_LAG_COUNT]; +uint16_t stp_tc_count; +uint32_t root_bridge_cost; +uint32_t stp_dcost[STP_ENTITIES]; +uint32_t stp_pcost[STP_ENTITIES]; +uint8_t stp_ent_of[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; +uint8_t stp_fwddelay_s = 15, stp_hello_s = 2, stp_maxage_s = 20; +uint8_t stp_pflags[STP_ENTITIES]; +uint8_t stp_pp2p[STP_ENTITIES]; +uint8_t stp_pprio[STP_ENTITIES]; +uint8_t stp_prio = 0x80, stp_root_port = 0xff, stp_rstp = 1, stp_txhold = 6; + +/* ---- SFP: no module present, reads fail ---- */ +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]); } diff --git a/test/hw_mock.c b/test/hw_mock.c new file mode 100644 index 00000000..8553884d --- /dev/null +++ b/test/hw_mock.c @@ -0,0 +1,342 @@ +/* + * hw_mock.c - the ASIC edge: SFR storage, register file, table engine. + * + * The shim declares every SFR extern so that including rtl837x_sfr.h from many + * translation units does not define them repeatedly. Undefining the four + * keywords here and re-including the header emits the definitions once, so + * the mock tracks the real register list without a hand-kept copy. + */ +#include +#include +#include +#include + +#undef __sfr +#undef __sfr16 +#undef __sfr32 +#undef __sbit +#define __sfr volatile uint8_t +#define __sfr16 volatile uint16_t +#define __sfr32 volatile uint32_t +#define __sbit bool +#include "rtl837x_sfr.h" +#undef __sfr +#undef __sfr16 +#undef __sfr32 +#undef __sbit +#define __sfr extern volatile uint8_t +#define __sfr16 extern volatile uint16_t +#define __sfr32 extern volatile uint32_t +#define __sbit extern bool + +#include "rtl837x_common.h" +#include "rtl837x_regs.h" +#include "hw_mock.h" + +uint8_t sfr_data[4]; +unsigned long hw_reads, hw_writes; + +static uint32_t regfile[0x10000]; +static uint32_t vlan_tbl[4096]; +static struct hw_l2 l2[HW_L2_MAX]; +static uint64_t counters[16][64]; + +void hw_reset(void) +{ + memset(regfile, 0, sizeof(regfile)); + memset(vlan_tbl, 0, sizeof(vlan_tbl)); + memset(l2, 0, sizeof(l2)); + memset(counters, 0, sizeof(counters)); + memset(sfr_data, 0, sizeof(sfr_data)); + hw_reads = hw_writes = 0; +} + +void hw_reg_set(uint16_t addr, uint32_t v) { regfile[addr] = v; } +uint32_t hw_reg_get(uint16_t addr) { return regfile[addr]; } +uint32_t hw_vlan_word(uint16_t vid) { return vlan_tbl[vid & 0xfff]; } +void hw_counter_set(uint8_t port, uint8_t counter, uint64_t value) { counters[port & 0xf][counter & 0x3f] = value; } + +/* ---- L2 table -------------------------------------------------------- */ + +static int l2_cmp(const void *x, const void *y) +{ + const struct hw_l2 *a = x, *b = y; + if (a->present != b->present) + return a->present ? -1 : 1; + return (int)a->idx - (int)b->idx; +} + +static void l2_sort(void) { qsort(l2, HW_L2_MAX, sizeof(l2[0]), l2_cmp); } + +int hw_l2_count(void) +{ + int n = 0; + for (int i = 0; i < HW_L2_MAX; i++) + n += l2[i].present; + return n; +} + +const struct hw_l2 *hw_l2_at(int n) +{ + l2_sort(); + return (n >= 0 && n < hw_l2_count()) ? &l2[n] : 0; +} + +static void l2_key(const struct hw_l2 *e, uint8_t mac[6], uint16_t *vid) +{ + mac[0] = e->b >> 8; mac[1] = e->b; + mac[2] = e->a >> 24; mac[3] = e->a >> 16; mac[4] = e->a >> 8; mac[5] = e->a; + *vid = ((e->b >> 16) & 0xfff); +} + +const struct hw_l2 *hw_l2_find(const uint8_t mac[6], uint16_t vid) +{ + for (int i = 0; i < HW_L2_MAX; i++) { + uint8_t m[6]; uint16_t v; + if (!l2[i].present) + continue; + l2_key(&l2[i], m, &v); + if (v == vid && !memcmp(m, mac, 6)) + return &l2[i]; + } + return 0; +} + +static struct hw_l2 *l2_free(void) +{ + for (int i = 0; i < HW_L2_MAX; i++) + if (!l2[i].present) + return &l2[i]; + return 0; +} + +static uint16_t l2_hash(const uint8_t mac[6], uint16_t vid) +{ + uint16_t h = vid; + for (int i = 0; i < 6; i++) + h = (uint16_t)(h * 31 + mac[i]); + return h & 0xfff; +} + +void hw_l2_put(uint16_t idx, const uint8_t mac[6], uint16_t vid, uint16_t port_or_pmask, bool is_static) +{ + struct hw_l2 *e = (struct hw_l2 *)hw_l2_find(mac, vid); + bool mc = mac[0] & 1; + if (!e) + e = l2_free(); + e->present = true; + e->idx = idx & 0xfff; + e->a = ((uint32_t)mac[2] << 24) | ((uint32_t)mac[3] << 16) | ((uint32_t)mac[4] << 8) | mac[5]; + e->b = (0x20u << 24) | ((uint32_t)(vid & 0xfff) << 16) | ((uint32_t)mac[0] << 8) | mac[1]; + if (mc) { + e->b |= (uint32_t)(port_or_pmask & 0x3) << 30; + e->c = (port_or_pmask >> 2) & 0xff; + } else { + e->b |= (uint32_t)(port_or_pmask & 0x3) << 30; + e->c = (port_or_pmask >> 2) & 0x3; + } + if (is_static) + e->c |= 1u << 16; +} + +void hw_l2_decode(const struct hw_l2 *e, uint8_t mac[6], uint16_t *vid, uint16_t *port_or_pmask, + bool *valid, bool *is_static, bool *mc) +{ + l2_key(e, mac, vid); + *valid = (e->b >> 29) & 1; + *is_static = (e->c >> 16) & 1; + *mc = mac[0] & 1; + if (*mc) + *port_or_pmask = ((e->b >> 30) & 0x3) | ((e->c & 0xff) << 2); + else + *port_or_pmask = ((e->b >> 30) & 0x3) | ((e->c & 0x3) << 2); +} + +/* ---- the engine ------------------------------------------------------ */ + +static void l2_out(const struct hw_l2 *e, uint16_t idx) +{ + regfile[RTL837x_L2_DATA_OUT_A] = e ? e->a : 0; + regfile[RTL837x_L2_DATA_OUT_B] = e ? e->b : 0; + regfile[RTL837x_L2_DATA_OUT_C] = e ? e->c : 0; + regfile[RTL837x_TBL_DATA_0] = (regfile[RTL837x_TBL_DATA_0] & ~0x1fffu) | (e ? 0x1000 : 0) | (idx & 0xfff); +} + +static void table_exec(uint32_t ctrl) +{ + uint16_t idx = ((ctrl >> 16) & 0xfff); + uint8_t type = ctrl >> 8; + bool write = ctrl & TBL_WRITE; + + if (type == TBL_VLAN) { + if (write) + vlan_tbl[idx] = regfile[RTL837x_TBL_DATA_IN_A]; + else + regfile[RTL837x_L2_DATA_OUT_A] = vlan_tbl[idx]; + return; + } + if (type != TBL_L2_UNICAST) + return; + + uint32_t d0 = regfile[RTL837x_TBL_DATA_0]; + uint8_t method = (d0 >> 14) & 0x3; + bool clear = (d0 >> 18) & 1; + + if (method == TBL_LUTREAD_MAC) { + struct hw_l2 key = { .present = true, .a = regfile[RTL837x_TBL_DATA_IN_A], + .b = regfile[RTL837x_TBL_DATA_IN_B], .c = regfile[RTL837x_TBL_DATA_IN_C] }; + uint8_t mac[6]; uint16_t vid; + l2_key(&key, mac, &vid); + struct hw_l2 *e = (struct hw_l2 *)hw_l2_find(mac, vid); + if (write) { + if (clear) { + if (e) + e->present = false; + l2_out(0, 0); + return; + } + if (!e) { + e = l2_free(); + e->idx = l2_hash(mac, vid); + } + e->present = true; + e->a = key.a; e->b = key.b; e->c = key.c; + l2_out(e, e->idx); + return; + } + l2_out(e, e ? e->idx : 0); + return; + } + + /* next-entry reads: the first present entry at or after idx, wrapping */ + l2_sort(); + int n = hw_l2_count(); + struct hw_l2 *e = 0; + for (int i = 0; i < n; i++) + if (l2[i].idx >= idx) { e = &l2[i]; break; } + if (!e && n) + e = &l2[0]; + if (write) { + /* a write by index replaces the entry that sits there */ + for (int i = 0; i < n; i++) + if (l2[i].idx == idx) { e = &l2[i]; break; } + if (!e) + e = l2_free(); + e->present = (regfile[RTL837x_TBL_DATA_IN_B] >> 29) & 1; + e->idx = idx; + e->a = regfile[RTL837x_TBL_DATA_IN_A]; + e->b = regfile[RTL837x_TBL_DATA_IN_B]; + e->c = regfile[RTL837x_TBL_DATA_IN_C]; + return; + } + l2_out(e, e ? e->idx : idx); +} + +static void flush_exec(uint32_t ctrl) +{ + uint16_t mask = ctrl & 0x3ff; + bool dynamic_only = regfile[RTL837x_L2_TBL_FLUSH_CNF] == 0; + for (int i = 0; i < HW_L2_MAX; i++) { + uint8_t mac[6]; uint16_t vid, p; bool valid, st, mc; + if (!l2[i].present) + continue; + hw_l2_decode(&l2[i], mac, &vid, &p, &valid, &st, &mc); + if (mc || (st && dynamic_only)) + continue; + if ((mask >> p) & 1) + l2[i].present = false; + } +} + +static void stat_exec(uint32_t v) +{ + uint8_t cnt = (uint8_t)(((v >> 8) & 0xff) << 3) | ((v >> 5) & 0x7); + uint8_t port = (v >> 1) & 0xf; + uint64_t c = counters[port][cnt & 0x3f]; + regfile[RTL837X_STAT_V_HIGH] = (uint32_t)(c >> 32); + regfile[RTL837X_STAT_V_LOW] = (uint32_t)c; +} + +static void reg_store(uint16_t addr, uint32_t v) +{ + hw_writes++; + regfile[addr] = v; + switch (addr) { + case RTL837X_TBL_CTRL: + if (v & TBL_EXECUTE) { + table_exec(v); + regfile[addr] = v & ~(uint32_t)TBL_EXECUTE; + } + break; + case RTL837x_L2_TBL_FLUSH_CTRL: + if (v & L2_TBL_FLUSH_EXEC) { + flush_exec(v); + regfile[addr] = v & ~(uint32_t)L2_TBL_FLUSH_EXEC; + } + break; + case RTL837X_STAT_GET: + if (v & 1) { + stat_exec(v); + regfile[addr] = v & ~1u; + } + break; + } +} + +static uint32_t sfr_word(void) +{ + return ((uint32_t)SFR_DATA_24 << 24) | ((uint32_t)SFR_DATA_16 << 16) | ((uint32_t)SFR_DATA_8 << 8) | SFR_DATA_0; +} + +static void sfr_load(uint32_t v) +{ + SFR_DATA_24 = v >> 24; SFR_DATA_16 = v >> 16; SFR_DATA_8 = v >> 8; SFR_DATA_0 = v; +} + +void reg_read(uint16_t addr) +{ + hw_reads++; + sfr_load(regfile[addr]); +} + +void reg_read_m(uint16_t addr) +{ + reg_read(addr); + sfr_data[0] = SFR_DATA_24; sfr_data[1] = SFR_DATA_16; sfr_data[2] = SFR_DATA_8; sfr_data[3] = SFR_DATA_0; +} + +void reg_write(uint16_t addr) +{ + reg_store(addr, sfr_word()); +} + +void reg_write_m(uint16_t addr) +{ + sfr_load(((uint32_t)sfr_data[0] << 24) | ((uint32_t)sfr_data[1] << 16) | ((uint32_t)sfr_data[2] << 8) | sfr_data[3]); + reg_write(addr); +} + +void reg_bit_set(uint16_t addr, char bit) +{ + reg_read_m(addr); + sfr_data[3 - (bit >> 3)] |= 1 << (bit & 7); + reg_write_m(addr); +} + +void reg_bit_clear(uint16_t addr, char bit) +{ + reg_read_m(addr); + sfr_data[3 - (bit >> 3)] &= ~(1 << (bit & 7)); + reg_write_m(addr); +} + +uint8_t reg_bit_test(uint16_t addr, char bit) +{ + reg_read_m(addr); + return (sfr_data[3 - (bit >> 3)] >> (bit & 7)) & 1; +} + +void sfr_mask_data(uint8_t n, uint8_t mask, uint8_t set) +{ + sfr_data[3 - n] = (sfr_data[3 - n] & ~mask) | set; +} diff --git a/test/hw_mock.h b/test/hw_mock.h new file mode 100644 index 00000000..dc7cd05e --- /dev/null +++ b/test/hw_mock.h @@ -0,0 +1,59 @@ +/* + * hw_mock.h - a simulated RTL837x register file and table engine. + * + * The firmware reaches the ASIC only through reg_read/reg_read_m/reg_write/ + * reg_write_m and the SFR_DATA_* registers, so mocking that edge is enough to + * run the table code on the host. Every register is a 32-bit word in a flat + * file; a few addresses are live: writing TBL_CTRL with EXECUTE performs the + * table operation at once and clears the bit, STAT_GET fetches a counter, + * L2_TBL_FLUSH_CTRL drops entries. Nothing here ever reports busy, so a + * polling loop in the firmware runs exactly once. + * + * The VLAN and L2 entry layouts mirror what rtl837x_port.c writes and what + * httpd/page_impl.c reads; the decoders below are an independent statement + * of that layout, so a test can compare the firmware's two ends against it. + */ +#ifndef TEST_HW_MOCK_H +#define TEST_HW_MOCK_H + +#include +#include + +void hw_reset(void); /* every register 0, tables empty */ +void hw_reg_set(uint16_t addr, uint32_t v); +uint32_t hw_reg_get(uint16_t addr); + +extern uint8_t sfr_data[4]; /* the firmware's register scratch, owned here */ +extern unsigned long hw_reads; /* reg_read + reg_read_m calls since hw_reset */ +extern unsigned long hw_writes; /* reg_write + reg_write_m calls since hw_reset */ + +/* VLAN table: the 32-bit word the firmware wrote for that VID, 0 = never written */ +uint32_t hw_vlan_word(uint16_t vid); + +/* L2 table: entries as the three data words the firmware exchanges with the + * engine. Word B: bit 29 valid, bits 31:30 the low two bits of the port (or + * of the member mask), bits 27:16 VID, bits 15:0 the first two MAC octets. + * Word A: the last four MAC octets. Word C: bit 16 static, bits 1:0 the high + * two bits of the port; for a multicast MAC bits 7:0 are the member mask + * shifted right by two. */ +#define HW_L2_MAX 64 +struct hw_l2 { + bool present; + uint16_t idx; + uint32_t a, b, c; /* DATA_IN_A/B/C on write, L2_DATA_OUT_A/B/C on read */ +}; + +/* Pointers returned below stay valid until the next table operation. */ +int hw_l2_count(void); +const struct hw_l2 *hw_l2_at(int n); /* n-th present entry in idx order */ +const struct hw_l2 *hw_l2_find(const uint8_t mac[6], uint16_t vid); +/* Put an entry the way the switch would hold it: a learned unicast entry on + * one port, or a static one. mc entries use pmask instead of a port. */ +void hw_l2_put(uint16_t idx, const uint8_t mac[6], uint16_t vid, uint16_t port_or_pmask, bool is_static); +void hw_l2_decode(const struct hw_l2 *e, uint8_t mac[6], uint16_t *vid, uint16_t *port_or_pmask, + bool *valid, bool *is_static, bool *mc); + +/* MIB counter the next STAT_GET for (port, counter) returns */ +void hw_counter_set(uint8_t port, uint8_t counter, uint64_t value); + +#endif /* TEST_HW_MOCK_H */ diff --git a/test/stub/html_data.h b/test/stub/html_data.h new file mode 100644 index 00000000..33f7fec3 --- /dev/null +++ b/test/stub/html_data.h @@ -0,0 +1,9 @@ +/* Stand-in for the generated html_data.h: the table generators under test + * never touch the embedded file list, and the real header exists only after + * a firmware build, which the host harness must not depend on. */ +#ifndef FDATA_DEFS_H +#define FDATA_DEFS_H +#include +typedef enum mime_type_e { mime_HTML = 0, mime_SVG, mime_ICO, mime_PNG, mime_JS, mime_CSS, mime_TXT } mime_type_t; +struct f_data { __code char *file; uint32_t start; uint16_t len; mime_type_t mime; uint8_t gzip; }; +#endif diff --git a/test/stub/version.h b/test/stub/version.h new file mode 100644 index 00000000..4e1b1c8d --- /dev/null +++ b/test/stub/version.h @@ -0,0 +1,5 @@ +#ifndef VERSION_H +#define VERSION_H +#define VERSION_SW "host-test" +#define BUILD_DATE "1970-01-01 00:00:00" +#endif diff --git a/test/test_port_tables.c b/test/test_port_tables.c new file mode 100644 index 00000000..d8d91cfd --- /dev/null +++ b/test/test_port_tables.c @@ -0,0 +1,181 @@ +/* + * test_port_tables.c - rtl837x_port.c against the simulated table engine. + * + * What the firmware writes to the VLAN and L2 tables is checked against an + * independent statement of the entry layout (hw_mock.h), and what it reads + * back is checked against what it wrote. The module is compiled unmodified. + */ +#include +#include +#include + +#include "rtl837x_common.h" +#include "rtl837x_regs.h" +#include "rtl837x_port.h" +#include "machine.h" +#include "support.h" +#include "hw_mock.h" + +extern struct machine_runtime machine_detected; + +/* VLAN entry as vlan_create() lays it out: bit 25 valid, bits 0-9 members, + * bits 10-19 the untag set, which is every port not in the tagged set (the + * bit of a non-member is meaningless to the switch and stays set). */ +static uint32_t vlan_word(uint16_t members, uint16_t tagged) +{ + return (1u << 25) | ((uint32_t)(~tagged & 0x3ff) << 10) | members; +} + +static void t_vlan_roundtrip(void) +{ + printf("[test] VLAN entry: create, read back, delete\n"); + hw_reset(); out_reset(); + vlan_settings.vlan = 10; + vlan_settings.members = 0x0015; /* ports 1, 3, 5 */ + vlan_settings.tagged = 0x0004; /* port 3 tagged */ + vlan_create(); + /* the CPU port is always a tagged member */ + CHECK(hw_vlan_word(10) == vlan_word(0x0015 | 0x0200, 0x0004 | 0x0200), "vlan 10 entry has the documented layout"); + CHECK(hw_vlan_word(11) == 0, "the neighbouring entry is untouched"); + + uint32_t w = hw_vlan_word(10); + CHECK(vlan_get(10) == 0, "vlan_get finds it"); + CHECK(((uint32_t)sfr_data[0] << 24 | (uint32_t)sfr_data[1] << 16 | (uint32_t)sfr_data[2] << 8 | sfr_data[3]) == w, + "and returns the word that was written"); + CHECK(sfr_data[0] & 0x02, "with the valid bit the JSON generators test for"); + + unsigned long r = hw_reads; + CHECK(vlan_get(4095) == -1, "VLAN 4095 is refused"); + CHECK(hw_reads == r, "without touching the engine"); + + vlan_delete(10); + CHECK(hw_vlan_word(10) == 0, "vlan_delete clears the entry"); + vlan_delete(0); + vlan_delete(4095); + CHECK(hw_vlan_word(0) == 0 && hw_vlan_word(4095) == 0, "VLAN 0 and 4095 cannot be deleted"); +} + +static void t_vlan_rtl8372(void) +{ + printf("[test] VLAN entry on an RTL8372: ports 0-2 are not there\n"); + hw_reset(); + machine_detected.isRTL8373 = 0; + vlan_settings.vlan = 20; + vlan_settings.members = 0x03ff; + vlan_settings.tagged = 0x0000; + vlan_create(); + uint32_t w = hw_vlan_word(20); + CHECK(((w >> 10) & 0x7) == 0, "the untagged bits of ports 0-2 stay clear"); + CHECK((w & 0x3ff) == 0x03ff, "the member bits are what was asked for"); + machine_detected.isRTL8373 = 1; +} + +static void t_pvid(void) +{ + printf("[test] PVID: two ports share one register\n"); + hw_reset(); + port_pvid_set(2, 100); + port_pvid_set(3, 4094); + CHECK(port_pvid_get(2) == 100, "even port reads back"); + CHECK(port_pvid_get(3) == 4094, "odd port reads back"); + port_pvid_set(3, 7); + CHECK(port_pvid_get(2) == 100, "writing the odd port leaves the even one alone"); + CHECK(port_pvid_get(3) == 7, "and takes the new value"); + port_pvid_set(2, 4001); + CHECK(port_pvid_get(3) == 7, "writing the even port leaves the odd one alone"); + CHECK(port_pvid_get(8) == 0, "an untouched port reads 0"); +} + +static void t_l2mc(void) +{ + printf("[test] static multicast entry steering a group to the CPU\n"); + hw_reset(); + port_l2mc_set(0x0e, 2, PMASK_CPU); + const uint8_t mac[6] = { 0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e }; + const struct hw_l2 *e = hw_l2_find(mac, 2); + CHECK(e != 0, "the entry exists for 01:80:c2:00:00:0e in VLAN 2"); + if (e) { + uint8_t m[6]; uint16_t vid, pm; bool valid, st, mc; + hw_l2_decode(e, m, &vid, &pm, &valid, &st, &mc); + CHECK(valid, "valid"); + CHECK(mc, "multicast"); + CHECK(pm == PMASK_CPU, "member mask is the CPU port alone"); + } + port_l2mc_set(0x0e, 2, PMASK_CPU | 0x0003); + e = hw_l2_find(mac, 2); + CHECK(hw_l2_count() == 1, "writing the same key again replaces, not adds"); + if (e) { + uint8_t m[6]; uint16_t vid, pm; bool valid, st, mc; + hw_l2_decode(e, m, &vid, &pm, &valid, &st, &mc); + CHECK(pm == (PMASK_CPU | 0x0003), "with the new mask, low bits included"); + } +} + +static void t_static_mgmt(void) +{ + printf("[test] the management MAC pinned to the CPU port\n"); + hw_reset(); + uint8_t mac[6] = { 0x02, 0x11, 0x22, 0x33, 0x44, 0x55 }; + port_l2_static_mgmt(mac, 2, false); + const struct hw_l2 *e = hw_l2_find(mac, 2); + CHECK(e != 0, "entry present in VLAN 2"); + if (e) { + uint8_t m[6]; uint16_t vid, p; bool valid, st, mc; + hw_l2_decode(e, m, &vid, &p, &valid, &st, &mc); + CHECK(valid && st && !mc, "valid, static, unicast"); + CHECK(p == CPU_PORT, "on the CPU port"); + } + port_l2_static_mgmt(mac, 0, false); + CHECK(hw_l2_find(mac, 1) != 0, "management VLAN 0 means VLAN 1"); + port_l2_static_mgmt(mac, 2, true); + CHECK(hw_l2_find(mac, 2) == 0, "removal deletes the VLAN 2 entry"); + CHECK(hw_l2_find(mac, 1) != 0, "and leaves the other one"); + port_l2_static_mgmt(mac, 3, true); + CHECK(hw_l2_count() == 1, "removing an entry that is not there changes nothing"); +} + +static void t_flush(void) +{ + printf("[test] flushing a port forgets its learned entries only\n"); + hw_reset(); + const uint8_t a[6] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x01 }; + const uint8_t b[6] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x02 }; + const uint8_t c[6] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x03 }; + hw_l2_put(0x010, a, 1, 4, false); + hw_l2_put(0x020, b, 1, 4, true); + hw_l2_put(0x030, c, 1, 5, false); + port_l2_forget_port(4); + CHECK(hw_l2_find(a, 1) == 0, "learned entry on port 5 (index 4) gone"); + CHECK(hw_l2_find(b, 1) != 0, "static entry on the same port stays"); + CHECK(hw_l2_find(c, 1) != 0, "entry on another port stays"); + port_l2_forget(); + CHECK(hw_l2_find(c, 1) == 0 && hw_l2_find(b, 1) != 0, "forget-all drops every learned entry and keeps the static one"); +} + +static void t_lag(void) +{ + printf("[test] trunk membership and its hash seed\n"); + hw_reset(); + port_lag_members_set(1, 0x0180); + CHECK(port_lag_members_get(1) == 0x0180, "members read back"); + CHECK(port_lag_of(7) == 1 && port_lag_of(8) == 1, "both ports belong to group 2"); + CHECK(port_lag_of(0) == PORT_LAG_NONE, "a port outside is in no group"); + CHECK((hw_reg_get(RTL837X_TRK_HASH_CTRL_BASE + 4) & 0xff) == LAG_HASH_DEFAULT, "an unset hash gets the default"); + hw_reg_set(RTL837X_TRK_HASH_CTRL_BASE + 8, 0x06); + port_lag_members_set(2, 0x0060); + CHECK((hw_reg_get(RTL837X_TRK_HASH_CTRL_BASE + 8) & 0xff) == 0x06, "a hash already set is kept"); +} + +int main(void) +{ + printf("== rtl837x_port.c table tests ==\n"); + t_vlan_roundtrip(); + t_vlan_rtl8372(); + t_pvid(); + t_l2mc(); + t_static_mgmt(); + t_flush(); + t_lag(); + printf("\n%d checks, %d failed\n", tests_run, tests_failed); + return tests_failed ? 1 : 0; +}