fix(e2e): assert real FORCE/TRACE/BREAK acks in the debug scenario

The final whole-branch review found runDebugScript only checked
mc.IsConnected() after sending FORCE/TRACE/BREAK/UNFORCE -- a liveness
check that would PASS even if DebugService silently no-op'd every command
(e.g. a signal-name/wire-format bug), undercutting the design's stated
rationale for this scenario ("catching wire-format/serialization bugs the
in-process suite cannot"). This mirrors the tautology already fixed for
the tcplogger scenario in an earlier task, but had not been applied here.

Added waitForAck(), which polls the sink's recorded "text_line" events for
DebugServiceBase::HandleCommand's real "OK <TOKEN> <count>\n" reply and
requires count > 0 -- HandleCommand prints this for every one of
FORCE/UNFORCE/TRACE/BREAK regardless of enable/disable direction, and
count is always "number of signals actually matched", so count==0 means
the signal path was never resolved. Verified with a real negative control
(temporarily pointing all commands at a nonexistent signal name): the
scenario now correctly FAILs, then reverted and reconfirmed PASS against
the real signal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-07-02 00:03:47 +02:00
parent 9a39cf923a
commit 28d149f536
2 changed files with 81 additions and 7 deletions
Binary file not shown.
+81 -7
View File
@@ -69,7 +69,7 @@ func main() {
var msg string
switch *mode {
case "debug":
ok, msg = runDebugScript(mc)
ok, msg = runDebugScript(mc, &events, &mu)
case "tcplogger":
ok, msg = runTcpLoggerScript(mc, *dur, &events, &mu, baseline)
default:
@@ -100,24 +100,98 @@ func main() {
}
}
// waitForAck polls events (from index start onward) for a MarteController
// "text_line" event exactly matching DebugServiceBase::HandleCommand's
// "OK <token> <count>\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 <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) {
// each replying "OK <TOKEN> <count>\n"). Each step waits for the real
// "OK <TOKEN> <count>" 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")
time.Sleep(200 * time.Millisecond)
if _, ok := waitForAck(events, mu, start, "FORCE", ackTimeout); !ok {
return false, "no \"OK FORCE <count>\" ack with count>0 received (signal not resolved)"
}
mu.Lock()
start = len(*events)
mu.Unlock()
mc.SendCommand("TRACE App.Data.Timer.Counter 1 1")
time.Sleep(200 * time.Millisecond)
if _, ok := waitForAck(events, mu, start, "TRACE", ackTimeout); !ok {
return false, "no \"OK TRACE <count>\" ack with count>0 received (signal not resolved)"
}
mu.Lock()
start = len(*events)
mu.Unlock()
mc.SendCommand("BREAK App.Data.Timer.Counter > 1000")
time.Sleep(500 * time.Millisecond)
if _, ok := waitForAck(events, mu, start, "BREAK", ackTimeout); !ok {
return false, "no \"OK BREAK <count>\" 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 <count>\" 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 <count>\" ack with count>0 received (signal not resolved)"
}
if !mc.IsConnected() {
return false, "lost connection to DebugService during script"
}
return true, "FORCE/TRACE/BREAK sequence completed without disconnect"
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