Add goTool autotest with bench configs and UART scenarios.

JSON configs describe network and node MACs; scenarios run command
sequences with expect checks. Share UART client API across CLI and tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-18 23:36:28 +02:00
co-authored by Cursor
parent a8ae65d9dc
commit d24b0cb5c3
17 changed files with 958 additions and 106 deletions
+133
View File
@@ -0,0 +1,133 @@
package autotest
import (
"encoding/json"
"fmt"
"os"
"strings"
)
// Config describes the bench: ESP-NOW network, master MAC, and known slaves.
type Config struct {
ID string `json:"id"`
Description string `json:"description,omitempty"`
Network uint `json:"network"`
MasterMAC string `json:"master_mac"`
Slaves []SlaveNode `json:"slaves"`
}
type SlaveNode struct {
ID string `json:"id"`
MAC string `json:"mac"`
ClientID *uint `json:"client_id,omitempty"`
}
type Bench struct {
Config
slaveByID map[string]*SlaveNode
}
func LoadConfig(path string) (*Bench, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config: %w", err)
}
return NewBench(cfg)
}
func NewBench(cfg Config) (*Bench, error) {
cfg.MasterMAC = normalizeMAC(cfg.MasterMAC)
if cfg.ID == "" {
return nil, fmt.Errorf("config: id is required")
}
if cfg.MasterMAC == "" {
return nil, fmt.Errorf("config %q: master_mac is required", cfg.ID)
}
if cfg.Network < 1 || cfg.Network > 8 {
return nil, fmt.Errorf("config %q: network must be 18 (DIP / IO expander)", cfg.ID)
}
b := &Bench{Config: cfg, slaveByID: make(map[string]*SlaveNode)}
for i := range cfg.Slaves {
s := &cfg.Slaves[i]
if s.ID == "" {
return nil, fmt.Errorf("config %q: slave missing id", cfg.ID)
}
if _, dup := b.slaveByID[s.ID]; dup {
return nil, fmt.Errorf("config %q: duplicate slave id %q", cfg.ID, s.ID)
}
mac, err := parseMAC(s.MAC)
if err != nil {
return nil, fmt.Errorf("config %q slave %q: %w", cfg.ID, s.ID, err)
}
s.MAC = mac
if s.ClientID == nil {
parts := strings.Split(mac, ":")
var last byte
fmt.Sscanf(parts[5], "%02x", &last)
id := uint(last)
s.ClientID = &id
}
b.slaveByID[s.ID] = s
}
return b, nil
}
func (b *Bench) Slave(id string) (*SlaveNode, error) {
s, ok := b.slaveByID[id]
if !ok {
return nil, fmt.Errorf("unknown slave %q (config %q)", id, b.ID)
}
return s, nil
}
func (b *Bench) ResolveClientID(slaveID string) (uint32, error) {
if slaveID == "" || slaveID == "master" || slaveID == "local" {
return 0, nil
}
s, err := b.Slave(slaveID)
if err != nil {
return 0, err
}
return uint32(*s.ClientID), nil
}
func normalizeMAC(s string) string {
s = strings.TrimSpace(strings.ToLower(s))
s = strings.ReplaceAll(s, "-", ":")
return s
}
func parseMAC(s string) (string, error) {
s = normalizeMAC(s)
parts := strings.Split(s, ":")
if len(parts) != 6 {
return "", fmt.Errorf("invalid mac %q", s)
}
out := make([]byte, 6)
for i, p := range parts {
var b byte
if _, err := fmt.Sscanf(p, "%02x", &b); err != nil {
return "", fmt.Errorf("invalid mac byte %q", p)
}
out[i] = b
}
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x",
out[0], out[1], out[2], out[3], out[4], out[5]), nil
}
func MACEqual(a, b string) bool {
return normalizeMAC(a) == normalizeMAC(b)
}
func MACFromProto(mac []byte) string {
if len(mac) != 6 {
return ""
}
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5])
}