Merge lp:~jtv/gwacl/batch-list-containers into lp:gwacl
- batch-list-containers
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Jeroen T. Vermeulen |
Approved revision: | 102 |
Merged at revision: | 96 |
Proposed branch: | lp:~jtv/gwacl/batch-list-containers |
Merge into: | lp:gwacl |
Diff against target: |
329 lines (+135/-30) 5 files modified
Makefile (+0/-2) storage_base.go (+53/-11) storage_base_test.go (+56/-10) testhelpers_http.go (+25/-4) testhelpers_misc.go (+1/-3) |
To merge this branch: | bzr merge lp:~jtv/gwacl/batch-list-containers |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Raphaël Badin (community) | Approve | ||
Review via email: mp+160844@code.launchpad.net |
Commit message
Support batched results in ListContainers().
Description of the change
This is actually required for ListContainers() to function properly. I kept the existing tests, but removed a NextMarker in an existing test. That test value was inappropriate (it was an unbatched result, so should not have a NextMarker) and broke the test once I had batch support working. Plus of course I added a test for a batching call before actually implementing support.
The code I ended up with isn't as simple as I'd like. If this were python, I'd test it much more thoroughly.
You'll also note some small drive-by improvements and TODO comments. In the Makefile you'll see that gofmt with the -s (simplify) option also does the regular work of a gofmt without the -s.
In a next branch I'll be doing the exact same batching for the List Blobs call. Hopefully I'll be able to extract some shared code. These were the only two instances of batching that I found in the storage API insofar as we support it.
Jeroen
- 101. By Jeroen T. Vermeulen
-
Review suggestion: don't try to relay unreliable results in the error case.
Jeroen T. Vermeulen (jtv) wrote : | # |
> Looks good. I've got a few remarks but I'm not really familiar with that code
> so take them with a grain of salt.
Thanks for the review. Yes, the code is not side we worked on in Brisbane... What I can tell you is it gets a bit ugly sometimes. :)
> [0]
>
> [in ListContainers]
> 54 + firstBatch, marker, err := context.
> 55 if err != nil {
> [...]
> 62 + return firstBatch, err
> 63 + }
>
> Is it worth returning the firstBatch if we get an error here? Unless there is
> a good reason why we would want to do this (and then it's worth adding to the
> method's documentation), it would seem more logical to me to return "nil, err"
> in this case…
You're right. I changed it.
I was reasoning that if I pass on whatever we get, I don't have to worry about any possibility of getting a useful result despite an error: if that ever happened we could just document whatever Azure does and our existing code would probably support it correctly. But that doesn't work very well in the presence of batched results: what would we do if we got an error code but a usable result *with* a NextMarker? Defensive coding could cope with any sensible scenario, but absent a good indication that there's any point, it's not the right thing to do.
> [1]
>
> 65 + batch, nextMarker, err :=
> context.
> 66 + if err != nil {
> 67 + return firstBatch, err
> 68 + }
>
> (let's forget [0] for a moment here) Why returning firstBatch here? To be
> more coherent with the first error return clause (the one which I talk about
> in [0]), I'd return the accumulated batches returned so far, *including the
> one we just got*. But maybe (see [0]) it's even more simple to simply return
> "nil, err".
I returned nil here as well.
> [2]
>
> 77 + // Since we must have arrived at the last batch, where NextMarker
> is empty,
> 78 + // it would be misleading to report the original NextMarker.
> 79 + firstBatch.
>
> I don't really understand the necessity for this (but I'm not really familiar
> with the context). From the looks of it, you're just introducing a small
> difference here, compared to what one will expect to get by reading the API
> documentation.
It's not so much a matter of necessity, as of avoiding a misrepresentation. If NextMarker is nonempty, that means the result is incomplete. Thus, for the complete result, it would be improper to keep NextMarker set to a nonempty value. The way I did it, a user who expects ListContainers to correspond exactly to one call to the List Containers API method will not be misled. It's just that the default batch size will be effectively infinite. (Except of course Go will break if you try to make an array of 2³¹ entries, but they've been hinting that they'll fix that at some point.)
There are different ways to argue this. The root cause is that ListContainers no longer corresponds exactly to an API call: it operates at a slightly higher level. And so ContainerEnumer
1....
- 102. By Jeroen T. Vermeulen
-
Streamline the ListContainers loop.
Julian Edwards (julian-edwards) wrote : | # |
Did you check to see if the example still runs after this change?
Jeroen T. Vermeulen (jtv) wrote : | # |
No, I didn't check... Couldn't, what with the consequences of that disk-deletion bug leaving my account "full." And indeed I left out a crucial bit, as corrected in my other branch.
Preview Diff
1 | === modified file 'Makefile' | |||
2 | --- Makefile 2013-04-24 08:36:17 +0000 | |||
3 | +++ Makefile 2013-04-25 14:03:27 +0000 | |||
4 | @@ -11,9 +11,7 @@ | |||
5 | 11 | 11 | ||
6 | 12 | # Reformat the source files to match our layout standards. | 12 | # Reformat the source files to match our layout standards. |
7 | 13 | # This includes gofmt's "simplify" option to streamline the source code. | 13 | # This includes gofmt's "simplify" option to streamline the source code. |
8 | 14 | # TODO: The -s run may actually also do the regular formatting. | ||
9 | 15 | format: | 14 | format: |
10 | 16 | ./utilities/format | ||
11 | 17 | ./utilities/format -s | 15 | ./utilities/format -s |
12 | 18 | 16 | ||
13 | 19 | # Build the examples (we have no tests for them). | 17 | # Build the examples (we have no tests for them). |
14 | 20 | 18 | ||
15 | === modified file 'storage_base.go' | |||
16 | --- storage_base.go 2013-04-22 16:00:31 +0000 | |||
17 | +++ storage_base.go 2013-04-25 14:03:27 +0000 | |||
18 | @@ -265,19 +265,61 @@ | |||
19 | 265 | return resp, nil | 265 | return resp, nil |
20 | 266 | } | 266 | } |
21 | 267 | 267 | ||
24 | 268 | // Send a request to the storage service to list the containers in the | 268 | // getListContainersBatch calls the "List Containers" operation on the storage |
25 | 269 | // storage account. error is non-nil if an error occurred. | 269 | // API, and returns a single batch of results; its "next marker" for batching, |
26 | 270 | // and an error code. | ||
27 | 271 | // The marker argument should be empty for a new List Containers request. for | ||
28 | 272 | // subsequent calls to get additional batches of the same result, pass the | ||
29 | 273 | // "next marker" returned by the previous call. | ||
30 | 274 | // The "next marker" will be empty on the last batch. | ||
31 | 275 | func (context *StorageContext) getListContainersBatch(marker string) (*ContainerEnumerationResults, string, error) { | ||
32 | 276 | uri := interpolateURL( | ||
33 | 277 | "http://___.blob.core.windows.net/?comp=list", context.Account) | ||
34 | 278 | req, err := http.NewRequest("GET", uri, nil) | ||
35 | 279 | if err != nil { | ||
36 | 280 | return nil, "", err | ||
37 | 281 | } | ||
38 | 282 | addStandardHeaders(req, context.Account, context.Key, "2012-02-12") | ||
39 | 283 | containers := ContainerEnumerationResults{} | ||
40 | 284 | _, err = context.send(req, &containers, http.StatusOK) | ||
41 | 285 | if err != nil { | ||
42 | 286 | return nil, "", err | ||
43 | 287 | } | ||
44 | 288 | |||
45 | 289 | // The response may contain a NextMarker field, to let us request a | ||
46 | 290 | // subsequent batch of results. The XML parser won't trim whitespace out | ||
47 | 291 | // of the marker tag, so we do that here. | ||
48 | 292 | nextMarker := strings.TrimSpace(containers.NextMarker) | ||
49 | 293 | return &containers, nextMarker, nil | ||
50 | 294 | } | ||
51 | 295 | |||
52 | 296 | // ListContainers sends a request to the storage service to list the containers | ||
53 | 297 | // in the storage account. error is non-nil if an error occurred. | ||
54 | 270 | func (context *StorageContext) ListContainers() (*ContainerEnumerationResults, error) { | 298 | func (context *StorageContext) ListContainers() (*ContainerEnumerationResults, error) { |
60 | 271 | uri := interpolateURL( | 299 | containers := make([]Container, 0) |
61 | 272 | "http://___.blob.core.windows.net/?comp=list", context.Account) | 300 | var batch *ContainerEnumerationResults |
62 | 273 | req, err := http.NewRequest("GET", uri, nil) | 301 | |
63 | 274 | if err != nil { | 302 | // Request the initial result, using the empty marker. Then, for as long |
64 | 275 | return nil, err | 303 | // as the result has a nonempty NextMarker, request the next batch using |
65 | 304 | // that marker. | ||
66 | 305 | for marker, nextMarker := "", "x"; nextMarker != ""; marker = nextMarker { | ||
67 | 306 | var err error | ||
68 | 307 | // Don't use := here or you'll shadow variables from the function's | ||
69 | 308 | // outer scopes. | ||
70 | 309 | batch, nextMarker, err = context.getListContainersBatch(marker) | ||
71 | 310 | if err != nil { | ||
72 | 311 | return nil, err | ||
73 | 312 | } | ||
74 | 313 | containers = append(containers, batch.Containers...) | ||
75 | 276 | } | 314 | } |
80 | 277 | addStandardHeaders(req, context.Account, context.Key, "2012-02-12") | 315 | |
81 | 278 | containers := &ContainerEnumerationResults{} | 316 | // There's more in a ContainerEnumerationResults than just the containers. |
82 | 279 | _, e := context.send(req, containers, http.StatusOK) | 317 | // Return the latest batch, but give it the full cumulative containers list |
83 | 280 | return containers, e | 318 | // instead of just the last batch. |
84 | 319 | // To the caller, this will look like they made one call to Azure's | ||
85 | 320 | // List Containers method, but batch size was unlimited. | ||
86 | 321 | batch.Containers = containers | ||
87 | 322 | return batch, nil | ||
88 | 281 | } | 323 | } |
89 | 282 | 324 | ||
90 | 283 | // Send a request to the storage service to list the blobs in a container. | 325 | // Send a request to the storage service to list the blobs in a container. |
91 | 284 | 326 | ||
92 | === modified file 'storage_base_test.go' | |||
93 | --- storage_base_test.go 2013-04-24 10:08:54 +0000 | |||
94 | +++ storage_base_test.go 2013-04-25 14:03:27 +0000 | |||
95 | @@ -295,13 +295,13 @@ | |||
96 | 295 | </Metadata> | 295 | </Metadata> |
97 | 296 | </Container> | 296 | </Container> |
98 | 297 | </Containers> | 297 | </Containers> |
100 | 298 | <NextMarker>next-marker-value</NextMarker> | 298 | <NextMarker/> |
101 | 299 | </EnumerationResults>` | 299 | </EnumerationResults>` |
102 | 300 | context := makeStorageContext() | 300 | context := makeStorageContext() |
103 | 301 | response := &http.Response{ | 301 | response := &http.Response{ |
104 | 302 | Status: fmt.Sprintf("%d", http.StatusOK), | 302 | Status: fmt.Sprintf("%d", http.StatusOK), |
105 | 303 | StatusCode: http.StatusOK, | 303 | StatusCode: http.StatusOK, |
107 | 304 | Body: ioutil.NopCloser(strings.NewReader(response_body)), | 304 | Body: makeResponseBody(response_body), |
108 | 305 | } | 305 | } |
109 | 306 | transport := &TestTransport{Response: response} | 306 | transport := &TestTransport{Response: response} |
110 | 307 | context.client = &http.Client{Transport: transport} | 307 | context.client = &http.Client{Transport: transport} |
111 | @@ -337,6 +337,52 @@ | |||
112 | 337 | c.Assert(err, NotNil) | 337 | c.Assert(err, NotNil) |
113 | 338 | } | 338 | } |
114 | 339 | 339 | ||
115 | 340 | // ListContainers combines multiple batches of output. | ||
116 | 341 | func (suite *TestListContainers) TestBatchedResult(c *C) { | ||
117 | 342 | firstContainer := "container1" | ||
118 | 343 | lastContainer := "container2" | ||
119 | 344 | marker := "moreplease" | ||
120 | 345 | firstBatch := http.Response{ | ||
121 | 346 | StatusCode: http.StatusOK, | ||
122 | 347 | Body: makeResponseBody(fmt.Sprintf(` | ||
123 | 348 | <EnumerationResults> | ||
124 | 349 | <Containers> | ||
125 | 350 | <Container> | ||
126 | 351 | <Name>%s</Name> | ||
127 | 352 | <URL>container-address</URL> | ||
128 | 353 | </Container> | ||
129 | 354 | </Containers> | ||
130 | 355 | <NextMarker>%s</NextMarker> | ||
131 | 356 | </EnumerationResults> | ||
132 | 357 | `, firstContainer, marker)), | ||
133 | 358 | } | ||
134 | 359 | lastBatch := http.Response{ | ||
135 | 360 | StatusCode: http.StatusOK, | ||
136 | 361 | Body: makeResponseBody(fmt.Sprintf(` | ||
137 | 362 | <EnumerationResults> | ||
138 | 363 | <Containers> | ||
139 | 364 | <Container> | ||
140 | 365 | <Name>%s</Name> | ||
141 | 366 | <URL>container-address</URL> | ||
142 | 367 | </Container> | ||
143 | 368 | </Containers> | ||
144 | 369 | </EnumerationResults> | ||
145 | 370 | `, lastContainer)), | ||
146 | 371 | } | ||
147 | 372 | transport := TestTransport2{} | ||
148 | 373 | transport.AddExchange(&firstBatch, nil) | ||
149 | 374 | transport.AddExchange(&lastBatch, nil) | ||
150 | 375 | context := makeStorageContext() | ||
151 | 376 | context.client = &http.Client{Transport: &transport} | ||
152 | 377 | |||
153 | 378 | containers, err := context.ListContainers() | ||
154 | 379 | c.Assert(err, IsNil) | ||
155 | 380 | |||
156 | 381 | c.Check(len(containers.Containers), Equals, 2) | ||
157 | 382 | c.Check(containers.Containers[0].Name, Equals, firstContainer) | ||
158 | 383 | c.Check(containers.Containers[1].Name, Equals, lastContainer) | ||
159 | 384 | } | ||
160 | 385 | |||
161 | 340 | type TestListBlobs struct{} | 386 | type TestListBlobs struct{} |
162 | 341 | 387 | ||
163 | 342 | var _ = Suite(&TestListBlobs{}) | 388 | var _ = Suite(&TestListBlobs{}) |
164 | @@ -392,7 +438,7 @@ | |||
165 | 392 | response := &http.Response{ | 438 | response := &http.Response{ |
166 | 393 | Status: fmt.Sprintf("%d", http.StatusOK), | 439 | Status: fmt.Sprintf("%d", http.StatusOK), |
167 | 394 | StatusCode: http.StatusOK, | 440 | StatusCode: http.StatusOK, |
169 | 395 | Body: ioutil.NopCloser(strings.NewReader(response_body)), | 441 | Body: makeResponseBody(response_body), |
170 | 396 | } | 442 | } |
171 | 397 | transport := &TestTransport{Response: response} | 443 | transport := &TestTransport{Response: response} |
172 | 398 | context.client = &http.Client{Transport: transport} | 444 | context.client = &http.Client{Transport: transport} |
173 | @@ -523,7 +569,7 @@ | |||
174 | 523 | response := &http.Response{ | 569 | response := &http.Response{ |
175 | 524 | Status: "102 Frotzed", | 570 | Status: "102 Frotzed", |
176 | 525 | StatusCode: 102, | 571 | StatusCode: 102, |
178 | 526 | Body: ioutil.NopCloser(strings.NewReader("<Error><Code>Frotzed</Code><Message>failed to put blob</Message></Error>")), | 572 | Body: makeResponseBody("<Error><Code>Frotzed</Code><Message>failed to put blob</Message></Error>"), |
179 | 527 | } | 573 | } |
180 | 528 | transport := &TestTransport{Response: response} | 574 | transport := &TestTransport{Response: response} |
181 | 529 | context := makeStorageContext() | 575 | context := makeStorageContext() |
182 | @@ -582,7 +628,7 @@ | |||
183 | 582 | response := &http.Response{ | 628 | response := &http.Response{ |
184 | 583 | Status: "102 Frotzed", | 629 | Status: "102 Frotzed", |
185 | 584 | StatusCode: 102, | 630 | StatusCode: 102, |
187 | 585 | Body: ioutil.NopCloser(strings.NewReader("<Error><Code>Frotzed</Code><Message>failed to put block</Message></Error>")), | 631 | Body: makeResponseBody("<Error><Code>Frotzed</Code><Message>failed to put block</Message></Error>"), |
188 | 586 | } | 632 | } |
189 | 587 | transport := &TestTransport{Response: response} | 633 | transport := &TestTransport{Response: response} |
190 | 588 | context := makeStorageContext() | 634 | context := makeStorageContext() |
191 | @@ -645,7 +691,7 @@ | |||
192 | 645 | response := &http.Response{ | 691 | response := &http.Response{ |
193 | 646 | Status: "102 Frotzed", | 692 | Status: "102 Frotzed", |
194 | 647 | StatusCode: 102, | 693 | StatusCode: 102, |
196 | 648 | Body: ioutil.NopCloser(strings.NewReader("<Error><Code>Frotzed</Code><Message>failed to put blocklist</Message></Error>")), | 694 | Body: makeResponseBody("<Error><Code>Frotzed</Code><Message>failed to put blocklist</Message></Error>"), |
197 | 649 | } | 695 | } |
198 | 650 | transport := &TestTransport{Response: response} | 696 | transport := &TestTransport{Response: response} |
199 | 651 | context := makeStorageContext() | 697 | context := makeStorageContext() |
200 | @@ -686,7 +732,7 @@ | |||
201 | 686 | response := &http.Response{ | 732 | response := &http.Response{ |
202 | 687 | Status: fmt.Sprintf("%d", http.StatusOK), | 733 | Status: fmt.Sprintf("%d", http.StatusOK), |
203 | 688 | StatusCode: http.StatusOK, | 734 | StatusCode: http.StatusOK, |
205 | 689 | Body: ioutil.NopCloser(strings.NewReader(response_body)), | 735 | Body: makeResponseBody(response_body), |
206 | 690 | } | 736 | } |
207 | 691 | transport := &TestTransport{Response: response} | 737 | transport := &TestTransport{Response: response} |
208 | 692 | context.client = &http.Client{Transport: transport} | 738 | context.client = &http.Client{Transport: transport} |
209 | @@ -760,7 +806,7 @@ | |||
210 | 760 | response := &http.Response{ | 806 | response := &http.Response{ |
211 | 761 | Status: "146 Frotzed", | 807 | Status: "146 Frotzed", |
212 | 762 | StatusCode: 146, | 808 | StatusCode: 146, |
214 | 763 | Body: ioutil.NopCloser(strings.NewReader("<Error><Code>Frotzed</Code><Message>failed to delete blob</Message></Error>")), | 809 | Body: makeResponseBody("<Error><Code>Frotzed</Code><Message>failed to delete blob</Message></Error>"), |
215 | 764 | } | 810 | } |
216 | 765 | transport := &TestTransport{Response: response} | 811 | transport := &TestTransport{Response: response} |
217 | 766 | context := makeStorageContext() | 812 | context := makeStorageContext() |
218 | @@ -782,7 +828,7 @@ | |||
219 | 782 | response := &http.Response{ | 828 | response := &http.Response{ |
220 | 783 | Status: fmt.Sprintf("%d", http.StatusOK), | 829 | Status: fmt.Sprintf("%d", http.StatusOK), |
221 | 784 | StatusCode: http.StatusOK, | 830 | StatusCode: http.StatusOK, |
223 | 785 | Body: ioutil.NopCloser(strings.NewReader(response_body)), | 831 | Body: makeResponseBody(response_body), |
224 | 786 | } | 832 | } |
225 | 787 | transport := &TestTransport{Response: response} | 833 | transport := &TestTransport{Response: response} |
226 | 788 | context.client = &http.Client{Transport: transport} | 834 | context.client = &http.Client{Transport: transport} |
227 | @@ -818,7 +864,7 @@ | |||
228 | 818 | response := &http.Response{ | 864 | response := &http.Response{ |
229 | 819 | Status: "246 Frotzed", | 865 | Status: "246 Frotzed", |
230 | 820 | StatusCode: 246, | 866 | StatusCode: 246, |
232 | 821 | Body: ioutil.NopCloser(strings.NewReader("<Error><Code>Frotzed</Code><Message>failed to get blob</Message></Error>")), | 867 | Body: makeResponseBody("<Error><Code>Frotzed</Code><Message>failed to get blob</Message></Error>"), |
233 | 822 | } | 868 | } |
234 | 823 | transport := &TestTransport{Response: response} | 869 | transport := &TestTransport{Response: response} |
235 | 824 | context := makeStorageContext() | 870 | context := makeStorageContext() |
236 | 825 | 871 | ||
237 | === modified file 'testhelpers_http.go' | |||
238 | --- testhelpers_http.go 2013-04-24 10:08:54 +0000 | |||
239 | +++ testhelpers_http.go 2013-04-25 14:03:27 +0000 | |||
240 | @@ -7,35 +7,49 @@ | |||
241 | 7 | 7 | ||
242 | 8 | import ( | 8 | import ( |
243 | 9 | "fmt" | 9 | "fmt" |
244 | 10 | "io" | ||
245 | 11 | "io/ioutil" | ||
246 | 10 | "net/http" | 12 | "net/http" |
247 | 13 | "strings" | ||
248 | 11 | ) | 14 | ) |
249 | 12 | 15 | ||
252 | 13 | // TestTransport is used as an http.Client.Transport for testing. The only | 16 | // TestTransport is used as an http.Client.Transport for testing. It records |
253 | 14 | // requirement is that it adhere to the http.RoundTripper interface. | 17 | // the latest request, and returns a predetermined Response and error. |
254 | 18 | // TODO: Why does this need to be exported? | ||
255 | 15 | type TestTransport struct { | 19 | type TestTransport struct { |
256 | 16 | Request *http.Request | 20 | Request *http.Request |
257 | 17 | Response *http.Response | 21 | Response *http.Response |
258 | 18 | Error error | 22 | Error error |
259 | 19 | } | 23 | } |
260 | 20 | 24 | ||
261 | 25 | // TestTransport implements the http.RoundTripper interface. | ||
262 | 26 | var _ http.RoundTripper = &TestTransport{} | ||
263 | 27 | |||
264 | 21 | func (t *TestTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { | 28 | func (t *TestTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { |
265 | 22 | t.Request = req | 29 | t.Request = req |
266 | 23 | return t.Response, t.Error | 30 | return t.Response, t.Error |
267 | 24 | } | 31 | } |
268 | 25 | 32 | ||
269 | 33 | // TODO: Why does this need to be exported? | ||
270 | 26 | type TestTransport2Exchange struct { | 34 | type TestTransport2Exchange struct { |
271 | 27 | Request *http.Request | 35 | Request *http.Request |
272 | 28 | Response *http.Response | 36 | Response *http.Response |
273 | 29 | Error error | 37 | Error error |
274 | 30 | } | 38 | } |
275 | 31 | 39 | ||
278 | 32 | // TestTransport2 is used as an http.Client.Transport for testing. The only | 40 | // TestTransport2 is used as an http.Client.Transport for testing. It records |
279 | 33 | // requirement is that it adhere to the http.RoundTripper interface. | 41 | // the sequence of requests, and returns a predetermined sequence of Responses |
280 | 42 | // and errors. | ||
281 | 43 | // TODO: Rename this to something remotely sensible. | ||
282 | 44 | // TODO: Why does this need to be exported? | ||
283 | 34 | type TestTransport2 struct { | 45 | type TestTransport2 struct { |
284 | 35 | Exchanges []*TestTransport2Exchange | 46 | Exchanges []*TestTransport2Exchange |
285 | 36 | ExchangeCount int | 47 | ExchangeCount int |
286 | 37 | } | 48 | } |
287 | 38 | 49 | ||
288 | 50 | // TestTransport2 implements the http.RoundTripper interface. | ||
289 | 51 | var _ http.RoundTripper = &TestTransport2{} | ||
290 | 52 | |||
291 | 39 | func (t *TestTransport2) AddExchange(response *http.Response, error error) { | 53 | func (t *TestTransport2) AddExchange(response *http.Response, error error) { |
292 | 40 | exchange := TestTransport2Exchange{Response: response, Error: error} | 54 | exchange := TestTransport2Exchange{Response: response, Error: error} |
293 | 41 | t.Exchanges = append(t.Exchanges, &exchange) | 55 | t.Exchanges = append(t.Exchanges, &exchange) |
294 | @@ -56,3 +70,10 @@ | |||
295 | 56 | Body: Empty, | 70 | Body: Empty, |
296 | 57 | } | 71 | } |
297 | 58 | } | 72 | } |
298 | 73 | |||
299 | 74 | // makeResponseBody creates an http response body containing the given string. | ||
300 | 75 | // Use this to initialize an http.Response.Body with a given string, without | ||
301 | 76 | // having to care about the type details. | ||
302 | 77 | func makeResponseBody(content string) io.ReadCloser { | ||
303 | 78 | return ioutil.NopCloser(strings.NewReader(content)) | ||
304 | 79 | } | ||
305 | 59 | 80 | ||
306 | === modified file 'testhelpers_misc.go' | |||
307 | --- testhelpers_misc.go 2013-04-24 10:08:54 +0000 | |||
308 | +++ testhelpers_misc.go 2013-04-25 14:03:27 +0000 | |||
309 | @@ -4,11 +4,9 @@ | |||
310 | 4 | package gwacl | 4 | package gwacl |
311 | 5 | 5 | ||
312 | 6 | import ( | 6 | import ( |
313 | 7 | "bytes" | ||
314 | 8 | "encoding/base64" | 7 | "encoding/base64" |
315 | 9 | "fmt" | 8 | "fmt" |
316 | 10 | "io" | 9 | "io" |
317 | 11 | "io/ioutil" | ||
318 | 12 | ) | 10 | ) |
319 | 13 | 11 | ||
320 | 14 | // b64 is shorthand for base64-encoding a string. | 12 | // b64 is shorthand for base64-encoding a string. |
321 | @@ -17,7 +15,7 @@ | |||
322 | 17 | } | 15 | } |
323 | 18 | 16 | ||
324 | 19 | // A Reader and ReadCloser that EOFs immediately. | 17 | // A Reader and ReadCloser that EOFs immediately. |
326 | 20 | var Empty io.ReadCloser = ioutil.NopCloser(bytes.NewReader(nil)) | 18 | var Empty io.ReadCloser = makeResponseBody("") |
327 | 21 | 19 | ||
328 | 22 | // BoolToString represents a boolean value as a string ("true" or "false"). | 20 | // BoolToString represents a boolean value as a string ("true" or "false"). |
329 | 23 | func BoolToString(v bool) string { | 21 | func BoolToString(v bool) string { |
Looks good. I've got a few remarks but I'm not really familiar with that code so take them with a grain of salt.
[0]
[in ListContainers] getListContaine rsBatch( "")
54 + firstBatch, marker, err := context.
55 if err != nil {
[...]
62 + return firstBatch, err
63 + }
Is it worth returning the firstBatch if we get an error here? Unless there is a good reason why we would want to do this (and then it's worth adding to the method's documentation), it would seem more logical to me to return "nil, err" in this case…
[1]
65 + batch, nextMarker, err := context. getListContaine rsBatch( marker)
66 + if err != nil {
67 + return firstBatch, err
68 + }
(let's forget [0] for a moment here) Why returning firstBatch here? To be more coherent with the first error return clause (the one which I talk about in [0]), I'd return the accumulated batches returned so far, *including the one we just got*. But maybe (see [0]) it's even more simple to simply return "nil, err".
[2]
77 + // Since we must have arrived at the last batch, where NextMarker is empty, NextMarker = ""
78 + // it would be misleading to report the original NextMarker.
79 + firstBatch.
I don't really understand the necessity for this (but I'm not really familiar with the context). From the looks of it, you're just introducing a small difference here, compared to what one will expect to get by reading the API documentation.
[3]
I understand the necessity for the nextMarker/marker trick given the way you've done things here… but how about a simpler implementation… something like this: paste.ubuntu. com/5600649/ paste.ubuntu. com/5600647/
The method: http://
The diff: http://