// 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 ", // "TRACE <0|1> [decim]", "BREAK ", // each replying "OK \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 " 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 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 \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 "→ " // (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" }