github.com/thiagoyeds/go-cloud@v0.26.0/pubsub/pubsub_test.go (about)

     1  // Copyright 2018 The Go Cloud Development Kit Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  package pubsub
    15  
    16  import (
    17  	"context"
    18  	"errors"
    19  	"fmt"
    20  	"net/url"
    21  	"strings"
    22  	"sync"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/google/go-cmp/cmp"
    27  	"gocloud.dev/gcerrors"
    28  	"gocloud.dev/internal/gcerr"
    29  	"gocloud.dev/internal/testing/octest"
    30  	"gocloud.dev/pubsub/batcher"
    31  	"gocloud.dev/pubsub/driver"
    32  )
    33  
    34  type driverTopic struct {
    35  	driver.Topic
    36  	subs []*driverSub
    37  }
    38  
    39  func (t *driverTopic) SendBatch(ctx context.Context, ms []*driver.Message) error {
    40  	for _, s := range t.subs {
    41  		select {
    42  		case <-s.sem:
    43  			s.q = append(s.q, ms...)
    44  			s.sem <- struct{}{}
    45  		case <-ctx.Done():
    46  			return ctx.Err()
    47  		}
    48  	}
    49  	return nil
    50  }
    51  
    52  func (*driverTopic) IsRetryable(error) bool             { return false }
    53  func (*driverTopic) ErrorCode(error) gcerrors.ErrorCode { return gcerrors.Unknown }
    54  func (*driverTopic) Close() error                       { return nil }
    55  
    56  type driverSub struct {
    57  	driver.Subscription
    58  	sem chan struct{}
    59  	// Normally this queue would live on a separate server in the cloud.
    60  	q []*driver.Message
    61  }
    62  
    63  func NewDriverSub() *driverSub {
    64  	ds := &driverSub{
    65  		sem: make(chan struct{}, 1),
    66  	}
    67  	ds.sem <- struct{}{}
    68  	return ds
    69  }
    70  
    71  func (s *driverSub) ReceiveBatch(ctx context.Context, maxMessages int) ([]*driver.Message, error) {
    72  	for {
    73  		select {
    74  		case <-s.sem:
    75  			ms := s.grabQueue(maxMessages)
    76  			if len(ms) != 0 {
    77  				return ms, nil
    78  			}
    79  		case <-ctx.Done():
    80  			return nil, ctx.Err()
    81  		default:
    82  		}
    83  	}
    84  }
    85  
    86  func (s *driverSub) grabQueue(maxMessages int) []*driver.Message {
    87  	defer func() { s.sem <- struct{}{} }()
    88  	if len(s.q) > 0 {
    89  		if len(s.q) <= maxMessages {
    90  			ms := s.q
    91  			s.q = nil
    92  			return ms
    93  		}
    94  		ms := s.q[:maxMessages]
    95  		s.q = s.q[maxMessages:]
    96  		return ms
    97  	}
    98  	return nil
    99  }
   100  
   101  func (s *driverSub) SendAcks(ctx context.Context, ackIDs []driver.AckID) error {
   102  	return nil
   103  }
   104  
   105  func (*driverSub) IsRetryable(error) bool             { return false }
   106  func (*driverSub) ErrorCode(error) gcerrors.ErrorCode { return gcerrors.Internal }
   107  func (*driverSub) CanNack() bool                      { return false }
   108  func (*driverSub) Close() error                       { return nil }
   109  
   110  func TestSendReceive(t *testing.T) {
   111  	ctx := context.Background()
   112  	ds := NewDriverSub()
   113  	dt := &driverTopic{
   114  		subs: []*driverSub{ds},
   115  	}
   116  	topic := NewTopic(dt, nil)
   117  	defer topic.Shutdown(ctx)
   118  	m := &Message{LoggableID: "foo", Body: []byte("user signed up")}
   119  	if err := topic.Send(ctx, m); err == nil {
   120  		t.Fatalf("expected a Send with a non-empty LoggableID to fail")
   121  	}
   122  	m.LoggableID = ""
   123  	if err := topic.Send(ctx, m); err != nil {
   124  		t.Fatal(err)
   125  	}
   126  
   127  	sub := NewSubscription(ds, nil, nil)
   128  	defer sub.Shutdown(ctx)
   129  	m2, err := sub.Receive(ctx)
   130  	if err != nil {
   131  		t.Fatal(err)
   132  	}
   133  	if string(m2.Body) != string(m.Body) {
   134  		t.Fatalf("received message has body %q, want %q", m2.Body, m.Body)
   135  	}
   136  	m2.Ack()
   137  }
   138  
   139  func TestConcurrentReceivesGetAllTheMessages(t *testing.T) {
   140  	howManyToSend := int(1e3)
   141  	ctx, cancel := context.WithCancel(context.Background())
   142  	dt := &driverTopic{}
   143  
   144  	// wg is used to wait until all messages are received.
   145  	var wg sync.WaitGroup
   146  	wg.Add(howManyToSend)
   147  
   148  	// Make a subscription.
   149  	ds := NewDriverSub()
   150  	dt.subs = append(dt.subs, ds)
   151  	s := NewSubscription(ds, nil, nil)
   152  	defer s.Shutdown(ctx)
   153  
   154  	// Start 10 goroutines to receive from it.
   155  	var mu sync.Mutex
   156  	receivedMsgs := make(map[string]bool)
   157  	for i := 0; i < 10; i++ {
   158  		go func() {
   159  			for {
   160  				m, err := s.Receive(ctx)
   161  				if err != nil {
   162  					// Permanent error; ctx cancelled or subscription closed is
   163  					// expected once we've received all the messages.
   164  					mu.Lock()
   165  					n := len(receivedMsgs)
   166  					mu.Unlock()
   167  					if n != howManyToSend {
   168  						t.Errorf("Worker's Receive failed before all messages were received (%d)", n)
   169  					}
   170  					return
   171  				}
   172  				m.Ack()
   173  				mu.Lock()
   174  				receivedMsgs[string(m.Body)] = true
   175  				mu.Unlock()
   176  				wg.Done()
   177  			}
   178  		}()
   179  	}
   180  
   181  	// Send messages. Each message has a unique body used as a key to receivedMsgs.
   182  	topic := NewTopic(dt, nil)
   183  	defer topic.Shutdown(ctx)
   184  	for i := 0; i < howManyToSend; i++ {
   185  		key := fmt.Sprintf("message #%d", i)
   186  		m := &Message{Body: []byte(key)}
   187  		if err := topic.Send(ctx, m); err != nil {
   188  			t.Fatal(err)
   189  		}
   190  	}
   191  
   192  	// Wait for the goroutines to receive all of the messages, then cancel the
   193  	// ctx so they all exit.
   194  	wg.Wait()
   195  	defer cancel()
   196  
   197  	// Check that all the messages were received.
   198  	for i := 0; i < howManyToSend; i++ {
   199  		key := fmt.Sprintf("message #%d", i)
   200  		if !receivedMsgs[key] {
   201  			t.Errorf("message %q was not received", key)
   202  		}
   203  	}
   204  }
   205  
   206  func TestCancelSend(t *testing.T) {
   207  	ctx, cancel := context.WithCancel(context.Background())
   208  	ds := NewDriverSub()
   209  	dt := &driverTopic{
   210  		subs: []*driverSub{ds},
   211  	}
   212  	topic := NewTopic(dt, nil)
   213  	defer topic.Shutdown(ctx)
   214  	m := &Message{}
   215  
   216  	// Intentionally break the driver subscription by acquiring its semaphore.
   217  	// Now topic.Send will have to wait for cancellation.
   218  	<-ds.sem
   219  
   220  	cancel()
   221  	if err := topic.Send(ctx, m); err == nil {
   222  		t.Error("got nil, want cancellation error")
   223  	}
   224  }
   225  
   226  func TestCancelReceive(t *testing.T) {
   227  	ctx, cancel := context.WithCancel(context.Background())
   228  	ds := NewDriverSub()
   229  	s := NewSubscription(ds, nil, nil)
   230  	defer s.Shutdown(ctx)
   231  	cancel()
   232  	// Without cancellation, this Receive would hang.
   233  	if _, err := s.Receive(ctx); err == nil {
   234  		t.Error("got nil, want cancellation error")
   235  	}
   236  }
   237  
   238  type blockingDriverSub struct {
   239  	driver.Subscription
   240  	inReceiveBatch chan struct{}
   241  }
   242  
   243  func (b blockingDriverSub) ReceiveBatch(ctx context.Context, maxMessages int) ([]*driver.Message, error) {
   244  	b.inReceiveBatch <- struct{}{}
   245  	<-ctx.Done()
   246  	return nil, ctx.Err()
   247  }
   248  func (blockingDriverSub) CanNack() bool          { return false }
   249  func (blockingDriverSub) IsRetryable(error) bool { return false }
   250  func (blockingDriverSub) Close() error           { return nil }
   251  
   252  func TestCancelTwoReceives(t *testing.T) {
   253  	// We want to create the following situation:
   254  	// 1. Goroutine 1 calls Receive, obtains the lock (Subscription.mu),
   255  	//    then releases the lock and calls driver.ReceiveBatch, which hangs.
   256  	// 2. Goroutine 2 calls Receive.
   257  	// 3. The context passed to the Goroutine 2 call is canceled.
   258  	// We expect Goroutine 2's Receive to exit immediately. That won't
   259  	// happen if Receive holds the lock during the call to ReceiveBatch.
   260  	inReceiveBatch := make(chan struct{})
   261  	s := NewSubscription(blockingDriverSub{inReceiveBatch: inReceiveBatch}, nil, nil)
   262  	defer s.Shutdown(context.Background())
   263  	go func() {
   264  		_, err := s.Receive(context.Background())
   265  		// This should happen at the very end of the test, during Shutdown.
   266  		if err != context.Canceled {
   267  			t.Errorf("got %v, want context.Canceled", err)
   268  		}
   269  	}()
   270  	<-inReceiveBatch
   271  	// Give the Receive call time to block on the mutex before timing out.
   272  	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
   273  	defer cancel()
   274  	errc := make(chan error)
   275  	go func() {
   276  		_, err := s.Receive(ctx)
   277  		errc <- err
   278  	}()
   279  	err := <-errc
   280  	if err != context.DeadlineExceeded {
   281  		t.Errorf("got %v, want context.DeadlineExceeded", err)
   282  	}
   283  }
   284  
   285  func TestRetryTopic(t *testing.T) {
   286  	// Test that Send is retried if the driver returns a retryable error.
   287  	ctx := context.Background()
   288  	ft := &failTopic{}
   289  	topic := NewTopic(ft, nil)
   290  	defer topic.Shutdown(ctx)
   291  	err := topic.Send(ctx, &Message{})
   292  	if err != nil {
   293  		t.Errorf("Send: got %v, want nil", err)
   294  	}
   295  	if got, want := ft.calls, nRetryCalls+1; got != want {
   296  		t.Errorf("calls: got %d, want %d", got, want)
   297  	}
   298  }
   299  
   300  var errRetry = errors.New("retry")
   301  
   302  func isRetryable(err error) bool {
   303  	return err == errRetry
   304  }
   305  
   306  const nRetryCalls = 2
   307  
   308  // failTopic helps test retries for SendBatch.
   309  //
   310  // SendBatch will fail nRetryCall times before succeeding.
   311  type failTopic struct {
   312  	driver.Topic
   313  	calls int
   314  }
   315  
   316  func (t *failTopic) SendBatch(ctx context.Context, ms []*driver.Message) error {
   317  	t.calls++
   318  	if t.calls <= nRetryCalls {
   319  		return errRetry
   320  	}
   321  	return nil
   322  }
   323  
   324  func (*failTopic) IsRetryable(err error) bool         { return isRetryable(err) }
   325  func (*failTopic) ErrorCode(error) gcerrors.ErrorCode { return gcerrors.Unknown }
   326  func (*failTopic) Close() error                       { return nil }
   327  
   328  func TestRetryReceive(t *testing.T) {
   329  	ctx := context.Background()
   330  	fs := &failSub{fail: true}
   331  	sub := NewSubscription(fs, nil, nil)
   332  	defer sub.Shutdown(ctx)
   333  	m, err := sub.Receive(ctx)
   334  	if err != nil {
   335  		t.Fatalf("Receive: got %v, want nil", err)
   336  	}
   337  	m.Ack()
   338  	if got, want := fs.calls, nRetryCalls+1; got != want {
   339  		t.Errorf("calls: got %d, want %d", got, want)
   340  	}
   341  }
   342  
   343  // TestBatchSizeDecay verifies that the batch size decays when no messages are available.
   344  // (see https://github.com/google/go-cloud/issues/2849).
   345  func TestBatchSizeDecays(t *testing.T) {
   346  	ctx := context.Background()
   347  	fs := &failSub{}
   348  	// Allow multiple handlers and cap max batch size to ensure we get concurrency.
   349  	sub := NewSubscription(fs, &batcher.Options{MaxHandlers: 10, MaxBatchSize: 2}, nil)
   350  	defer sub.Shutdown(ctx)
   351  
   352  	// Records the last batch size.
   353  	var mu sync.Mutex
   354  	lastMaxMessages := 0
   355  	sub.preReceiveBatchHook = func(maxMessages int) {
   356  		mu.Lock()
   357  		defer mu.Unlock()
   358  		lastMaxMessages = maxMessages
   359  	}
   360  
   361  	// Do some receives to allow the number of batches to increase past 1.
   362  	for n := 0; n < 100; n++ {
   363  		m, err := sub.Receive(ctx)
   364  		if err != nil {
   365  			t.Fatalf("Receive: got %v, want nil", err)
   366  		}
   367  		m.Ack()
   368  	}
   369  
   370  	// Tell the failSub to start returning no messages.
   371  	fs.mu.Lock()
   372  	fs.empty = true
   373  	fs.mu.Unlock()
   374  
   375  	mu.Lock()
   376  	highWaterMarkBatchSize := lastMaxMessages
   377  	if lastMaxMessages <= 1 {
   378  		t.Fatal("max messages wasn't greater than 1")
   379  	}
   380  	mu.Unlock()
   381  
   382  	// Make a bunch of calls to Receive to drain any outstanding
   383  	// messages, and wait some extra time during which we should
   384  	// continue polling, and the batch size should decay.
   385  	for {
   386  		ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
   387  		defer cancel()
   388  		m, err := sub.Receive(ctx)
   389  		if err != nil {
   390  			// Expected: no more messages, and timed out.
   391  			break
   392  		}
   393  		// Drained a message.
   394  		m.Ack()
   395  	}
   396  
   397  	// Verify that the batch size decayed.
   398  	mu.Lock()
   399  	if lastMaxMessages >= highWaterMarkBatchSize {
   400  		t.Fatalf("wanted batch size to decay; high water mark was %d, now %d", highWaterMarkBatchSize, lastMaxMessages)
   401  	}
   402  	mu.Unlock()
   403  }
   404  
   405  // TestRetryReceiveBatches verifies that batching and retries work without races
   406  // (see https://github.com/google/go-cloud/issues/2676).
   407  func TestRetryReceiveInBatchesDoesntRace(t *testing.T) {
   408  	ctx := context.Background()
   409  	fs := &failSub{}
   410  	// Allow multiple handlers and cap max batch size to ensure we get concurrency.
   411  	sub := NewSubscription(fs, &batcher.Options{MaxHandlers: 10, MaxBatchSize: 2}, nil)
   412  	defer sub.Shutdown(ctx)
   413  
   414  	// Do some receives to allow the number of batches to increase past 1.
   415  	for n := 0; n < 100; n++ {
   416  		m, err := sub.Receive(ctx)
   417  		if err != nil {
   418  			t.Fatalf("Receive: got %v, want nil", err)
   419  		}
   420  		m.Ack()
   421  	}
   422  	// Tell the failSub to start failing.
   423  	fs.mu.Lock()
   424  	fs.fail = true
   425  	fs.mu.Unlock()
   426  
   427  	// This call to Receive should result in nRetryCalls+1 calls to ReceiveBatch for
   428  	// each batch. In the issue noted above, this would cause a race.
   429  	for n := 0; n < 100; n++ {
   430  		m, err := sub.Receive(ctx)
   431  		if err != nil {
   432  			t.Fatalf("Receive: got %v, want nil", err)
   433  		}
   434  		m.Ack()
   435  	}
   436  	// Don't try to verify the exact number of calls, as it is unpredictable
   437  	// based on the timing of the batching.
   438  }
   439  
   440  // failSub helps test retries for ReceiveBatch.
   441  //
   442  // Once start=true, ReceiveBatch will fail nRetryCalls times before succeeding.
   443  type failSub struct {
   444  	driver.Subscription
   445  	fail  bool
   446  	empty bool
   447  	calls int
   448  	mu    sync.Mutex
   449  }
   450  
   451  func (t *failSub) ReceiveBatch(ctx context.Context, maxMessages int) ([]*driver.Message, error) {
   452  	t.mu.Lock()
   453  	defer t.mu.Unlock()
   454  	if t.fail {
   455  		t.calls++
   456  		if t.calls <= nRetryCalls {
   457  			return nil, errRetry
   458  		}
   459  	}
   460  	if t.empty {
   461  		t.calls++
   462  		return nil, nil
   463  	}
   464  	return []*driver.Message{{Body: []byte("")}}, nil
   465  }
   466  
   467  func (*failSub) SendAcks(ctx context.Context, ackIDs []driver.AckID) error { return nil }
   468  func (*failSub) IsRetryable(err error) bool                                { return isRetryable(err) }
   469  func (*failSub) CanNack() bool                                             { return false }
   470  func (*failSub) Close() error                                              { return nil }
   471  
   472  // TODO(jba): add a test for retry of SendAcks.
   473  
   474  var errDriver = errors.New("driver error")
   475  
   476  type erroringTopic struct {
   477  	driver.Topic
   478  }
   479  
   480  func (erroringTopic) SendBatch(context.Context, []*driver.Message) error { return errDriver }
   481  func (erroringTopic) IsRetryable(err error) bool                         { return isRetryable(err) }
   482  func (erroringTopic) ErrorCode(error) gcerrors.ErrorCode                 { return gcerrors.AlreadyExists }
   483  func (erroringTopic) Close() error                                       { return errDriver }
   484  
   485  type erroringSubscription struct {
   486  	driver.Subscription
   487  }
   488  
   489  func (erroringSubscription) ReceiveBatch(context.Context, int) ([]*driver.Message, error) {
   490  	return nil, errDriver
   491  }
   492  
   493  func (erroringSubscription) SendAcks(context.Context, []driver.AckID) error { return errDriver }
   494  func (erroringSubscription) IsRetryable(err error) bool                     { return isRetryable(err) }
   495  func (erroringSubscription) ErrorCode(error) gcerrors.ErrorCode             { return gcerrors.AlreadyExists }
   496  func (erroringSubscription) CanNack() bool                                  { return false }
   497  func (erroringSubscription) Close() error                                   { return errDriver }
   498  
   499  // TestErrorsAreWrapped tests that all errors returned from the driver are
   500  // wrapped exactly once by the portable type.
   501  func TestErrorsAreWrapped(t *testing.T) {
   502  	ctx := context.Background()
   503  
   504  	verify := func(err error) {
   505  		t.Helper()
   506  		if err == nil {
   507  			t.Errorf("got nil error, wanted non-nil")
   508  			return
   509  		}
   510  		if e, ok := err.(*gcerr.Error); !ok {
   511  			t.Errorf("not wrapped: %v", err)
   512  		} else if got := e.Unwrap(); got != errDriver {
   513  			t.Errorf("got %v for wrapped error, not errDriver", got)
   514  		}
   515  		if s := err.Error(); !strings.HasPrefix(s, "pubsub ") {
   516  			t.Errorf("Error() for wrapped error doesn't start with 'pubsub': prefix: %s", s)
   517  		}
   518  	}
   519  
   520  	topic := NewTopic(erroringTopic{}, nil)
   521  	verify(topic.Send(ctx, &Message{}))
   522  	err := topic.Shutdown(ctx)
   523  	verify(err)
   524  
   525  	sub := NewSubscription(erroringSubscription{}, nil, nil)
   526  	_, err = sub.Receive(ctx)
   527  	verify(err)
   528  	err = sub.Shutdown(ctx)
   529  	verify(err)
   530  }
   531  
   532  func TestOpenCensus(t *testing.T) {
   533  	ctx := context.Background()
   534  	te := octest.NewTestExporter(OpenCensusViews)
   535  	defer te.Unregister()
   536  
   537  	ds := NewDriverSub()
   538  	dt := &driverTopic{
   539  		subs: []*driverSub{ds},
   540  	}
   541  	topic := NewTopic(dt, nil)
   542  	defer topic.Shutdown(ctx)
   543  	sub := NewSubscription(ds, nil, nil)
   544  	defer sub.Shutdown(ctx)
   545  	if err := topic.Send(ctx, &Message{Body: []byte("x")}); err != nil {
   546  		t.Fatal(err)
   547  	}
   548  	if err := topic.Shutdown(ctx); err != nil {
   549  		t.Fatal(err)
   550  	}
   551  	msg, err := sub.Receive(ctx)
   552  	if err != nil {
   553  		t.Fatal(err)
   554  	}
   555  	msg.Ack()
   556  	if err := sub.Shutdown(ctx); err != nil {
   557  		t.Fatal(err)
   558  	}
   559  	_, _ = sub.Receive(ctx)
   560  
   561  	diff := octest.Diff(te.Spans(), te.Counts(), "gocloud.dev/pubsub", "gocloud.dev/pubsub", []octest.Call{
   562  		{Method: "driver.Topic.SendBatch", Code: gcerrors.OK},
   563  		{Method: "Topic.Send", Code: gcerrors.OK},
   564  		{Method: "Topic.Shutdown", Code: gcerrors.OK},
   565  		{Method: "driver.Subscription.ReceiveBatch", Code: gcerrors.OK},
   566  		{Method: "Subscription.Receive", Code: gcerrors.OK},
   567  		{Method: "driver.Subscription.SendAcks", Code: gcerrors.OK},
   568  		{Method: "Subscription.Shutdown", Code: gcerrors.OK},
   569  		{Method: "Subscription.Receive", Code: gcerrors.FailedPrecondition},
   570  	})
   571  	if diff != "" {
   572  		t.Error(diff)
   573  	}
   574  }
   575  
   576  var (
   577  	testOpenOnce sync.Once
   578  	testOpenGot  *url.URL
   579  )
   580  
   581  func TestURLMux(t *testing.T) {
   582  	ctx := context.Background()
   583  
   584  	mux := new(URLMux)
   585  	fake := &fakeOpener{}
   586  	mux.RegisterTopic("foo", fake)
   587  	mux.RegisterTopic("err", fake)
   588  	mux.RegisterSubscription("foo", fake)
   589  	mux.RegisterSubscription("err", fake)
   590  
   591  	if diff := cmp.Diff(mux.TopicSchemes(), []string{"err", "foo"}); diff != "" {
   592  		t.Errorf("Schemes: %s", diff)
   593  	}
   594  	if !mux.ValidTopicScheme("foo") || !mux.ValidTopicScheme("err") {
   595  		t.Errorf("ValidTopicScheme didn't return true for valid scheme")
   596  	}
   597  	if mux.ValidTopicScheme("foo2") || mux.ValidTopicScheme("http") {
   598  		t.Errorf("ValidTopicScheme didn't return false for invalid scheme")
   599  	}
   600  
   601  	if diff := cmp.Diff(mux.SubscriptionSchemes(), []string{"err", "foo"}); diff != "" {
   602  		t.Errorf("Schemes: %s", diff)
   603  	}
   604  	if !mux.ValidSubscriptionScheme("foo") || !mux.ValidSubscriptionScheme("err") {
   605  		t.Errorf("ValidSubscriptionScheme didn't return true for valid scheme")
   606  	}
   607  	if mux.ValidSubscriptionScheme("foo2") || mux.ValidSubscriptionScheme("http") {
   608  		t.Errorf("ValidSubscriptionScheme didn't return false for invalid scheme")
   609  	}
   610  
   611  	for _, tc := range []struct {
   612  		name    string
   613  		url     string
   614  		wantErr bool
   615  	}{
   616  		{
   617  			name:    "empty URL",
   618  			wantErr: true,
   619  		},
   620  		{
   621  			name:    "invalid URL",
   622  			url:     ":foo",
   623  			wantErr: true,
   624  		},
   625  		{
   626  			name:    "invalid URL no scheme",
   627  			url:     "foo",
   628  			wantErr: true,
   629  		},
   630  		{
   631  			name:    "unregistered scheme",
   632  			url:     "bar://myps",
   633  			wantErr: true,
   634  		},
   635  		{
   636  			name:    "func returns error",
   637  			url:     "err://myps",
   638  			wantErr: true,
   639  		},
   640  		{
   641  			name: "no query options",
   642  			url:  "foo://myps",
   643  		},
   644  		{
   645  			name: "empty query options",
   646  			url:  "foo://myps?",
   647  		},
   648  		{
   649  			name: "query options",
   650  			url:  "foo://myps?aAa=bBb&cCc=dDd",
   651  		},
   652  		{
   653  			name: "multiple query options",
   654  			url:  "foo://myps?x=a&x=b&x=c",
   655  		},
   656  		{
   657  			name: "fancy ps name",
   658  			url:  "foo:///foo/bar/baz",
   659  		},
   660  		{
   661  			name: "using api schema prefix",
   662  			url:  "pubsub+foo://foo",
   663  		},
   664  	} {
   665  		t.Run("topic: "+tc.name, func(t *testing.T) {
   666  			_, gotErr := mux.OpenTopic(ctx, tc.url)
   667  			if (gotErr != nil) != tc.wantErr {
   668  				t.Fatalf("got err %v, want error %v", gotErr, tc.wantErr)
   669  			}
   670  			if gotErr != nil {
   671  				return
   672  			}
   673  			if got := fake.u.String(); got != tc.url {
   674  				t.Errorf("got %q want %q", got, tc.url)
   675  			}
   676  			// Repeat with OpenTopicURL.
   677  			parsed, err := url.Parse(tc.url)
   678  			if err != nil {
   679  				t.Fatal(err)
   680  			}
   681  			_, gotErr = mux.OpenTopicURL(ctx, parsed)
   682  			if gotErr != nil {
   683  				t.Fatalf("got err %v, want nil", gotErr)
   684  			}
   685  			if got := fake.u.String(); got != tc.url {
   686  				t.Errorf("got %q want %q", got, tc.url)
   687  			}
   688  		})
   689  		t.Run("subscription: "+tc.name, func(t *testing.T) {
   690  			_, gotErr := mux.OpenSubscription(ctx, tc.url)
   691  			if (gotErr != nil) != tc.wantErr {
   692  				t.Fatalf("got err %v, want error %v", gotErr, tc.wantErr)
   693  			}
   694  			if gotErr != nil {
   695  				return
   696  			}
   697  			if got := fake.u.String(); got != tc.url {
   698  				t.Errorf("got %q want %q", got, tc.url)
   699  			}
   700  			// Repeat with OpenSubscriptionURL.
   701  			parsed, err := url.Parse(tc.url)
   702  			if err != nil {
   703  				t.Fatal(err)
   704  			}
   705  			_, gotErr = mux.OpenSubscriptionURL(ctx, parsed)
   706  			if gotErr != nil {
   707  				t.Fatalf("got err %v, want nil", gotErr)
   708  			}
   709  			if got := fake.u.String(); got != tc.url {
   710  				t.Errorf("got %q want %q", got, tc.url)
   711  			}
   712  		})
   713  	}
   714  }
   715  
   716  type fakeOpener struct {
   717  	u *url.URL // last url passed to OpenTopicURL/OpenSubscriptionURL
   718  }
   719  
   720  func (o *fakeOpener) OpenTopicURL(ctx context.Context, u *url.URL) (*Topic, error) {
   721  	if u.Scheme == "err" {
   722  		return nil, errors.New("fail")
   723  	}
   724  	o.u = u
   725  	return nil, nil
   726  }
   727  
   728  func (o *fakeOpener) OpenSubscriptionURL(ctx context.Context, u *url.URL) (*Subscription, error) {
   729  	if u.Scheme == "err" {
   730  		return nil, errors.New("fail")
   731  	}
   732  	o.u = u
   733  	return nil, nil
   734  }