client_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  1. // Copyright 2015 CoreOS, Inc.
  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. // http://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 client
  15. import (
  16. "errors"
  17. "io"
  18. "io/ioutil"
  19. "net/http"
  20. "net/url"
  21. "reflect"
  22. "strings"
  23. "testing"
  24. "time"
  25. "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
  26. )
  27. type actionAssertingHTTPClient struct {
  28. t *testing.T
  29. num int
  30. act httpAction
  31. resp http.Response
  32. body []byte
  33. err error
  34. }
  35. func (a *actionAssertingHTTPClient) Do(_ context.Context, act httpAction) (*http.Response, []byte, error) {
  36. if !reflect.DeepEqual(a.act, act) {
  37. a.t.Errorf("#%d: unexpected httpAction: want=%#v got=%#v", a.num, a.act, act)
  38. }
  39. return &a.resp, a.body, a.err
  40. }
  41. type staticHTTPClient struct {
  42. resp http.Response
  43. body []byte
  44. err error
  45. }
  46. func (s *staticHTTPClient) Do(context.Context, httpAction) (*http.Response, []byte, error) {
  47. return &s.resp, s.body, s.err
  48. }
  49. type staticHTTPAction struct {
  50. request http.Request
  51. }
  52. func (s *staticHTTPAction) HTTPRequest(url.URL) *http.Request {
  53. return &s.request
  54. }
  55. type staticHTTPResponse struct {
  56. resp http.Response
  57. body []byte
  58. err error
  59. }
  60. type multiStaticHTTPClient struct {
  61. responses []staticHTTPResponse
  62. cur int
  63. }
  64. func (s *multiStaticHTTPClient) Do(context.Context, httpAction) (*http.Response, []byte, error) {
  65. r := s.responses[s.cur]
  66. s.cur++
  67. return &r.resp, r.body, r.err
  68. }
  69. func newStaticHTTPClientFactory(responses []staticHTTPResponse) httpClientFactory {
  70. var cur int
  71. return func(url.URL) httpClient {
  72. r := responses[cur]
  73. cur++
  74. return &staticHTTPClient{resp: r.resp, body: r.body, err: r.err}
  75. }
  76. }
  77. type fakeTransport struct {
  78. respchan chan *http.Response
  79. errchan chan error
  80. startCancel chan struct{}
  81. finishCancel chan struct{}
  82. }
  83. func newFakeTransport() *fakeTransport {
  84. return &fakeTransport{
  85. respchan: make(chan *http.Response, 1),
  86. errchan: make(chan error, 1),
  87. startCancel: make(chan struct{}, 1),
  88. finishCancel: make(chan struct{}, 1),
  89. }
  90. }
  91. func (t *fakeTransport) RoundTrip(*http.Request) (*http.Response, error) {
  92. select {
  93. case resp := <-t.respchan:
  94. return resp, nil
  95. case err := <-t.errchan:
  96. return nil, err
  97. case <-t.startCancel:
  98. // wait on finishCancel to simulate taking some amount of
  99. // time while calling CancelRequest
  100. <-t.finishCancel
  101. return nil, errors.New("cancelled")
  102. }
  103. }
  104. func (t *fakeTransport) CancelRequest(*http.Request) {
  105. t.startCancel <- struct{}{}
  106. }
  107. type fakeAction struct{}
  108. func (a *fakeAction) HTTPRequest(url.URL) *http.Request {
  109. return &http.Request{}
  110. }
  111. func TestSimpleHTTPClientDoSuccess(t *testing.T) {
  112. tr := newFakeTransport()
  113. c := &simpleHTTPClient{transport: tr}
  114. tr.respchan <- &http.Response{
  115. StatusCode: http.StatusTeapot,
  116. Body: ioutil.NopCloser(strings.NewReader("foo")),
  117. }
  118. resp, body, err := c.Do(context.Background(), &fakeAction{})
  119. if err != nil {
  120. t.Fatalf("incorrect error value: want=nil got=%v", err)
  121. }
  122. wantCode := http.StatusTeapot
  123. if wantCode != resp.StatusCode {
  124. t.Fatalf("invalid response code: want=%d got=%d", wantCode, resp.StatusCode)
  125. }
  126. wantBody := []byte("foo")
  127. if !reflect.DeepEqual(wantBody, body) {
  128. t.Fatalf("invalid response body: want=%q got=%q", wantBody, body)
  129. }
  130. }
  131. func TestSimpleHTTPClientDoError(t *testing.T) {
  132. tr := newFakeTransport()
  133. c := &simpleHTTPClient{transport: tr}
  134. tr.errchan <- errors.New("fixture")
  135. _, _, err := c.Do(context.Background(), &fakeAction{})
  136. if err == nil {
  137. t.Fatalf("expected non-nil error, got nil")
  138. }
  139. }
  140. func TestSimpleHTTPClientDoCancelContext(t *testing.T) {
  141. tr := newFakeTransport()
  142. c := &simpleHTTPClient{transport: tr}
  143. tr.startCancel <- struct{}{}
  144. tr.finishCancel <- struct{}{}
  145. _, _, err := c.Do(context.Background(), &fakeAction{})
  146. if err == nil {
  147. t.Fatalf("expected non-nil error, got nil")
  148. }
  149. }
  150. type checkableReadCloser struct {
  151. io.ReadCloser
  152. closed bool
  153. }
  154. func (c *checkableReadCloser) Close() error {
  155. c.closed = true
  156. return c.ReadCloser.Close()
  157. }
  158. func TestSimpleHTTPClientDoCancelContextResponseBodyClosed(t *testing.T) {
  159. tr := newFakeTransport()
  160. c := &simpleHTTPClient{transport: tr}
  161. // create an already-cancelled context
  162. ctx, cancel := context.WithCancel(context.Background())
  163. cancel()
  164. body := &checkableReadCloser{ReadCloser: ioutil.NopCloser(strings.NewReader("foo"))}
  165. go func() {
  166. // wait for CancelRequest to be called, informing us that simpleHTTPClient
  167. // knows the context is already timed out
  168. <-tr.startCancel
  169. tr.respchan <- &http.Response{Body: body}
  170. tr.finishCancel <- struct{}{}
  171. }()
  172. _, _, err := c.Do(ctx, &fakeAction{})
  173. if err == nil {
  174. t.Fatalf("expected non-nil error, got nil")
  175. }
  176. if !body.closed {
  177. t.Fatalf("expected closed body")
  178. }
  179. }
  180. func TestSimpleHTTPClientDoCancelContextWaitForRoundTrip(t *testing.T) {
  181. tr := newFakeTransport()
  182. c := &simpleHTTPClient{transport: tr}
  183. donechan := make(chan struct{})
  184. ctx, cancel := context.WithCancel(context.Background())
  185. go func() {
  186. c.Do(ctx, &fakeAction{})
  187. close(donechan)
  188. }()
  189. // This should call CancelRequest and begin the cancellation process
  190. cancel()
  191. select {
  192. case <-donechan:
  193. t.Fatalf("simpleHTTPClient.Do should not have exited yet")
  194. default:
  195. }
  196. tr.finishCancel <- struct{}{}
  197. select {
  198. case <-donechan:
  199. //expected behavior
  200. return
  201. case <-time.After(time.Second):
  202. t.Fatalf("simpleHTTPClient.Do did not exit within 1s")
  203. }
  204. }
  205. func TestHTTPClusterClientDo(t *testing.T) {
  206. fakeErr := errors.New("fake!")
  207. fakeURL := url.URL{}
  208. tests := []struct {
  209. client *httpClusterClient
  210. wantCode int
  211. wantErr error
  212. }{
  213. // first good response short-circuits Do
  214. {
  215. client: &httpClusterClient{
  216. endpoints: []url.URL{fakeURL, fakeURL},
  217. clientFactory: newStaticHTTPClientFactory(
  218. []staticHTTPResponse{
  219. staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
  220. staticHTTPResponse{err: fakeErr},
  221. },
  222. ),
  223. },
  224. wantCode: http.StatusTeapot,
  225. },
  226. // fall through to good endpoint if err is arbitrary
  227. {
  228. client: &httpClusterClient{
  229. endpoints: []url.URL{fakeURL, fakeURL},
  230. clientFactory: newStaticHTTPClientFactory(
  231. []staticHTTPResponse{
  232. staticHTTPResponse{err: fakeErr},
  233. staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
  234. },
  235. ),
  236. },
  237. wantCode: http.StatusTeapot,
  238. },
  239. // context.DeadlineExceeded short-circuits Do
  240. {
  241. client: &httpClusterClient{
  242. endpoints: []url.URL{fakeURL, fakeURL},
  243. clientFactory: newStaticHTTPClientFactory(
  244. []staticHTTPResponse{
  245. staticHTTPResponse{err: context.DeadlineExceeded},
  246. staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
  247. },
  248. ),
  249. },
  250. wantErr: context.DeadlineExceeded,
  251. },
  252. // context.Canceled short-circuits Do
  253. {
  254. client: &httpClusterClient{
  255. endpoints: []url.URL{fakeURL, fakeURL},
  256. clientFactory: newStaticHTTPClientFactory(
  257. []staticHTTPResponse{
  258. staticHTTPResponse{err: context.Canceled},
  259. staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
  260. },
  261. ),
  262. },
  263. wantErr: context.Canceled,
  264. },
  265. // return err if there are no endpoints
  266. {
  267. client: &httpClusterClient{
  268. endpoints: []url.URL{},
  269. clientFactory: newHTTPClientFactory(nil, nil),
  270. },
  271. wantErr: ErrNoEndpoints,
  272. },
  273. // return err if all endpoints return arbitrary errors
  274. {
  275. client: &httpClusterClient{
  276. endpoints: []url.URL{fakeURL, fakeURL},
  277. clientFactory: newStaticHTTPClientFactory(
  278. []staticHTTPResponse{
  279. staticHTTPResponse{err: fakeErr},
  280. staticHTTPResponse{err: fakeErr},
  281. },
  282. ),
  283. },
  284. wantErr: fakeErr,
  285. },
  286. // 500-level errors cause Do to fallthrough to next endpoint
  287. {
  288. client: &httpClusterClient{
  289. endpoints: []url.URL{fakeURL, fakeURL},
  290. clientFactory: newStaticHTTPClientFactory(
  291. []staticHTTPResponse{
  292. staticHTTPResponse{resp: http.Response{StatusCode: http.StatusBadGateway}},
  293. staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
  294. },
  295. ),
  296. },
  297. wantCode: http.StatusTeapot,
  298. },
  299. }
  300. for i, tt := range tests {
  301. resp, _, err := tt.client.Do(context.Background(), nil)
  302. if !reflect.DeepEqual(tt.wantErr, err) {
  303. t.Errorf("#%d: got err=%v, want=%v", i, err, tt.wantErr)
  304. continue
  305. }
  306. if resp == nil {
  307. if tt.wantCode != 0 {
  308. t.Errorf("#%d: resp is nil, want=%d", i, tt.wantCode)
  309. }
  310. continue
  311. }
  312. if resp.StatusCode != tt.wantCode {
  313. t.Errorf("#%d: resp code=%d, want=%d", i, resp.StatusCode, tt.wantCode)
  314. continue
  315. }
  316. }
  317. }
  318. func TestRedirectedHTTPAction(t *testing.T) {
  319. act := &redirectedHTTPAction{
  320. action: &staticHTTPAction{
  321. request: http.Request{
  322. Method: "DELETE",
  323. URL: &url.URL{
  324. Scheme: "https",
  325. Host: "foo.example.com",
  326. Path: "/ping",
  327. },
  328. },
  329. },
  330. location: url.URL{
  331. Scheme: "https",
  332. Host: "bar.example.com",
  333. Path: "/pong",
  334. },
  335. }
  336. want := &http.Request{
  337. Method: "DELETE",
  338. URL: &url.URL{
  339. Scheme: "https",
  340. Host: "bar.example.com",
  341. Path: "/pong",
  342. },
  343. }
  344. got := act.HTTPRequest(url.URL{Scheme: "http", Host: "baz.example.com", Path: "/pang"})
  345. if !reflect.DeepEqual(want, got) {
  346. t.Fatalf("HTTPRequest is %#v, want %#v", want, got)
  347. }
  348. }
  349. func TestRedirectFollowingHTTPClient(t *testing.T) {
  350. tests := []struct {
  351. checkRedirect CheckRedirectFunc
  352. client httpClient
  353. wantCode int
  354. wantErr error
  355. }{
  356. // errors bubbled up
  357. {
  358. checkRedirect: func(int) error { return ErrTooManyRedirects },
  359. client: &multiStaticHTTPClient{
  360. responses: []staticHTTPResponse{
  361. staticHTTPResponse{
  362. err: errors.New("fail!"),
  363. },
  364. },
  365. },
  366. wantErr: errors.New("fail!"),
  367. },
  368. // no need to follow redirect if none given
  369. {
  370. checkRedirect: func(int) error { return ErrTooManyRedirects },
  371. client: &multiStaticHTTPClient{
  372. responses: []staticHTTPResponse{
  373. staticHTTPResponse{
  374. resp: http.Response{
  375. StatusCode: http.StatusTeapot,
  376. },
  377. },
  378. },
  379. },
  380. wantCode: http.StatusTeapot,
  381. },
  382. // redirects if less than max
  383. {
  384. checkRedirect: func(via int) error {
  385. if via >= 2 {
  386. return ErrTooManyRedirects
  387. }
  388. return nil
  389. },
  390. client: &multiStaticHTTPClient{
  391. responses: []staticHTTPResponse{
  392. staticHTTPResponse{
  393. resp: http.Response{
  394. StatusCode: http.StatusTemporaryRedirect,
  395. Header: http.Header{"Location": []string{"http://example.com"}},
  396. },
  397. },
  398. staticHTTPResponse{
  399. resp: http.Response{
  400. StatusCode: http.StatusTeapot,
  401. },
  402. },
  403. },
  404. },
  405. wantCode: http.StatusTeapot,
  406. },
  407. // succeed after reaching max redirects
  408. {
  409. checkRedirect: func(via int) error {
  410. if via >= 3 {
  411. return ErrTooManyRedirects
  412. }
  413. return nil
  414. },
  415. client: &multiStaticHTTPClient{
  416. responses: []staticHTTPResponse{
  417. staticHTTPResponse{
  418. resp: http.Response{
  419. StatusCode: http.StatusTemporaryRedirect,
  420. Header: http.Header{"Location": []string{"http://example.com"}},
  421. },
  422. },
  423. staticHTTPResponse{
  424. resp: http.Response{
  425. StatusCode: http.StatusTemporaryRedirect,
  426. Header: http.Header{"Location": []string{"http://example.com"}},
  427. },
  428. },
  429. staticHTTPResponse{
  430. resp: http.Response{
  431. StatusCode: http.StatusTeapot,
  432. },
  433. },
  434. },
  435. },
  436. wantCode: http.StatusTeapot,
  437. },
  438. // fail if too many redirects
  439. {
  440. checkRedirect: func(via int) error {
  441. if via >= 2 {
  442. return ErrTooManyRedirects
  443. }
  444. return nil
  445. },
  446. client: &multiStaticHTTPClient{
  447. responses: []staticHTTPResponse{
  448. staticHTTPResponse{
  449. resp: http.Response{
  450. StatusCode: http.StatusTemporaryRedirect,
  451. Header: http.Header{"Location": []string{"http://example.com"}},
  452. },
  453. },
  454. staticHTTPResponse{
  455. resp: http.Response{
  456. StatusCode: http.StatusTemporaryRedirect,
  457. Header: http.Header{"Location": []string{"http://example.com"}},
  458. },
  459. },
  460. staticHTTPResponse{
  461. resp: http.Response{
  462. StatusCode: http.StatusTeapot,
  463. },
  464. },
  465. },
  466. },
  467. wantErr: ErrTooManyRedirects,
  468. },
  469. // fail if Location header not set
  470. {
  471. checkRedirect: func(int) error { return ErrTooManyRedirects },
  472. client: &multiStaticHTTPClient{
  473. responses: []staticHTTPResponse{
  474. staticHTTPResponse{
  475. resp: http.Response{
  476. StatusCode: http.StatusTemporaryRedirect,
  477. },
  478. },
  479. },
  480. },
  481. wantErr: errors.New("Location header not set"),
  482. },
  483. // fail if Location header is invalid
  484. {
  485. checkRedirect: func(int) error { return ErrTooManyRedirects },
  486. client: &multiStaticHTTPClient{
  487. responses: []staticHTTPResponse{
  488. staticHTTPResponse{
  489. resp: http.Response{
  490. StatusCode: http.StatusTemporaryRedirect,
  491. Header: http.Header{"Location": []string{":"}},
  492. },
  493. },
  494. },
  495. },
  496. wantErr: errors.New("Location header not valid URL: :"),
  497. },
  498. // fail if redirects checked way too many times
  499. {
  500. checkRedirect: func(int) error { return nil },
  501. client: &staticHTTPClient{
  502. resp: http.Response{
  503. StatusCode: http.StatusTemporaryRedirect,
  504. Header: http.Header{"Location": []string{"http://example.com"}},
  505. },
  506. },
  507. wantErr: errTooManyRedirectChecks,
  508. },
  509. }
  510. for i, tt := range tests {
  511. client := &redirectFollowingHTTPClient{client: tt.client, checkRedirect: tt.checkRedirect}
  512. resp, _, err := client.Do(context.Background(), nil)
  513. if !reflect.DeepEqual(tt.wantErr, err) {
  514. t.Errorf("#%d: got err=%v, want=%v", i, err, tt.wantErr)
  515. continue
  516. }
  517. if resp == nil {
  518. if tt.wantCode != 0 {
  519. t.Errorf("#%d: resp is nil, want=%d", i, tt.wantCode)
  520. }
  521. continue
  522. }
  523. if resp.StatusCode != tt.wantCode {
  524. t.Errorf("#%d: resp code=%d, want=%d", i, resp.StatusCode, tt.wantCode)
  525. continue
  526. }
  527. }
  528. }
  529. func TestDefaultCheckRedirect(t *testing.T) {
  530. tests := []struct {
  531. num int
  532. err error
  533. }{
  534. {0, nil},
  535. {5, nil},
  536. {10, nil},
  537. {11, ErrTooManyRedirects},
  538. {29, ErrTooManyRedirects},
  539. }
  540. for i, tt := range tests {
  541. err := DefaultCheckRedirect(tt.num)
  542. if !reflect.DeepEqual(tt.err, err) {
  543. t.Errorf("#%d: want=%#v got=%#v", i, tt.err, err)
  544. }
  545. }
  546. }
  547. func TestHTTPClusterClientSync(t *testing.T) {
  548. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  549. staticHTTPResponse{
  550. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  551. body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
  552. },
  553. })
  554. hc := &httpClusterClient{clientFactory: cf}
  555. err := hc.reset([]string{"http://127.0.0.1:2379"})
  556. if err != nil {
  557. t.Fatalf("unexpected error during setup: %#v", err)
  558. }
  559. want := []string{"http://127.0.0.1:2379"}
  560. got := hc.Endpoints()
  561. if !reflect.DeepEqual(want, got) {
  562. t.Fatalf("incorrect endpoints: want=%#v got=%#v", want, got)
  563. }
  564. err = hc.Sync(context.Background())
  565. if err != nil {
  566. t.Fatalf("unexpected error during Sync: %#v", err)
  567. }
  568. want = []string{"http://127.0.0.1:4003", "http://127.0.0.1:2379", "http://127.0.0.1:4001", "http://127.0.0.1:4002"}
  569. got = hc.Endpoints()
  570. if !reflect.DeepEqual(want, got) {
  571. t.Fatalf("incorrect endpoints post-Sync: want=%#v got=%#v", want, got)
  572. }
  573. err = hc.reset([]string{"http://127.0.0.1:4009"})
  574. if err != nil {
  575. t.Fatalf("unexpected error during reset: %#v", err)
  576. }
  577. want = []string{"http://127.0.0.1:4009"}
  578. got = hc.Endpoints()
  579. if !reflect.DeepEqual(want, got) {
  580. t.Fatalf("incorrect endpoints post-reset: want=%#v got=%#v", want, got)
  581. }
  582. }
  583. func TestHTTPClusterClientSyncFail(t *testing.T) {
  584. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  585. staticHTTPResponse{err: errors.New("fail!")},
  586. })
  587. hc := &httpClusterClient{clientFactory: cf}
  588. err := hc.reset([]string{"http://127.0.0.1:2379"})
  589. if err != nil {
  590. t.Fatalf("unexpected error during setup: %#v", err)
  591. }
  592. want := []string{"http://127.0.0.1:2379"}
  593. got := hc.Endpoints()
  594. if !reflect.DeepEqual(want, got) {
  595. t.Fatalf("incorrect endpoints: want=%#v got=%#v", want, got)
  596. }
  597. err = hc.Sync(context.Background())
  598. if err == nil {
  599. t.Fatalf("got nil error during Sync")
  600. }
  601. got = hc.Endpoints()
  602. if !reflect.DeepEqual(want, got) {
  603. t.Fatalf("incorrect endpoints after failed Sync: want=%#v got=%#v", want, got)
  604. }
  605. }
  606. func TestHTTPClusterClientResetFail(t *testing.T) {
  607. tests := [][]string{
  608. // need at least one endpoint
  609. []string{},
  610. // urls must be valid
  611. []string{":"},
  612. }
  613. for i, tt := range tests {
  614. hc := &httpClusterClient{}
  615. err := hc.reset(tt)
  616. if err == nil {
  617. t.Errorf("#%d: expected non-nil error", i)
  618. }
  619. }
  620. }