From 3fb7d42287e579f30b6e535b7da6208250bd784c Mon Sep 17 00:00:00 2001 From: Tobias Harnickell Date: Wed, 27 Aug 2025 14:41:00 +0200 Subject: [PATCH 1/2] Improve test-coverage on blobplugin --- pkg/blobplugin/main_test.go | 98 +++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/pkg/blobplugin/main_test.go b/pkg/blobplugin/main_test.go index e73985d1a..0d8f721a9 100644 --- a/pkg/blobplugin/main_test.go +++ b/pkg/blobplugin/main_test.go @@ -17,11 +17,15 @@ limitations under the License. package main import ( + "context" "fmt" + "io" "net" + "net/http" "os" "reflect" "testing" + "time" ) func TestMain(t *testing.T) { @@ -80,3 +84,97 @@ func TestTrapClosedConnErr(t *testing.T) { } } } + +func TestServeMetrics(t *testing.T) { + // Open random test port + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + + // Start serveMetrics in background + errCh := make(chan error, 1) + go func() { errCh <- serveMetrics(l) }() + + // Build URL + url := "http://" + l.Addr().String() + "/metrics" + + // Client timeout for each request + client := &http.Client{Timeout: 500 * time.Millisecond} + + // Poll with 3-second deadlines until server is ready + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + done := make(chan struct{}) + go func(ctx context.Context) { + defer close(done) + for { + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + // Execute probe, expecting status 200 + resp, err := client.Do(req) + if err == nil { + if resp.StatusCode == http.StatusOK { + resp.Body.Close() + return + } + resp.Body.Close() + } + // Abort probe if context deadline is expired or canceled + select { + case <-ctx.Done(): + return + default: + time.Sleep(20 * time.Millisecond) + } + } + }(ctx) + + // Wait for readiness or fail on timeout + select { + case <-done: + case <-ctx.Done(): + t.Fatalf("server not ready: %v", ctx.Err()) + } + + // Perform the request + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + resp, err := client.Do(req) + if err != nil { + t.Fatalf("Get /metrics: %v", err) + } + t.Cleanup(func() { + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } + }) + + // Check for HTTP status 200 + if resp.StatusCode != http.StatusOK { + t.Fatalf("unexpected status: %d", resp.StatusCode) + } + // Validate response body is non-empty + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("read body: %v", err) + } + // Validate response body is non-empty + if len(body) == 0 { + t.Fatalf("empty metrics body") + } + + // Trigger graceful shutdown + if err := l.Close(); err != nil { + t.Fatalf("close listener %v", err) + } + + // Fail if errCh exits non-graceful or is not closed after 2 sec + select { + case err := <-errCh: + if err != nil { + t.Fatalf("serveMetrics error after close: %v", err) + } + case <-time.After(2 * time.Second): + t.Fatalf("serveMetrics did not exit after listener close") + } +} From b60e7dd6a423b972efb557971a750fe86e781e00 Mon Sep 17 00:00:00 2001 From: Tobias Harnickell Date: Wed, 27 Aug 2025 14:41:00 +0200 Subject: [PATCH 2/2] test: Improve test-coverage on blobplugin Refs: #703 Signed-off-by: Tobias Harnickell --- pkg/blobplugin/main_test.go | 98 +++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/pkg/blobplugin/main_test.go b/pkg/blobplugin/main_test.go index e73985d1a..0d8f721a9 100644 --- a/pkg/blobplugin/main_test.go +++ b/pkg/blobplugin/main_test.go @@ -17,11 +17,15 @@ limitations under the License. package main import ( + "context" "fmt" + "io" "net" + "net/http" "os" "reflect" "testing" + "time" ) func TestMain(t *testing.T) { @@ -80,3 +84,97 @@ func TestTrapClosedConnErr(t *testing.T) { } } } + +func TestServeMetrics(t *testing.T) { + // Open random test port + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + + // Start serveMetrics in background + errCh := make(chan error, 1) + go func() { errCh <- serveMetrics(l) }() + + // Build URL + url := "http://" + l.Addr().String() + "/metrics" + + // Client timeout for each request + client := &http.Client{Timeout: 500 * time.Millisecond} + + // Poll with 3-second deadlines until server is ready + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + done := make(chan struct{}) + go func(ctx context.Context) { + defer close(done) + for { + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + // Execute probe, expecting status 200 + resp, err := client.Do(req) + if err == nil { + if resp.StatusCode == http.StatusOK { + resp.Body.Close() + return + } + resp.Body.Close() + } + // Abort probe if context deadline is expired or canceled + select { + case <-ctx.Done(): + return + default: + time.Sleep(20 * time.Millisecond) + } + } + }(ctx) + + // Wait for readiness or fail on timeout + select { + case <-done: + case <-ctx.Done(): + t.Fatalf("server not ready: %v", ctx.Err()) + } + + // Perform the request + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + resp, err := client.Do(req) + if err != nil { + t.Fatalf("Get /metrics: %v", err) + } + t.Cleanup(func() { + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } + }) + + // Check for HTTP status 200 + if resp.StatusCode != http.StatusOK { + t.Fatalf("unexpected status: %d", resp.StatusCode) + } + // Validate response body is non-empty + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("read body: %v", err) + } + // Validate response body is non-empty + if len(body) == 0 { + t.Fatalf("empty metrics body") + } + + // Trigger graceful shutdown + if err := l.Close(); err != nil { + t.Fatalf("close listener %v", err) + } + + // Fail if errCh exits non-graceful or is not closed after 2 sec + select { + case err := <-errCh: + if err != nil { + t.Fatalf("serveMetrics error after close: %v", err) + } + case <-time.After(2 * time.Second): + t.Fatalf("serveMetrics did not exit after listener close") + } +}