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 }