feat(e2e): add debug/tcplogger E2E scenario kinds using debugclient

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>
This commit is contained in:
Martino Ferrari
2026-07-01 19:36:47 +02:00
parent efb4ea48fb
commit 83d0a060fe
5 changed files with 216 additions and 18 deletions
+58 -14
View File
@@ -9,6 +9,7 @@ import (
"flag"
"fmt"
"os"
"strings"
"sync"
"time"
@@ -54,13 +55,23 @@ func main() {
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)
ok, msg = runTcpLoggerScript(mc, *dur, &events, &mu, baseline)
default:
fmt.Fprintf(os.Stderr, "unknown -mode %q\n", *mode)
os.Exit(2)
@@ -109,28 +120,61 @@ func runDebugScript(mc *debugger.MarteController) (bool, string) {
return true, "FORCE/TRACE/BREAK sequence completed without disconnect"
}
// runTcpLoggerScript triggers a log-worthy event (an invalid FORCE) and
// waits for a matching "log" event to arrive over the TCPLogger TCP port
// within dur. MarteController's runLog forwards each "LOG <level> <msg>"
// line it reads from the TCPLogger connection through sink as
// map[string]any{"type": "log", "time": ..., "level": ..., "message": ...},
// so we poll the captured events slice for that shape.
func runTcpLoggerScript(mc *debugger.MarteController, dur time.Duration, events *[]event, mu *sync.Mutex) (bool, string) {
mc.SendCommand("FORCE App.Data.DoesNotExist 1")
// 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 {
for _, e := range (*events)[baseline:] {
if m, ok := e.data.(map[string]any); ok && m["type"] == "log" {
mu.Unlock()
return true, fmt.Sprintf("received log event: %v", m)
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 log event received"
return false, "lost connection during tcplogger wait and no matching log event received"
}
return false, "no log event received within duration"
return false, "no matching log event received within duration"
}