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; +}