From 7bf0de0ff5a47a2858eb73104dc8db246544bc18 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 21 Oct 2026 21:57:53 +0200 Subject: [PATCH] Move the socket-return fill tests (issue 1165) to the integration level: they make a real socket and watch its directory --- .../mesh-provisioner-client/client_test.go | 93 --------------- .../socket_integration_test.go | 108 ++++++++++++++++++ 2 files changed, 108 insertions(+), 93 deletions(-) create mode 100644 modules/mesh-provisioner-client/cmd/mesh-provisioner-client/socket_integration_test.go diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client_test.go index bbba78d3..cc089ba4 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client_test.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client_test.go @@ -741,96 +741,3 @@ func (b *aBus) defaults(t *testing.T, module string, manifest []byte) { } } } - -// Issue 1165: a file whose write failed because the node-engine's socket was gone is written when the socket is made -// again, with no timer; a file that was placed is not asked again by it. -func TestNatsAFillThatFailedForWantOfTheSocketIsTriedWhenTheSocketReturns(t *testing.T) { - b := startBus(t) - heard := b.listen(t) - e := newEngine(t, map[string]string{dunstrc: "owned"}) - b.config(t, "dunst", "font-size", "13") - b.assign(t, "dunst", dunst(t)) - aClient(t, b, e, t.TempDir()) - - eventually(t, "the failure is said", func() bool { - r, _ := heard.last("dunst") - return r.Status == StatusFailed && strings.Contains(r.Reason, "fill.sock") - }) - if got := e.read(t, dunstrc); got != "" { - t.Fatalf("written without a socket: %q", got) - } - e.listen(t) - eventually(t, "dunstrc is written once the socket is back", func() bool { - return strings.Contains(e.read(t, dunstrc), "Monospace 13") - }) - eventually(t, "and the report says applied", func() bool { - r, _ := heard.last("dunst") - return r.Status == StatusApplied - }) -} - -func TestNatsAReturningSocketAsksNothingOfAModuleThatWasPlaced(t *testing.T) { - b := startBus(t) - e := newEngine(t, map[string]string{dunstrc: "owned"}) - stop := e.listen(t) - b.config(t, "dunst", "font-size", "13") - b.assign(t, "dunst", dunst(t)) - aClient(t, b, e, t.TempDir()) - eventually(t, "dunstrc is filled", func() bool { return strings.Contains(e.read(t, dunstrc), "Monospace 13") }) - _, before := e.counts(dunstrc) - - // The node-engine replaces itself: the socket goes and returns, and a later change proves the loop ran past it. - stop() - e.listen(t) - b.config(t, "dunst", "font-size", "15") - eventually(t, "the change is written", func() bool { return strings.Contains(e.read(t, dunstrc), "Monospace 15") }) - _, after := e.counts(dunstrc) - if after-before != 1 { - t.Errorf("dunstrc was asked %d times for one change", after-before) - } -} - -func TestWatchSocketWakesWhenTheSocketIsMadeAndWhenItsDirectoryReturns(t *testing.T) { - base, err := os.MkdirTemp("/tmp", "ws-") - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { os.RemoveAll(base) }) - dir := filepath.Join(base, "engine") - if err := os.Mkdir(dir, 0o755); err != nil { - t.Fatal(err) - } - socket := filepath.Join(dir, "fill.sock") - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - up, err := watchSocket(ctx, socket) - if err != nil { - t.Fatal(err) - } - heard := func(what string) { - t.Helper() - select { - case <-up: - case <-time.After(5 * time.Second): - t.Fatalf("no wake for %s", what) - } - } - l, err := net.Listen("unix", socket) - if err != nil { - t.Fatal(err) - } - heard("the socket made") - l.Close() - // The whole directory goes, as with a runtime directory removed on stop, and returns with the socket. - if err := os.RemoveAll(dir); err != nil { - t.Fatal(err) - } - if err := os.Mkdir(dir, 0o755); err != nil { - t.Fatal(err) - } - if l, err = net.Listen("unix", socket); err != nil { - t.Fatal(err) - } - defer l.Close() - heard("the directory and socket back") -} diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/socket_integration_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/socket_integration_test.go new file mode 100644 index 00000000..094a7e29 --- /dev/null +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/socket_integration_test.go @@ -0,0 +1,108 @@ +//go:build integration + +package main + +// Issue 1165: these tests make a real unix socket and watch its directory, so they run in the integration level. + +import ( + "context" + "net" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +// Issue 1165: a file whose write failed because the node-engine's socket was gone is written when the socket is made +// again, with no timer; a file that was placed is not asked again by it. +func TestNatsAFillThatFailedForWantOfTheSocketIsTriedWhenTheSocketReturns(t *testing.T) { + b := startBus(t) + heard := b.listen(t) + e := newEngine(t, map[string]string{dunstrc: "owned"}) + b.config(t, "dunst", "font-size", "13") + b.assign(t, "dunst", dunst(t)) + aClient(t, b, e, t.TempDir()) + + eventually(t, "the failure is said", func() bool { + r, _ := heard.last("dunst") + return r.Status == StatusFailed && strings.Contains(r.Reason, "fill.sock") + }) + if got := e.read(t, dunstrc); got != "" { + t.Fatalf("written without a socket: %q", got) + } + e.listen(t) + eventually(t, "dunstrc is written once the socket is back", func() bool { + return strings.Contains(e.read(t, dunstrc), "Monospace 13") + }) + eventually(t, "and the report says applied", func() bool { + r, _ := heard.last("dunst") + return r.Status == StatusApplied + }) +} + +func TestNatsAReturningSocketAsksNothingOfAModuleThatWasPlaced(t *testing.T) { + b := startBus(t) + e := newEngine(t, map[string]string{dunstrc: "owned"}) + stop := e.listen(t) + b.config(t, "dunst", "font-size", "13") + b.assign(t, "dunst", dunst(t)) + aClient(t, b, e, t.TempDir()) + eventually(t, "dunstrc is filled", func() bool { return strings.Contains(e.read(t, dunstrc), "Monospace 13") }) + _, before := e.counts(dunstrc) + + // The node-engine replaces itself: the socket goes and returns, and a later change proves the loop ran past it. + stop() + e.listen(t) + b.config(t, "dunst", "font-size", "15") + eventually(t, "the change is written", func() bool { return strings.Contains(e.read(t, dunstrc), "Monospace 15") }) + _, after := e.counts(dunstrc) + if after-before != 1 { + t.Errorf("dunstrc was asked %d times for one change", after-before) + } +} + +func TestWatchSocketWakesWhenTheSocketIsMadeAndWhenItsDirectoryReturns(t *testing.T) { + base, err := os.MkdirTemp("/tmp", "ws-") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { os.RemoveAll(base) }) + dir := filepath.Join(base, "engine") + if err := os.Mkdir(dir, 0o755); err != nil { + t.Fatal(err) + } + socket := filepath.Join(dir, "fill.sock") + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + up, err := watchSocket(ctx, socket) + if err != nil { + t.Fatal(err) + } + heard := func(what string) { + t.Helper() + select { + case <-up: + case <-time.After(5 * time.Second): + t.Fatalf("no wake for %s", what) + } + } + l, err := net.Listen("unix", socket) + if err != nil { + t.Fatal(err) + } + heard("the socket made") + l.Close() + // The whole directory goes, as with a runtime directory removed on stop, and returns with the socket. + if err := os.RemoveAll(dir); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(dir, 0o755); err != nil { + t.Fatal(err) + } + if l, err = net.Listen("unix", socket); err != nil { + t.Fatal(err) + } + defer l.Close() + heard("the directory and socket back") +}