package server import ( "context" "fmt" "math/rand" //nolint: gosec "testing" "time" "cloud.google.com/go/pubsub" "cloud.google.com/go/pubsub/pstest" "github.com/stretchr/testify/require" "google.golang.org/api/option" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) // TestDiffReceiver is a unit test that directly calls and validates the `ReceiverMux.diffReceivers` function. // The "pubsub" and "pstest" packages are only used to call client.SubscriptionInProject which is just a thin wrapper. func TestDiffReceiver(t *testing.T) { var srv = pstest.NewServer() defer srv.Close() var grpcOpt = grpc.WithTransportCredentials(insecure.NewCredentials()) conn, err := grpc.NewClient(srv.Addr, grpcOpt) if err != nil { t.Fatal(err) } defer conn.Close() var ctx, cancel = context.WithCancel(context.Background()) defer cancel() client, err := pubsub.NewClient(ctx, "project", option.WithGRPCConn(conn)) if err != nil { t.Fatal(err) } defer client.Close() var rm = &ReceiverMux{ // these values are passed into NewReceiver and don't need to be real. cfg: &ReceiverMuxConfig{ Handler: func(_ context.Context, _ *pubsub.Message) error { return nil }, PollSubscriptionExistsPeriod: time.Hour, SubscriptionID: "fake", }, client: client, receivers: make(map[string]*Receiver), } var dtcs = []*DiffTestCase{ // No Change NewDiffTestCase(t, 0, 0, 0), NewDiffTestCase(t, 1, 0, 0), NewDiffTestCase(t, 50, 0, 0), // Found New Banner NewDiffTestCase(t, 0, 1, 0), NewDiffTestCase(t, 1, 1, 0), NewDiffTestCase(t, 20, 1, 0), // Found New Banners NewDiffTestCase(t, 0, 10, 0), NewDiffTestCase(t, 1, 20, 0), NewDiffTestCase(t, 20, 20, 0), // Dropped Existing Banner NewDiffTestCase(t, 1, 0, 1), NewDiffTestCase(t, 2, 0, 1), NewDiffTestCase(t, 20, 0, 1), // Dropped Existing Banners NewDiffTestCase(t, 2, 0, 2), NewDiffTestCase(t, 10, 0, 10), NewDiffTestCase(t, 20, 0, 10), // Found New Banner And Dropped Existing Banner NewDiffTestCase(t, 1, 1, 1), NewDiffTestCase(t, 10, 1, 1), // Found New Banners And Dropped Existing Banners NewDiffTestCase(t, 2, 2, 2), NewDiffTestCase(t, 10, 5, 5), NewDiffTestCase(t, 10, 2, 7), NewDiffTestCase(t, 10, 7, 2), } for i, dtc := range dtcs { t.Logf("Test case %d: current=%d added=%d, dropped=%d", i, len(dtc.Before), len(dtc.Added), len(dtc.Dropped)) dtc.Test(t, rm) } } type DiffTestCase struct { Added map[string]bool Dropped map[string]bool Before map[string]bool Polled []string // Implementation details: // The diff function immediately adds to the `rm.receivers` map, but it does not remove from it. // So the final state should equal the Before banners plus the Added banners. // Another way to state this is the final state should equal the Polled banners plus the Dropped banners. After map[string]bool } func (dtc *DiffTestCase) Test(t *testing.T, rm *ReceiverMux) { // reset the receivers map for each test case rm.receivers = make(map[string]*Receiver) for b := range dtc.Before { rm.receivers[b] = &Receiver{ projectID: b, } } require.Equal(t, len(dtc.Before), len(rm.receivers)) // diffReceivers var added, dropped = rm.diffReceivers(dtc.Polled...) require.Equal(t, len(added), len(dtc.Added)) for _, r := range added { require.True(t, dtc.Added[r.projectID]) } require.Equal(t, len(dropped), len(dtc.Dropped)) for _, r := range dropped { require.True(t, dtc.Dropped[r.projectID]) } require.Equal(t, len(rm.receivers), len(dtc.After)) // redundant assertions to help you understand how the receiver mux's diff function acts. require.Equal(t, len(rm.receivers), len(dtc.Before)+len(dtc.Added)) require.Equal(t, len(rm.receivers), len(dtc.Polled)+len(dtc.Dropped)) for _, r := range rm.receivers { require.True(t, dtc.After[r.projectID]) } } func NewDiffTestCase(t *testing.T, current, add, drop int) *DiffTestCase { if drop > current { panic("current must be greater than the amount dropped") } var dtc = DiffTestCase{ Added: make(map[string]bool), Dropped: make(map[string]bool), Before: make(map[string]bool), After: make(map[string]bool), } for range add { var b = fmt.Sprintf("b%x", rand.Int63()) //nolint: gosec dtc.Added[b] = true dtc.After[b] = true dtc.Polled = append(dtc.Polled, b) } var dropped = make(map[int]bool) var perm = rand.Perm(current) //nolint: gosec for _, j := range perm[:drop] { dropped[j] = true } for i := range current { var b = fmt.Sprintf("b%x", rand.Int63()) //nolint: gosec dtc.Before[b] = true dtc.After[b] = true if dropped[i] { dtc.Dropped[b] = true } else { dtc.Polled = append(dtc.Polled, b) } } // sanity checks require.Equal(t, current, len(dtc.Before)) require.Equal(t, add, len(dtc.Added)) require.Equal(t, drop, len(dtc.Dropped)) require.Equal(t, current+add, len(dtc.After)) require.Equal(t, current+add-drop, len(dtc.Polled)) for dropped := range dtc.Dropped { require.True(t, dtc.After[dropped]) require.True(t, dtc.Before[dropped]) require.False(t, dtc.Added[dropped]) } for added := range dtc.Added { require.True(t, dtc.After[added]) require.False(t, dtc.Before[added]) require.False(t, dtc.Dropped[added]) } for _, polled := range dtc.Polled { require.True(t, dtc.After[polled]) require.True(t, dtc.Added[polled] || dtc.Before[polled]) require.False(t, dtc.Dropped[polled]) } return &dtc }