83d0a060fe
Adds a trimmed debug_e2e.cfg (DebugService on 8080/8081, TcpLogger on
9090) and two new scenarios (s55_debug_force_trace_break, kind=debug;
s56_tcplogger_delivery, kind=tcplogger) reusing it, with matching
run_e2e.sh scenario-list/dispatch wiring and debugclient build steps.
Also fixes a real bug found while wiring s56: debugclient's tcplogger
check was tautological (it matched MarteController's own local
"CMD"-level echo of the outgoing command, which contains the same text
as the triggered event, instead of a line actually delivered over the
real TCPLogger TCP socket) and its trigger command (an invalid FORCE)
never reaches DebugServiceBase's REPORT_ERROR at all. Switched the
trigger to a MSG-to-missing-destination command (which does call
REPORT_ERROR) and the match to require the real log text
("not found in ORD"), recorded only after a connect-settle baseline —
verified with a positive run (real Warning-level TCPLogger line
received) and a negative control (LogPort=0 disables TcpLogger and the
scenario correctly FAILs).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
181 lines
6.9 KiB
Go
181 lines
6.9 KiB
Go
// Command debugclient drives a running MARTeApp.ex DebugService+TCPLogger
|
|
// instance over TCP/UDP for the Test/E2E/suite "debug" and "tcplogger"
|
|
// scenario kinds. It scripts a fixed command sequence, captures responses
|
|
// and log/trace output, and reports PASS/FAIL as JSON to -out.
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
debugger "marte2debugger/controller" // see go.mod replace directive; marte2debugger is Client/debugger's module name
|
|
)
|
|
|
|
type event struct {
|
|
kind string
|
|
data any
|
|
}
|
|
|
|
func main() {
|
|
host := flag.String("host", "127.0.0.1", "MARTeApp host")
|
|
cmdPort := flag.Int("cmdport", 8080, "DebugService TCP command port")
|
|
udpPort := flag.Int("udpport", 8081, "DebugService UDP trace port")
|
|
logPort := flag.Int("logport", 9090, "TCPLogger TCP port")
|
|
scenario := flag.String("scenario", "", "scenario id (for output naming)")
|
|
outDir := flag.String("out", "/tmp/debug_e2e", "output directory")
|
|
mode := flag.String("mode", "debug", "debug|tcplogger")
|
|
dur := flag.Duration("dur", 4*time.Second, "how long to run the script/collection")
|
|
flag.Parse()
|
|
|
|
if *scenario == "" {
|
|
fmt.Fprintln(os.Stderr, "missing -scenario")
|
|
os.Exit(2)
|
|
}
|
|
os.MkdirAll(*outDir, 0o755)
|
|
|
|
var mu sync.Mutex
|
|
var events []event
|
|
sink := func(v any) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
// runLog (and every other MarteController event site) passes a
|
|
// map[string]any literal to sink; store it as-is, no
|
|
// unmarshal/remarshal needed.
|
|
events = append(events, event{kind: fmt.Sprintf("%T", v), data: v})
|
|
}
|
|
|
|
mc := debugger.NewHeadlessMarteController(sink)
|
|
mc.Connect(*host, *cmdPort, *udpPort, *logPort)
|
|
defer mc.Disconnect()
|
|
|
|
time.Sleep(500 * time.Millisecond) // allow TCP/UDP/log connects to establish
|
|
|
|
// Snapshot the event count *after* the connect-settle sleep so
|
|
// runTcpLoggerScript only scans events recorded from here on. Without this,
|
|
// the "log" event that Connect() itself emits synchronously (a "Connecting
|
|
// to ..." INFO message, well before any TCPLogger TCP traffic happens)
|
|
// would satisfy the check instantly regardless of whether the real
|
|
// TCPLogger (port 9090) ever delivers anything for the triggered event.
|
|
mu.Lock()
|
|
baseline := len(events)
|
|
mu.Unlock()
|
|
|
|
var ok bool
|
|
var msg string
|
|
switch *mode {
|
|
case "debug":
|
|
ok, msg = runDebugScript(mc)
|
|
case "tcplogger":
|
|
ok, msg = runTcpLoggerScript(mc, *dur, &events, &mu, baseline)
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "unknown -mode %q\n", *mode)
|
|
os.Exit(2)
|
|
}
|
|
|
|
time.Sleep(300 * time.Millisecond) // drain final events
|
|
|
|
result := map[string]any{
|
|
"id": *scenario,
|
|
"pass": ok,
|
|
"detail": msg,
|
|
}
|
|
f, _ := os.Create(fmt.Sprintf("%s/result_%s.json", *outDir, *scenario))
|
|
json.NewEncoder(f).Encode(result)
|
|
f.Close()
|
|
|
|
status := "FAIL"
|
|
if ok {
|
|
status = "PASS"
|
|
}
|
|
fmt.Printf("%s: %s - %s\n", *scenario, status, msg)
|
|
os.WriteFile(fmt.Sprintf("%s/status_%s.txt", *outDir, *scenario), []byte(status), 0o644)
|
|
|
|
if !ok {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// runDebugScript exercises FORCE/TRACE/BREAK over the TCP 8080 command
|
|
// protocol (grammar confirmed against Source/Components/Interfaces/
|
|
// DebugService/DebugServiceBase.cpp's HandleCommand: "FORCE <name> <val>",
|
|
// "TRACE <name> <0|1> [decim]", "BREAK <name> <op|OFF> <threshold>",
|
|
// each replying "OK <TOKEN> <count>\n").
|
|
func runDebugScript(mc *debugger.MarteController) (bool, string) {
|
|
mc.SendCommand("FORCE App.Data.Timer.Counter 42")
|
|
time.Sleep(200 * time.Millisecond)
|
|
mc.SendCommand("TRACE App.Data.Timer.Counter 1 1")
|
|
time.Sleep(200 * time.Millisecond)
|
|
mc.SendCommand("BREAK App.Data.Timer.Counter > 1000")
|
|
time.Sleep(500 * time.Millisecond)
|
|
mc.SendCommand("BREAK App.Data.Timer.Counter OFF")
|
|
mc.SendCommand("UNFORCE App.Data.Timer.Counter")
|
|
if !mc.IsConnected() {
|
|
return false, "lost connection to DebugService during script"
|
|
}
|
|
return true, "FORCE/TRACE/BREAK sequence completed without disconnect"
|
|
}
|
|
|
|
// runTcpLoggerScript triggers a log-worthy event and waits for a matching
|
|
// "log" event to arrive over the real TCPLogger TCP connection (port 9090)
|
|
// within dur.
|
|
//
|
|
// NOTE: an invalid "FORCE <nonexistent signal> <val>" is *not* usable here —
|
|
// DebugServiceBase::ForceSignal() silently no-ops (count stays 0, no
|
|
// REPORT_ERROR call) when no signal matches, so it never reaches the logger
|
|
// at all. Instead we send "MSG <nonexistent destination> <fn> 0", which
|
|
// DebugServiceBase::HandleCommand's MSG handler always resolves via
|
|
// ObjectRegistryDatabase::Find() and, on failure, does call
|
|
// REPORT_ERROR_STATIC(Warning, "MSG: destination '%s' not found in ORD.")
|
|
// (Source/Components/Interfaces/DebugService/DebugServiceBase.cpp) — a real,
|
|
// synchronous, deterministic log emission that the framework's LoggerService
|
|
// broadcasts to the auto-injected TcpLogger, which then writes
|
|
// "LOG <level> <description>\n" to every connected TCP client (see
|
|
// Source/Components/Interfaces/TCPLogger/TcpLogger.cpp ConsumeLogMessage /
|
|
// Execute). MarteController's runLog reads that line from the real TCP 9090
|
|
// socket and forwards it through sink as map[string]any{"type": "log",
|
|
// "time": ..., "level": ..., "message": ...}.
|
|
//
|
|
// baseline is the length of *events captured right after the initial
|
|
// connect-settle sleep, before this function sends the MSG command: only
|
|
// events recorded from index baseline onward are eligible, so the "log"
|
|
// event that MarteController.Connect() itself emits synchronously (a
|
|
// "Connecting to ..." INFO message, emitted before any TCP/UDP/log socket
|
|
// activity) can never satisfy this check.
|
|
func runTcpLoggerScript(mc *debugger.MarteController, dur time.Duration, events *[]event, mu *sync.Mutex, baseline int) (bool, string) {
|
|
const missingDest = "DebugClientNoSuchDestination"
|
|
mc.SendCommand(fmt.Sprintf("MSG %s SomeFunction 0", missingDest))
|
|
deadline := time.Now().Add(dur)
|
|
for time.Now().Before(deadline) {
|
|
mu.Lock()
|
|
for _, e := range (*events)[baseline:] {
|
|
if m, ok := e.data.(map[string]any); ok && m["type"] == "log" {
|
|
msg, _ := m["message"].(string)
|
|
// MarteController.writeCmd() also emits a client-side "CMD"
|
|
// level log echoing every outgoing command as "→ <cmd>"
|
|
// (see martecontrol.go) — since our own MSG command text
|
|
// contains missingDest, that local echo would otherwise
|
|
// satisfy a naive substring check instantly, before the real
|
|
// TCPLogger round-trip ever happens. Require the actual
|
|
// REPORT_ERROR text the DebugService MSG handler produces
|
|
// ("not found in ORD"), which can only arrive via runLog's
|
|
// real TCP 9090 read, never via the local echo.
|
|
if strings.Contains(msg, missingDest) && strings.Contains(msg, "not found in ORD") {
|
|
mu.Unlock()
|
|
return true, fmt.Sprintf("received real TCPLogger log event: %v", m)
|
|
}
|
|
}
|
|
}
|
|
mu.Unlock()
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
if !mc.IsConnected() {
|
|
return false, "lost connection during tcplogger wait and no matching log event received"
|
|
}
|
|
return false, "no matching log event received within duration"
|
|
}
|