From 194362c9bbb88191c8454605a44e0b17bec276d4 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Sat, 17 Oct 2026 20:32:48 +0200 Subject: [PATCH] test(ocpp): stop test chargers after the server (#1712) Core probes each charger it accepts (GetConfiguration, then a TriggerMessage identity probe). Several tests stopped their charge point before the server, so a probe could arrive while the charger closed. ocpp-go v0.19.0's ws.Client.Write checks the connection and then sends without its lock, so the answer could send on the closed queue and panic ("send on closed channel", seen in run 39045946853). stopAfterServer stops the server first at cleanup; Server.Stop is idempotent. Only test code changes; Core uses the ocpp-go server, and the client runs only in tests and cmd/sim-ocpp. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Fredrik Ahlgren --- go/internal/ocpp/auth_test.go | 2 +- go/internal/ocpp/charging_needs_test.go | 2 +- go/internal/ocpp/control_test.go | 2 +- go/internal/ocpp/control_v201_test.go | 2 +- go/internal/ocpp/identity_test.go | 2 +- go/internal/ocpp/server_test.go | 19 ++++++++++++++++--- 6 files changed, 21 insertions(+), 8 deletions(-) diff --git a/go/internal/ocpp/auth_test.go b/go/internal/ocpp/auth_test.go index cb1f79371..c92c6f8b0 100644 --- a/go/internal/ocpp/auth_test.go +++ b/go/internal/ocpp/auth_test.go @@ -288,7 +288,7 @@ func TestPerChargerCredentialOverTheWire(t *testing.T) { cp := ocpp16.NewChargePoint(id, nil, client) err := cp.Start(fmt.Sprintf("ws://127.0.0.1:%d", port)) if err == nil { - t.Cleanup(cp.Stop) + stopAfterServer(t, srv, cp.Stop) } return err } diff --git a/go/internal/ocpp/charging_needs_test.go b/go/internal/ocpp/charging_needs_test.go index 97d8d4c82..259b2ebab 100644 --- a/go/internal/ocpp/charging_needs_test.go +++ b/go/internal/ocpp/charging_needs_test.go @@ -33,7 +33,7 @@ func connectStationForNeeds(t *testing.T, srv *Server, port int, id string) ocpp t.Fatalf("charging station connect: %v", err) } var once sync.Once - t.Cleanup(func() { once.Do(cs.Stop) }) + stopAfterServer(t, srv, func() { once.Do(cs.Stop) }) if _, err := cs.BootNotification(provisioning.BootReasonPowerUp, "Dawn", "Charge Amps"); err != nil { t.Fatalf("boot: %v", err) diff --git a/go/internal/ocpp/control_test.go b/go/internal/ocpp/control_test.go index 03d746e4f..574a431a3 100644 --- a/go/internal/ocpp/control_test.go +++ b/go/internal/ocpp/control_test.go @@ -114,7 +114,7 @@ func connectCharger(t *testing.T, srv *Server, port int, id string) (ocpp16.Char } var once sync.Once stop := func() { once.Do(cp.Stop) } - t.Cleanup(stop) + stopAfterServer(t, srv, stop) if _, err := cp.BootNotification("Dawn", "Charge Amps"); err != nil { t.Fatalf("boot: %v", err) diff --git a/go/internal/ocpp/control_v201_test.go b/go/internal/ocpp/control_v201_test.go index 1b0887b9f..38d175c61 100644 --- a/go/internal/ocpp/control_v201_test.go +++ b/go/internal/ocpp/control_v201_test.go @@ -121,7 +121,7 @@ func connectStationV201(t *testing.T, srv *Server, port int, id string) (*fakeSt } var once sync.Once stop := func() { once.Do(cs.Stop) } - t.Cleanup(stop) + stopAfterServer(t, srv, stop) station := provisioning.ChargingStationType{Model: "Dawn", VendorName: "Charge Amps"} if _, err := cs.BootNotification(provisioning.BootReasonPowerUp, station.Model, station.VendorName); err != nil { diff --git a/go/internal/ocpp/identity_test.go b/go/internal/ocpp/identity_test.go index d20c459ad..a7fc610d3 100644 --- a/go/internal/ocpp/identity_test.go +++ b/go/internal/ocpp/identity_test.go @@ -121,7 +121,7 @@ func TestBootNotificationTakesEitherSerialField(t *testing.T) { if err := cp.Start(fmt.Sprintf("ws://127.0.0.1:%d", port)); err != nil { t.Fatalf("connect: %v", err) } - t.Cleanup(cp.Stop) + stopAfterServer(t, srv, cp.Stop) if _, err := cp.BootNotification("Halo", "Charge Amps", tc.props...); err != nil { t.Fatalf("boot: %v", err) diff --git a/go/internal/ocpp/server_test.go b/go/internal/ocpp/server_test.go index c5c385ed9..4422b4ee9 100644 --- a/go/internal/ocpp/server_test.go +++ b/go/internal/ocpp/server_test.go @@ -55,6 +55,19 @@ func startServer(t *testing.T, tel *telemetry.Store, approved ...string) (int, * return 0, nil } +// stopAfterServer stops a test charger at cleanup, after the server. Core +// probes each charger it accepts, and ocpp-go v0.19.0's ws.Client.Write checks +// the connection, then sends without its lock: a probe answered while the +// charger closes can send on the closed queue and panic (#1712). Server.Stop +// is idempotent. +func stopAfterServer(t *testing.T, srv *Server, stop func()) { + t.Helper() + t.Cleanup(func() { + srv.Stop() + stop() + }) +} + func TestStopIsConcurrentAndIdempotent(t *testing.T) { _, srv := startServer(t, telemetry.NewStore()) @@ -80,7 +93,7 @@ func TestBootAndMeterValuesPushDerEV(t *testing.T) { if err := cp.Start(url); err != nil { t.Fatalf("client connect: %v", err) } - defer cp.Stop() + stopAfterServer(t, srv, cp.Stop) if _, err := cp.BootNotification("EaseeHome", "Easee"); err != nil { t.Fatalf("boot: %v", err) @@ -149,7 +162,7 @@ func TestStartStopTransactionTracksSession(t *testing.T) { if err := cp.Start(fmt.Sprintf("ws://127.0.0.1:%d", port)); err != nil { t.Fatalf("connect: %v", err) } - defer cp.Stop() + stopAfterServer(t, srv, cp.Stop) if _, err := cp.BootNotification("Home", "Easee"); err != nil { t.Fatalf("boot: %v", err) @@ -200,7 +213,7 @@ func TestPendingChargerIsQuarantinedFromTelemetry(t *testing.T) { if err := cp.Start(fmt.Sprintf("ws://127.0.0.1:%d", port)); err != nil { t.Fatalf("connect: %v", err) } - defer cp.Stop() + stopAfterServer(t, srv, cp.Stop) if _, err := cp.BootNotification("EvilCo", "FakeCharger"); err != nil { t.Fatalf("boot: %v", err)