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/httpd/page_impl.c b/httpd/page_impl.c index ec7ef26a..cb11075c 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -175,6 +175,20 @@ void reg_to_html_long(uint16_t reg) } +/* The 64-bit counter of the last STAT_GET as one number: the low word keeps + * its leading zeros whenever the high word is set. */ +void counter_to_html(void) +{ + reg_read_m(RTL837X_STAT_V_HIGH); + if (SFR_DATA_24 | SFR_DATA_16 | SFR_DATA_8 | SFR_DATA_0) { + sfr_data_to_html(); + reg_to_html_long(RTL837X_STAT_V_LOW); + } else { + reg_to_html(RTL837X_STAT_V_LOW); + } +} + + void send_sfp_info(uint8_t sfp) { // This loops over the Vendor-name, Vendor OUI, Vendor PN and Vendor rev ASCII fields @@ -311,8 +325,7 @@ bool send_counters(uint8_t phys_port) for (uint8_t counter = 0; counter < 0x37; counter++) { STAT_GET(counter, log_port); slen += strtox(outbuf + slen, "\"0x"); - reg_to_html(RTL837X_STAT_V_HIGH); - reg_to_html_long(RTL837X_STAT_V_LOW); + counter_to_html(); char_to_html('\"'); if (counter != 0x36) char_to_html(','); @@ -432,8 +445,6 @@ void l2_delete(uint16_t idx) slen = strtox(outbuf, HTTP_RESPONCE_JSON); dbg_string("L2 DELETE\n"); dbg_short(idx); - __xdata uint8_t entries_left = L2_MAX_TRANSFER; - do { reg_read(RTL837X_TBL_CTRL); } while (SFR_DATA_0 & TBL_EXECUTE); @@ -914,8 +925,7 @@ void send_status(void) STAT_GET(STAT_COUNTER_TX_PKTS, i); slen += strtox(outbuf + slen, ",\"txG\":\"0x"); - reg_to_html(RTL837X_STAT_V_HIGH); - reg_to_html(RTL837X_STAT_V_LOW); + counter_to_html(); slen += strtox(outbuf + slen, "\",\"txB\":\"0x"); STAT_GET(STAT_COUNTER_ERR_PKTS, i); @@ -923,8 +933,7 @@ void send_status(void) slen += strtox(outbuf + slen, "\",\"rxG\":\"0x"); STAT_GET(STAT_COUNTER_RX_PKTS, i); - reg_to_html(RTL837X_STAT_V_HIGH); - reg_to_html(RTL837X_STAT_V_LOW); + counter_to_html(); slen += strtox(outbuf + slen, "\",\"rxB\":\"0x"); STAT_GET(STAT_COUNTER_ERR_PKTS, i); diff --git a/rtl837x_port.c b/rtl837x_port.c index a640fb5d..8ee105c6 100644 --- a/rtl837x_port.c +++ b/rtl837x_port.c @@ -102,9 +102,9 @@ static void port_pvid_write(uint8_t port, __xdata uint16_t pvid) reg_read_m(reg); if (port & 0x1) { - REG_WRITE(reg, sfr_data[0], pvid >> 4, sfr_data[2] & 0x0f | (pvid << 4), sfr_data[3]); + REG_WRITE(reg, sfr_data[0], pvid >> 4, (sfr_data[2] & 0x0f) | (pvid << 4), sfr_data[3]); } else { - REG_WRITE(reg, sfr_data[0], sfr_data[1], sfr_data[2] & 0xf0 | (pvid >> 8), pvid); + REG_WRITE(reg, sfr_data[0], sfr_data[1], (sfr_data[2] & 0xf0) | (pvid >> 8), pvid); } } @@ -285,7 +285,7 @@ void vlan_setup(void) __banked #endif reg_read_m(reg); if (i & 0x1) { - REG_WRITE(reg, sfr_data[0], 0, sfr_data[2] & 0x0f | 0x10, sfr_data[3]); + REG_WRITE(reg, sfr_data[0], 0, (sfr_data[2] & 0x0f) | 0x10, sfr_data[3]); } else { REG_WRITE(reg, sfr_data[0], sfr_data[1], sfr_data[2] & 0xf0, 0x01); } @@ -758,7 +758,7 @@ void port_eee_enable_all(__xdata uint8_t speed) __banked port_eee_enable(i, speed); } else { if (speed & EEE_10G) - port_eee_enable(i, speed & EEE_NORESET | EEE_2G5); + port_eee_enable(i, (speed & EEE_NORESET) | EEE_2G5); else port_eee_enable(i, speed); } @@ -899,7 +899,7 @@ void vlan_dump(void) __banked /** Set the ingress VLAN filtering */ bool port_ingress_vlan_filter_set(uint8_t port, __xdata bool enabled) __banked { - if (port < machine.min_port || port > machine.max_port && port != CPU_PORT) { + if (port < machine.min_port || (port > machine.max_port && port != CPU_PORT)) { return false; } if (enabled) @@ -913,7 +913,7 @@ bool port_ingress_vlan_filter_set(uint8_t port, __xdata bool enabled) __banked /** Get the ingress VLAN filtering status */ bool port_ingress_vlan_filter_get(uint8_t port) __banked { - if (port < machine.min_port || port > machine.max_port && port != CPU_PORT) { + if (port < machine.min_port || (port > machine.max_port && port != CPU_PORT)) { return false; } diff --git a/test/Makefile b/test/Makefile index 550cea3b..1740bbb8 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,14 @@ 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 \ + $(BUILDDIR)/test_page_json + +# 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 +42,13 @@ $(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) + +$(BUILDDIR)/test_page_json: test_page_json.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..cd03b247 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 what `page_impl.c` reads. +- **`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,8 @@ 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 | +| `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` | ## 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/sdcc_shim.h b/test/sdcc_shim.h index c080ff44..a1dfa848 100644 --- a/test/sdcc_shim.h +++ b/test/sdcc_shim.h @@ -12,6 +12,7 @@ #define SDCC_SHIM_H #include +#include /* Memory-space qualifiers → nothing (host has one flat address space). */ #define __xdata @@ -22,12 +23,27 @@ /* bool, not unsigned char: SDCC's bit types normalize any assigned value * to 0/1, which _Bool reproduces exactly. */ #define __bit bool -#define __sbit bool -/* NOTE: the firmware ships its own 8051 memcpy/memset/strlen/strcpy/sleep with - * incompatible signatures (rtl837x_common.h). Host TUs must therefore build with - * -fno-builtin (so gcc doesn't auto-declare the builtins) and must NOT include - * /. Test code uses the t_* helpers in support.h instead. */ +/* Special-function registers are declared in rtl837x_sfr.h as + * `__sfr __at(0xNN) NAME;`. With __at erased those become plain declarations, + * so they must carry extern or every translation unit including the header + * defines its own copy. __sbit is the bit-in-an-SFR form and only appears + * there; the local bit variable keyword is __bit above. test/hw_mock.c + * undefines these four and re-includes the header once to emit the storage. */ +#define __sfr extern volatile uint8_t +#define __sfr16 extern volatile uint16_t +#define __sfr32 extern volatile uint32_t +#define __sbit extern bool + +/* The firmware ships its own memcpy/memset/strlen/strcpy/sleep, and + * rtl837x_common.h hides those prototypes under RTLP_HOST_TEST so host builds + * take the C library versions. Something has to declare them, and firmware + * sources do call memset and memcpy; this header is force-included ahead of + * everything, so pulling in here puts the libc versions in scope + * before any firmware header and nothing clashes. sleep() is not in + * and is declared on its own rather than through . */ +#include +extern unsigned int sleep(unsigned int seconds); /* Function attributes → nothing (no banking / no 8051 calling conventions). */ #define __banked 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_page_json.c b/test/test_page_json.c new file mode 100644 index 00000000..e650a538 --- /dev/null +++ b/test/test_page_json.c @@ -0,0 +1,216 @@ +/* + * test_page_json.c - the JSON generators of httpd/page_impl.c, fed by the + * table code of rtl837x_port.c over the simulated engine. Both modules are + * compiled unmodified, so what the web page shows is checked against what + * the CLI configured, through the same bytes the switch would hold. + */ +#include +#include +#include +#include + +#include "rtl837x_common.h" +#include "rtl837x_regs.h" +#include "rtl837x_port.h" +#include "machine.h" +#include "page_impl.h" +#include "support.h" +#include "hw_mock.h" + +extern uint8_t outbuf[TCP_OUTBUF_SIZE]; +extern uint16_t slen; +extern uint8_t vlan_names[VLAN_NAMES_SIZE]; +extern uint16_t vlan_ptr; +extern uint16_t management_vlan; + +/* The response body: what follows the blank line after the header. */ +static const char *body(void) +{ + outbuf[slen < TCP_OUTBUF_SIZE ? slen : TCP_OUTBUF_SIZE - 1] = 0; + const char *p = strstr((const char *)outbuf, "\r\n\r\n"); + return p ? p + 4 : (const char *)outbuf; +} + +static int has(const char *needle) { return strstr(body(), needle) != 0; } + +static int count(const char *needle) +{ + int n = 0; + for (const char *p = body(); (p = strstr(p, needle)); p += strlen(needle)) + n++; + return n; +} + +/* Brackets and braces balance outside strings, quotes pair up. */ +static int balanced(void) +{ + int depth = 0, instr = 0; + for (const char *p = body(); *p; p++) { + if (*p == '"') { instr = !instr; continue; } + if (instr) continue; + if (*p == '{' || *p == '[') depth++; + if (*p == '}' || *p == ']') depth--; + if (depth < 0) return 0; + } + return depth == 0 && !instr; +} + +static void name_vlan(uint16_t vid, const char *name) +{ + static const char h[] = "0123456789abcdef"; + vlan_names[vlan_ptr++] = h[(vid >> 8) & 0xf]; + vlan_names[vlan_ptr++] = h[(vid >> 4) & 0xf]; + vlan_names[vlan_ptr++] = h[vid & 0xf]; + while (*name) vlan_names[vlan_ptr++] = *name++; + vlan_names[vlan_ptr++] = ' '; + vlan_names[vlan_ptr] = 0; +} + +static void make_vlan(uint16_t vid, uint16_t members, uint16_t tagged) +{ + vlan_settings.vlan = vid; + vlan_settings.members = members; + vlan_settings.tagged = tagged; + vlan_create(); +} + +static void t_vlan(void) +{ + printf("[test] /vlan.json shows what vlan_create and pvid wrote\n"); + hw_reset(); out_reset(); vlan_ptr = 0; vlan_names[0] = 0; + make_vlan(10, 0x0015, 0x0004); + name_vlan(10, "lab"); + port_pvid_set(0, 10); + port_pvid_set(2, 10); + port_pvid_set(4, 20); + send_vlan(10); + char want[32]; + snprintf(want, sizeof want, "\"members\":\"0x%x\"", hw_vlan_word(10)); + CHECK(has(want), "members is the entry word"); + CHECK(has("\"name\":\"lab\""), "name comes from the name table"); + CHECK(has("\"pvid\":\"0x0005\""), "pvid mask has ports 1 and 3, not 5"); + CHECK(balanced(), "valid JSON"); +} + +static void t_vlanlist(void) +{ + printf("[test] /vlanlist lists the VLANs that exist\n"); + hw_reset(); out_reset(); vlan_ptr = 0; vlan_names[0] = 0; + make_vlan(10, 0x0015, 0x0004); + make_vlan(20, 0x00f0, 0x0000); + make_vlan(4094, 0x0001, 0x0000); + name_vlan(10, "lab"); + name_vlan(4094, "top"); + management_vlan = 10; + send_vlanlist(); + CHECK(has("\"mgmt\":10"), "management VLAN"); + CHECK(has("{\"id\":10,\"name\":\"lab\"}"), "VLAN 10 with its name"); + CHECK(has("{\"id\":20,\"name\":\"\"}"), "VLAN 20 without a name"); + CHECK(has("{\"id\":4094,\"name\":\"top\"}"), "VLAN 4094 is the last one listed"); + CHECK(count("\"id\":") == 3, "and nothing else"); + CHECK(balanced(), "valid JSON"); + vlan_delete(20); + send_vlanlist(); + CHECK(count("\"id\":") == 2 && !has("\"id\":20"), "a deleted VLAN drops out"); + management_vlan = 1; +} + +static const uint8_t base_mac[6] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x00 }; + +static void put(uint16_t idx, uint8_t last, uint16_t vid, uint8_t port, bool st) +{ + uint8_t m[6]; + memcpy(m, base_mac, 6); + m[5] = last; + hw_l2_put(idx, m, vid, port, st); +} + +static void t_l2(void) +{ + printf("[test] /l2.json walks the table\n"); + hw_reset(); out_reset(); + put(0x010, 0x01, 1, 4, false); + put(0x020, 0x02, 2, CPU_PORT, true); + put(0x8f0, 0x03, 1, 7, false); + send_l2(0); + CHECK(count("\"mac\":") == 4, "three entries, and the first one again: that repeat is how the page sees the wrap"); + CHECK(count("\"idx\":\"0010\"") == 2 && strstr(body(), "\"idx\":\"0010\"}]"), "the repeat closes the list"); + CHECK(has("{\"vlan\":\"001\",\"mac\":\"00:11:22:33:44:01\",\"type\":\"l\",\"port\":4,\"lag\":0,\"idx\":\"0010\"}"), "learned entry, VLAN, port and index"); + CHECK(has("\"mac\":\"00:11:22:33:44:02\",\"type\":\"s\",\"port\":9"), "static entry on the CPU port"); + CHECK(has("\"idx\":\"08f0\""), "an index above 0x800 keeps its high bit"); + CHECK(balanced(), "valid JSON"); + + out_reset(); + send_l2(0x100); + const char *b = body(); + CHECK(strstr(b, "44:03") && strstr(b, "44:03") < strstr(b, "44:01"), "starting at 0x100 lists 0x8f0 first and wraps"); + CHECK(count("\"mac\":") == 4 && count("44:03") == 2, "and still finds all three before repeating 0x8f0"); +} + +/* Walk the table the way the page does: ask for the index after the last + * entry of each page until an index comes back that was seen before. */ +static void t_l2_page(void) +{ + printf("[test] /l2.json pages the way the web page walks it\n"); + hw_reset(); out_reset(); + for (int i = 0; i < 40; i++) + put(0x100 + i * 3, 0x10 + i, 1, i % 8, false); + uint8_t seen[4096 / 8] = { 0 }; + int pages = 0, unique = 0, wrapped = 0, idx = 0, biggest = 0; + while (!wrapped && pages < 10) { + out_reset(); + send_l2(idx); + pages++; + if (slen > TCP_OUTBUF_SIZE || !balanced()) + break; + if ((int)slen > biggest) + biggest = slen; + int n = 0, last = -1; + for (const char *p = body(); (p = strstr(p, "\"idx\":\"")); p += 7) { + int v = (int)strtol(p + 7, 0, 16); + n++; + if (seen[v >> 3] & (1 << (v & 7))) { wrapped = 1; break; } + seen[v >> 3] |= 1 << (v & 7); + unique++; + last = v; + } + if (!n) + break; + idx = last + 1; + } + CHECK(wrapped, "the walk ends on a repeated index"); + CHECK(unique == 40, "and has visited every entry once"); + CHECK(pages == 2, "in two pages"); + CHECK(biggest <= TCP_OUTBUF_SIZE, "no page outgrows outbuf"); + CHECK(balanced(), "every page is valid JSON"); +} + +static void t_status_counters(void) +{ + printf("[test] /status.json prints 64-bit packet counters in full\n"); + hw_reset(); out_reset(); + hw_counter_set(0, STAT_COUNTER_TX_PKTS, 0x100000005ULL); + hw_counter_set(0, STAT_COUNTER_RX_PKTS, 0x00000000000000ffULL); + hw_counter_set(1, STAT_COUNTER_TX_PKTS, 0x00000001ffffffffULL); + send_status(); + CHECK(has("\"txG\":\"0x100000005\""), "a counter past 2^32 keeps the zeros of its low word"); + CHECK(has("\"rxG\":\"0xff\""), "a small counter stays short"); + CHECK(has("\"txG\":\"0x1ffffffff\""), "a low word of all ones"); + out_reset(); + send_counters(1); + CHECK(has("\"0x100000005\"") && has("\"0x0\""), "/counters.json prints them the same way"); + CHECK(slen <= TCP_OUTBUF_SIZE, "fits the buffer"); + CHECK(balanced(), "valid JSON"); +} + +int main(void) +{ + printf("== httpd/page_impl.c JSON tests ==\n"); + t_vlan(); + t_vlanlist(); + t_l2(); + t_l2_page(); + t_status_counters(); + printf("\n%d checks, %d failed\n", tests_run, tests_failed); + return tests_failed ? 1 : 0; +} 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; +}