blob: 7d68b7ae9dff8dc99b37ce56f169d01ca1efdf9f [file] [edit]
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rsproxy
import (
"context"
"net"
"os"
"path/filepath"
"sync"
"testing"
"time"
"github.com/bazelbuild/rsclient/internal/pkg/auth"
"github.com/bazelbuild/rsclient/internal/pkg/fakejobstatus"
"github.com/bazelbuild/rsclient/internal/pkg/fakeresultstore"
"github.com/bazelbuild/rsclient/internal/pkg/fifo"
"github.com/bazelbuild/rsclient/internal/pkg/fifo/fifotest"
"github.com/bazelbuild/rsclient/internal/pkg/grpcutil"
"github.com/bazelbuild/rsclient/internal/pkg/grpcutil/grpctest"
"github.com/bazelbuild/rsclient/internal/pkg/netutil"
"github.com/bazelbuild/rsclient/internal/pkg/rsmsg"
"github.com/bazelbuild/rsclient/internal/pkg/testenv"
"google.golang.org/grpc"
"google.golang.org/grpc/test/bufconn"
jspb "github.com/bazelbuild/rsclient/api/jobstatus"
rspb "google.golang.org/genproto/googleapis/devtools/resultstore/v2"
_ "github.com/bazelbuild/rsclient/internal/pkg/testsetup"
)
const (
// pollTimeout is used for short-lived polling loops waiting for background
// tasks to update shared state.
pollTimeout = 2 * time.Second
)
// proxyTestEnv wraps the standard test environment with a Proxy instance
// configured for testing.
type proxyTestEnv struct {
*testenv.Env
proxy *Proxy
fakeRS *fakeresultstore.Server
fakeJS *fakejobstatus.Server
}
// newProxyTestEnv initializes a fake backend environment and creates a Proxy
// connected to it via in-memory bufconn listeners.
func newProxyTestEnv(ctx context.Context, t *testing.T, opts ...ProxyOpt) (*proxyTestEnv, error) {
// Use a sub-context with timeout for dialing and initialization of fake servers.
setupCtx, cancel := context.WithTimeout(ctx, defaultTestTimeout)
defer cancel()
// Always provide standard test defaults.
allOpts := []ProxyOpt{&ProxyConfig{
ResultStoreEgressOptions: ResultStoreEgressOptions{
PartialInvocation: &rspb.Invocation{
InvocationAttributes: &rspb.InvocationAttributes{},
},
CASInstanceName: "instance",
},
}}
allOpts = append(allOpts, opts...)
env, err := testenv.New(setupCtx, t, testenv.WithResultStore(), testenv.WithJobStatus())
if err != nil {
return nil, err
}
// Use the fake connections from the test environment.
params := ProxyEgressParams{
RSConn: env.RS.Conn,
CASConn: env.CAS.Conn,
JSConn: env.JS.Conn,
}
// The Proxy itself should use the long-lived ctx.
proxy, err := NewProxyFromConnection(ctx, params, allOpts...)
if err != nil {
return nil, err
}
// Register an automatic cleanup task to close the proxy.
proxy.AddCleanup(func() {
cleanupCtx, cancel := context.WithTimeout(t.Context(), extendedTestTimeout)
defer cancel()
if err := proxy.Close(cleanupCtx); err != nil {
if grpcutil.IsContextError(err) {
return
}
t.Errorf("proxy.Close() failed: %v", err)
}
})
return &proxyTestEnv{
Env: env,
proxy: proxy,
fakeRS: env.RS.Impl,
fakeJS: env.JS.Impl,
}, nil
}
// TestProxyConfig_ApplyProxy_Merging verifies that different ProxyOpt types
// correctly merge their settings into the Proxy configuration without
// overwriting unrelated fields.
func TestProxyConfig_ApplyProxy_Merging(t *testing.T) {
p := &Proxy{
config: &ProxyConfig{
ResultStoreEgressOptions: ResultStoreEgressOptions{
BundleCountThreshold: 100,
BundleByteThreshold: 1000,
},
},
}
// 1. Apply an option that only sets BundleCountThreshold.
opt1 := &ProxyConfig{
ResultStoreEgressOptions: ResultStoreEgressOptions{
BundleCountThreshold: 50,
},
}
opt1.ApplyProxy(p)
if p.config.BundleCountThreshold != 50 {
t.Errorf("BundleCountThreshold = %d, want 50", p.config.BundleCountThreshold)
}
if p.config.BundleByteThreshold != 1000 {
t.Errorf("BundleByteThreshold = %d, want 1000 (preserved)", p.config.BundleByteThreshold)
}
// 2. Apply an option that sets RSDialParams via DialParamsOpt.
opt2 := &DialParamsOpt{
RSDialParams: auth.DialParams{Service: "myservice"},
}
opt2.ApplyProxy(p)
if p.config.RSEgress == nil || p.config.RSEgress.DialParams.Service != "myservice" {
t.Errorf("RSEgress.Service = %v, want \"myservice\"", p.config.RSEgress)
}
if p.config.BundleCountThreshold != 50 {
t.Errorf("BundleCountThreshold = %d, want 50 (preserved)", p.config.BundleCountThreshold)
}
}
// TestProxy_Run verifies the core build event fan-out pipeline of the Proxy.
func TestProxy_Run(t *testing.T) {
testCases := []struct {
name string
requests []*rspb.UploadRequest
}{
{
name: "empty request stream",
requests: nil,
},
{
name: "single create request",
requests: []*rspb.UploadRequest{
rsmsg.NewInvocationUploadRequest(rspb.UploadRequest_CREATE, rsmsg.NewInvocation("test-invocation", rsmsg.StatusUnspecified)),
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
testCtx, cancel := context.WithTimeout(t.Context(), extendedTestTimeout)
defer cancel()
fifoPath := fifotest.NewTestFIFO(t)
opts := []ProxyOpt{&ProxyConfig{
StreamIngressOptions: StreamIngressOptions{
InputFIFO: fifoPath,
},
}}
env, err := newProxyTestEnv(testCtx, t, opts...)
if err != nil {
t.Fatalf("newProxyTestEnv() failed in test case %q: %v", tc.name, err)
}
defer env.proxy.Shutdown()
// Ground the FIFO with KeepOpen to prevent premature reader EOF.
ko := fifotest.NewKeepOpen(t, fifoPath)
defer ko.Close()
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
if err := env.proxy.Run(testCtx); err != nil && err != context.Canceled {
t.Errorf("Proxy.Run() failed in test case %q: %v", tc.name, err)
}
}()
// Write requests to unblock the proxy.
go func() {
defer wg.Done()
// Open the writer and hand off from KeepOpen.
fw, err := ko.OpenWriter(t.Context())
if err != nil {
t.Errorf("failed to open FIFO for writing in test case %q: %v", tc.name, err)
return
}
defer fw.Close()
for _, req := range tc.requests {
if err := WriteUploadRequest(fw, req); err != nil {
t.Errorf("failed to write upload request in test case %q: %v", tc.name, err)
return
}
}
// After writing all requests, signal the proxy to finalize.
env.proxy.SignalFinalization()
}()
wg.Wait()
})
}
}
// TestProxy_ReadySignal verifies that the Proxy signals readiness on Ready()
// as soon as its internal ingress processing is initialized and ready for traffic.
func TestProxy_ReadySignal(t *testing.T) {
testCtx, cancel := context.WithTimeout(t.Context(), extendedTestTimeout)
defer cancel()
fifoPath := fifotest.NewTestFIFO(t)
opts := []ProxyOpt{&ProxyConfig{
StreamIngressOptions: StreamIngressOptions{
InputFIFO: fifoPath,
},
}}
env, err := newProxyTestEnv(testCtx, t, opts...)
if err != nil {
t.Fatalf("newProxyTestEnv() failed: %v", err)
}
defer env.proxy.Shutdown()
// Ground the FIFO with KeepOpen to prevent premature reader EOF.
ko := fifotest.NewKeepOpen(t, fifoPath)
defer ko.Close()
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
env.proxy.Run(testCtx)
}()
select {
case <-env.proxy.Ready():
// Success.
case <-testCtx.Done():
t.Fatal("Timeout waiting for ready signal")
}
// Unblock Run() by closing the writer end of the FIFO.
go func() {
defer wg.Done()
fw, err := ko.OpenWriter(t.Context())
if err != nil {
t.Errorf("failed to open FIFO for writing: %v", err)
return
}
fw.Close()
}()
wg.Wait()
}
// TestProxy_ReadyMetadata verifies that the metadata returned by Ready()
// contains the correct information for different ingress types.
func TestProxy_ReadyMetadata(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), extendedTestTimeout)
defer cancel()
t.Run("FIFO", func(t *testing.T) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
fifoPath := fifotest.NewTestFIFO(t)
p, err := NewProxy(ctx, &ProxyConfig{
StreamIngressOptions: StreamIngressOptions{
InputFIFO: fifoPath,
},
})
if err != nil {
t.Fatalf("NewProxy failed: %v", err)
}
defer p.Close(ctx)
go p.Run(ctx)
select {
case m := <-p.Ready():
if m.FIFOPath != fifoPath {
t.Errorf("FIFOPath = %q, want %q", m.FIFOPath, fifoPath)
}
if m.GRPCAddr != "" {
t.Errorf("GRPCAddr = %q, want empty", m.GRPCAddr)
}
case <-ctx.Done():
t.Fatal("Timeout waiting for ready signal")
}
})
t.Run("GRPC", func(t *testing.T) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
lis, err := net.Listen("tcp", netutil.DefaultLocalIPv4Addr)
if err != nil {
t.Fatalf("net.Listen failed: %v", err)
}
addr := lis.Addr().String()
p, err := NewProxy(ctx, &ProxyConfig{
IngressListener: lis,
})
if err != nil {
t.Fatalf("NewProxy failed: %v", err)
}
defer p.Close(ctx)
go p.Run(ctx)
select {
case m := <-p.Ready():
if m.GRPCAddr != addr {
t.Errorf("GRPCAddr = %q, want %q", m.GRPCAddr, addr)
}
if m.FIFOPath != "" {
t.Errorf("FIFOPath = %q, want empty", m.FIFOPath)
}
case <-ctx.Done():
t.Fatal("Timeout waiting for ready signal")
}
})
}
// TestProxy_SignalFinalization verifies that SignalFinalization() correctly
// notifies all internal workers to process post-build artifacts.
func TestProxy_SignalFinalization(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), extendedTestTimeout)
defer cancel()
env, err := newProxyTestEnv(ctx, t)
if err != nil {
t.Fatalf("newProxyTestEnv() failed: %v", err)
}
defer env.proxy.Shutdown()
env.proxy.SignalFinalization()
select {
case <-env.proxy.FinalizationSignal:
// Success.
case <-time.After(1 * time.Second):
t.Fatal("Timeout waiting for finalization signal")
}
}
// TestProxy_AddCleanup verifies that registered cleanup functions are executed
// during Proxy shutdown.
func TestProxy_AddCleanup(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), extendedTestTimeout)
defer cancel()
env, err := newProxyTestEnv(ctx, t)
if err != nil {
t.Fatalf("newProxyTestEnv() failed: %v", err)
}
cleanupCalled := false
env.proxy.AddCleanup(func() {
cleanupCalled = true
})
env.proxy.Shutdown()
if !cleanupCalled {
t.Error("Cleanup function was not called during shutdown")
}
}
// TestProxy_IngressDrainedSignal verifies that IngressDrainedChan is closed
// only after the ingress worker has finished and the dispatcher has emptied
// the inbound queue.
func TestProxy_IngressDrainedSignal(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), extendedTestTimeout)
defer cancel()
fifoPath := fifotest.NewTestFIFO(t)
// In Sink Mode, we don't need newProxyTestEnv's fakes.
// We use OpenBlocking=true to ensure precise synchronization.
opts := []ProxyOpt{
&ProxyConfig{
StreamIngressOptions: StreamIngressOptions{
InputFIFO: fifoPath,
OpenBlocking: true,
},
},
&DialParamsOpt{}, // Effectively disables standard egress.
}
var wg sync.WaitGroup
wg.Add(1)
writerClosed := make(chan struct{})
canCloseWriter := make(chan struct{})
// 1. Start background writer BEFORE creating the Proxy.
// NewProxy will call Setup(), which blocks until this writer connects.
req := rsmsg.NewInvocationUploadRequest(rspb.UploadRequest_CREATE, rsmsg.NewInvocation("drain-test", rsmsg.StatusUnspecified))
go func() {
defer wg.Done()
fw, err := fifo.OpenWriterContext(t.Context(), fifoPath)
if err != nil {
t.Errorf("failed to open FIFO for writing: %v", err)
return
}
if err := WriteUploadRequest(fw, req); err != nil {
t.Errorf("failed to write upload request: %v", err)
fw.Close()
return
}
// Keep the writer end open until the test signals we can close it.
<-canCloseWriter
fw.Close()
close(writerClosed)
}()
p, err := NewProxy(ctx, opts...)
if err != nil {
t.Fatalf("NewProxy() failed: %v", err)
}
defer p.Close(ctx)
wg.Add(1)
go func() {
defer wg.Done()
if err := p.Run(ctx); err != nil && err != context.Canceled {
t.Errorf("Proxy.Run() failed: %v", err)
}
}()
// Wait for the proxy to be ready.
select {
case <-p.Ready():
case <-ctx.Done():
t.Fatal("Timeout waiting for proxy to be ready")
}
// 2. The drained channel should still be open while the writer is connected.
select {
case <-p.IngressDrainedChan():
t.Fatal("IngressDrainedChan closed prematurely while writer was connected")
default:
// Success.
}
// 3. Now allow the writer to close, which should eventually close IngressDrainedChan.
close(canCloseWriter)
<-writerClosed
select {
case <-p.IngressDrainedChan():
// Success.
case <-ctx.Done():
t.Errorf("Timeout waiting for ingress drained signal after writer closed")
}
}
// TestProxy_MultiEgress verifies that the Proxy correctly initializes and
// orchestrates multiple egress workers simultaneously, ensuring that incoming
// events are fanned out to all of them.
func TestProxy_MultiEgress(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), extendedTestTimeout)
defer cancel()
tmpDir := t.TempDir()
dumpPath := filepath.Join(tmpDir, "dump.txt")
fifoPath := fifotest.NewTestFIFO(t)
// 1. Configure Proxy with multiple egresses: ResultStore and PrototextWriter.
opts := []ProxyOpt{&ProxyConfig{
ResultStoreEgressOptions: ResultStoreEgressOptions{
RSInstanceName: "projects/test/instances/default",
CASInstanceName: "projects/test/instances/default",
},
PrototextWriterOptions: PrototextWriterOptions{
Path: dumpPath,
},
StreamIngressOptions: StreamIngressOptions{
InputFIFO: fifoPath,
},
}}
env, err := newProxyTestEnv(ctx, t, opts...)
if err != nil {
t.Fatalf("newProxyTestEnv() failed: %v", err)
}
defer env.proxy.Shutdown()
// Ground the FIFO with KeepOpen to prevent premature reader EOF.
ko := fifotest.NewKeepOpen(t, fifoPath)
defer ko.Close()
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
if err := env.proxy.Run(ctx); err != nil && err != context.Canceled {
t.Errorf("Proxy.Run() failed: %v", err)
}
}()
// 2. Send build events.
req1 := rsmsg.NewInvocationUploadRequest(rspb.UploadRequest_CREATE,
rsmsg.NewInvocation("test-multi-egress", rsmsg.StatusUnspecified))
go func() {
defer wg.Done()
fw, err := ko.OpenWriter(t.Context())
if err != nil {
t.Errorf("failed to open FIFO for writing: %v", err)
return
}
defer fw.Close()
if err := WriteUploadRequest(fw, req1); err != nil {
t.Errorf("failed to write req1: %v", err)
}
env.proxy.SignalFinalization()
}()
// Wait for the proxy and writer goroutines to finish.
// Router.Run() ensures that all egress workers have finished their loops.
wg.Wait()
// 3. Verify ResultStore egress (via FakeResultStore).
invName := "invocations/test-multi-egress"
err = env.fakeRS.WaitUntil(ctx, func(data fakeresultstore.ServerData) bool {
return data.FinalState.Invocations[invName] != nil
})
if err != nil {
t.Errorf("Invocation %q was not created in ResultStore: %v", invName, err)
// Print all recorded errors to help diagnose flakiness.
for _, err := range env.proxy.Errors() {
t.Logf("Recorded error: %v", err)
}
}
// 4. Verify PrototextWriter egress (via dump file).
var dumpContent []byte
start := time.Now()
for time.Since(start) < fillTimeout {
dumpContent, _ = os.ReadFile(dumpPath)
if len(dumpContent) > 0 {
break
}
time.Sleep(pollInterval)
}
if len(dumpContent) == 0 {
t.Error("Dump file is empty; PrototextWriter failed to record requests")
}
}
// TestProxy_AutonomousFIFOIngress verifies that the Proxy can autonomously
// manage its own FIFO ingress (creation and cleanup).
func TestProxy_AutonomousFIFOIngress(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), extendedTestTimeout)
defer cancel()
// 1. Configure Proxy for autonomous temporary FIFO with non-blocking open.
// This matches the integrated mode configuration.
opts := []ProxyOpt{&ProxyConfig{
StreamIngressOptions: StreamIngressOptions{
IsTemp: true,
OpenBlocking: false,
},
}}
env, err := newProxyTestEnv(ctx, t, opts...)
if err != nil {
t.Fatalf("newProxyTestEnv() failed: %v", err)
}
// 3. Open the FIFO for writing using the Patient Writer.
// This ensures the writer end is connected before the proxy starts reading,
// preventing a premature EOF if the proxy starts faster than the writer.
m := <-env.proxy.Ready()
fifoPath := m.FIFOPath
if fifoPath == "" {
t.Fatal("Proxy.Ready() returned empty FIFOPath for autonomous FIFO")
}
// 2. Verify FIFO exists on disk.
if _, err := os.Stat(fifoPath); os.IsNotExist(err) {
t.Errorf("FIFO file %s was not created", fifoPath)
}
fw, err := fifo.OpenWriterContext(t.Context(), fifoPath)
if err != nil {
t.Fatalf("Failed to open FIFO for writing: %v", err)
}
defer fw.Close()
// 4. Start the proxy.
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
if err := env.proxy.Run(ctx); err != nil && err != context.Canceled {
t.Errorf("Proxy.Run() failed: %v", err)
}
}()
// 5. Send requests to the FIFO.
req := rsmsg.NewInvocationUploadRequest(rspb.UploadRequest_CREATE, rsmsg.NewInvocation("fifo-test", rsmsg.StatusUnspecified))
if err := WriteUploadRequest(fw, req); err != nil {
t.Errorf("Failed to write to FIFO: %v", err)
}
// 6. Finalize and verify data reached backend.
// We explicitly signal finalization then close the writer to trigger EOF.
env.proxy.SignalFinalization()
fw.Close()
wg.Wait()
invName := "invocations/fifo-test"
gotInvoc := env.fakeRS.Data().FinalState.Invocations[invName]
if gotInvoc == nil {
t.Errorf("ResultStore: Invocation %q not received via autonomous FIFO", invName)
}
// 7. Shutdown and verify cleanup.
env.proxy.Shutdown()
if _, err := os.Stat(fifoPath); err == nil {
t.Errorf("FIFO file %s was not removed after shutdown", fifoPath)
}
}
// TestNewProxy_SinkMode verifies that the Proxy can be initialized without
// any egress backend services, in which case it operates in "sink" mode
// (consuming events but doing nothing with them).
func TestNewProxy_SinkMode(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), extendedTestTimeout)
defer cancel()
// NewProxy should succeed even if no services are specified.
proxy, err := NewProxy(ctx, &DialParamsOpt{
RSDialParams: auth.DialParams{},
CASDialParams: auth.DialParams{},
})
if err != nil {
t.Fatalf("NewProxy() with no services failed: %v", err)
}
if proxy == nil {
t.Fatal("NewProxy() returned nil proxy")
}
defer proxy.Close(ctx)
if proxy.HasEgress() {
t.Error("proxy.HasEgress() expected false (Sink mode), but got true")
}
// NewProxy should succeed if at least JobStatus is specified, even if
// ResultStore and CAS are empty.
jsServer := fakejobstatus.New()
jsBackend := grpctest.NewTCPServer(t, func(s *grpc.Server) {
jspb.RegisterJobStatusServer(s, jsServer)
})
opts := []ProxyOpt{&ProxyConfig{
ResultStoreEgressOptions: ResultStoreEgressOptions{
CASInstanceName: "instance",
},
JobStatusEgressOptions: JobStatusEgressOptions{
JobStatusAddr: jsBackend.Addr(),
},
}}
proxyJS, err := NewProxy(ctx, append(opts, &DialParamsOpt{
JSDialParams: auth.DialParams{
Service: jsBackend.Addr(),
NoSecurity: true,
},
})...)
if err != nil {
t.Fatalf("NewProxy() with JobStatusAddr failed: %v", err)
}
if proxyJS == nil {
t.Fatal("NewProxy() returned nil proxy")
}
defer proxyJS.Close(ctx)
if !proxyJS.HasEgress() {
t.Error("proxyJS.HasEgress() expected true (JobStatus), but got false")
}
}
// TestProxy_AutonomousGRPCIngress verifies that the Proxy can manage its own
// gRPC ingress server when a listener is provided.
func TestProxy_AutonomousGRPCIngress(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), extendedTestTimeout)
defer cancel()
lis := bufconn.Listen(1024 * 1024)
// 1. Configure Proxy with gRPC ingress via bufconn.
opts := []ProxyOpt{&ProxyConfig{
IngressListener: lis,
}}
env, err := newProxyTestEnv(ctx, t, opts...)
if err != nil {
t.Fatalf("newProxyTestEnv() failed: %v", err)
}
var wg sync.WaitGroup
wg.Add(1)
proxyCtx, proxyCancel := context.WithCancel(ctx)
go func() {
defer wg.Done()
if err := env.proxy.Run(proxyCtx); err != nil && err != context.Canceled {
t.Errorf("Proxy.Run() failed: %v", err)
}
}()
// 2. Wait for the proxy (and its internal gRPC server) to be ready.
select {
case <-env.proxy.Ready():
case <-ctx.Done():
t.Fatal("Timeout waiting for proxy to be ready")
}
// 3. Connect to the autonomous ingress and send a request.
conn := grpctest.ConnectListener(ctx, t, lis)
defer conn.Close()
client := rspb.NewResultStoreUploadClient(conn)
invocID := "autonomous-grpc-test"
_, err = client.CreateInvocation(ctx, &rspb.CreateInvocationRequest{
InvocationId: invocID,
Invocation: &rspb.Invocation{
Id: &rspb.Invocation_Id{InvocationId: invocID},
},
})
if err != nil {
t.Fatalf("CreateInvocation failed: %v", err)
}
// 4. Verify request reached the fake backend.
err = env.fakeRS.WaitUntil(ctx, func(data fakeresultstore.ServerData) bool {
return len(data.CreateInvReqs) > 0
})
if err != nil {
t.Errorf("ResultStore: CreateInvocation not received via autonomous gRPC ingress: %v", err)
}
// 5. Finalize the session via gRPC.
_, err = client.FinalizeInvocation(ctx, &rspb.FinalizeInvocationRequest{
Name: "invocations/" + invocID,
})
if err != nil {
t.Fatalf("FinalizeInvocation failed: %v", err)
}
// 6. Cleanup: Close client connection first to unblock GracefulStop!
conn.Close()
// 7. Stop proxy.
proxyCancel()
wg.Wait()
}
// TestProxy_CloseUnstartedProxy verifies that Close() returns immediately and
// does not deadlock if Run() was never called.
func TestProxy_CloseUnstartedProxy(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), shortTestTimeout)
defer cancel()
env, err := newProxyTestEnv(ctx, t)
if err != nil {
t.Fatalf("newProxyTestEnv() failed: %v", err)
}
closeDone := make(chan error, 1)
go func() {
closeDone <- env.proxy.Close(ctx)
}()
select {
case err := <-closeDone:
if err != nil {
t.Errorf("Close() failed: %v", err)
}
case <-ctx.Done():
t.Fatal("Close() deadlocked on unstarted proxy")
}
}
// TestProxy_SignalIngressEOF verifies that the Proxy can unblock its ingress
// reader by signaling EOF to its FIFO.
func TestProxy_SignalIngressEOF(t *testing.T) {
ctx := t.Context()
fifoPath := fifotest.NewTestFIFO(t)
opts := []ProxyOpt{
&ProxyConfig{
StreamIngressOptions: StreamIngressOptions{
InputFIFO: fifoPath,
OpenBlocking: false,
},
},
}
p, err := NewProxy(ctx, opts...)
if err != nil {
t.Fatalf("NewProxy failed: %v", err)
}
defer p.Close(ctx)
if err := p.SignalIngressEOF(); err != nil {
t.Errorf("SignalIngressEOF failed: %v", err)
}
}
// TestProxy_HasEgress verifies that the Proxy correctly reports whether it
// has any registered egress workers.
func TestProxy_HasEgress(t *testing.T) {
ctx := t.Context()
// Verify that a Proxy with no workers reports false.
t.Run("NoEgress", func(t *testing.T) {
p, _ := NewProxy(ctx)
if p.HasEgress() {
t.Error("HasEgress() = true, want false")
}
})
// Verify that a Proxy with a configured Prototext worker reports true.
t.Run("WithEgress", func(t *testing.T) {
p, _ := NewProxy(ctx, &ProxyConfig{
PrototextWriterOptions: PrototextWriterOptions{
Path: filepath.Join(t.TempDir(), "dump.txt"),
},
})
if !p.HasEgress() {
t.Error("HasEgress() = false, want true")
}
})
}