// 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, &events, &mu) 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) } } // waitForAck polls events (from index start onward) for a MarteController // "text_line" event exactly matching DebugServiceBase::HandleCommand's // "OK \n" reply grammar, requiring count > 0. HandleCommand // prints this reply for every one of FORCE/UNFORCE/TRACE/BREAK regardless of // enable/disable direction, and count is always "number of signals the // command actually matched" (DebugServiceBase.cpp:1468-1525) — so count==0 // means the target signal path was never resolved (e.g. a wire-format/name- // translation bug), which a bare "did the socket stay open" check like the // old runDebugScript could never catch. func waitForAck(events *[]event, mu *sync.Mutex, start int, token string, timeout time.Duration) (string, bool) { prefix := "OK " + token + " " deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { mu.Lock() for _, e := range (*events)[start:] { m, ok := e.data.(map[string]any) if !ok || m["type"] != "text_line" { continue } line, _ := m["data"].(string) if !strings.HasPrefix(line, prefix) { continue } var count uint32 if _, err := fmt.Sscanf(line, prefix+"%d", &count); err == nil && count > 0 { mu.Unlock() return line, true } } mu.Unlock() time.Sleep(50 * time.Millisecond) } return "", false } // 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"). Each step waits for the real // "OK " acknowledgement with count > 0 via waitForAck, // confirming DebugService actually resolved and applied the command against // the named signal — not merely that the TCP connection stayed alive (which // would pass even if DebugService silently no-op'd every command, e.g. due // to a signal-name/wire-format bug). func runDebugScript(mc *debugger.MarteController, events *[]event, mu *sync.Mutex) (bool, string) { const ackTimeout = 2 * time.Second mu.Lock() start := len(*events) mu.Unlock() mc.SendCommand("FORCE App.Data.Timer.Counter 42") if _, ok := waitForAck(events, mu, start, "FORCE", ackTimeout); !ok { return false, "no \"OK FORCE \" ack with count>0 received (signal not resolved)" } mu.Lock() start = len(*events) mu.Unlock() mc.SendCommand("TRACE App.Data.Timer.Counter 1 1") if _, ok := waitForAck(events, mu, start, "TRACE", ackTimeout); !ok { return false, "no \"OK TRACE \" ack with count>0 received (signal not resolved)" } mu.Lock() start = len(*events) mu.Unlock() mc.SendCommand("BREAK App.Data.Timer.Counter > 1000") if _, ok := waitForAck(events, mu, start, "BREAK", ackTimeout); !ok { return false, "no \"OK BREAK \" ack with count>0 received for SetBreak (signal not resolved)" } mu.Lock() start = len(*events) mu.Unlock() mc.SendCommand("BREAK App.Data.Timer.Counter OFF") if _, ok := waitForAck(events, mu, start, "BREAK", ackTimeout); !ok { return false, "no \"OK BREAK \" ack with count>0 received for ClearBreak (signal not resolved)" } mu.Lock() start = len(*events) mu.Unlock() mc.SendCommand("UNFORCE App.Data.Timer.Counter") if _, ok := waitForAck(events, mu, start, "UNFORCE", ackTimeout); !ok { return false, "no \"OK UNFORCE \" ack with count>0 received (signal not resolved)" } if !mc.IsConnected() { return false, "lost connection to DebugService during script" } return true, "FORCE/TRACE/BREAK/UNFORCE all acknowledged with count>0 (real signal resolution confirmed)" } // 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" }