Merge lp:~allenap/gwacl/list-files-by-prefix into lp:gwacl
- list-files-by-prefix
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Gavin Panella |
Approved revision: | 112 |
Merged at revision: | 106 |
Proposed branch: | lp:~allenap/gwacl/list-files-by-prefix |
Merge into: | lp:gwacl |
Diff against target: |
467 lines (+250/-94) 5 files modified
example/storage/run.go (+5/-1) storage.go (+35/-0) storage_base.go (+17/-41) storage_base_test.go (+52/-52) storage_test.go (+141/-0) |
To merge this branch: | bzr merge lp:~allenap/gwacl/list-files-by-prefix |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Julian Edwards (community) | Approve | ||
Review via email: mp+168479@code.launchpad.net |
Commit message
List blobs matching a prefix.
Description of the change
This branch's mission - and it's chosen to accept it - is to add
prefix support to the List Blobs API call via GWACL, where `prefix` is
defined as:
Optional. Filters the results to return only blobs whose names begin
with the specified prefix.
As far as I am aware, it's not possible to have "proper" optional
arguments with Go, so I've created the ListBlobsRequest struct to
provide this behaviour. It'll also make it easy to add support for
maxresults, include, timeout, etc. down the road.
However, first I had some refactoring to do: the List Blobs API call
was implemented in private getListBlobsBatch function, and ListBlobs
was a wrapper that was repeatedly calling getListBlobsBatch to get all
blobs. I think the layering is good, but masking the foundational API
call is limiting, and premature.
To address this I've:
- moved ListBlobs from storage_base.go to storage.go and renamed it
ListAllBlobs.
- getListBlobsBatch is now ListBlobs.
- Both functions take the new ListBlobsRequest struct as their only
argument.
A similar problem exists in ListContainers and getListContaine
but that can be addressed in a separate branch if the approach taken
here is acceptable.
I've also updated the storage example so that a prefix can be passed
in, and verified that it works.
Gavin Panella (allenap) wrote : | # |
> (You gotta wonder why Go doesn't have default args :/)
>
> The first thing that popped into my head was, do you think that we should put
> mandatory parameters as separate args to the ListAllBlobs function, and
> reserve ListBlobsRequest for optional params only? It seems as though you
> don't check for the mandatory params anyway, but it would obviate that param
> checking and is more explicit.
Now that we have a struct to represent the request, I think it should
be the place that it's represented. We could add a Validate() method
to the struct to check always-required parameters or something, but
I'm more inclined to just throw at Azure whatever is passed in, and
let Azure do the checking. Let's make the fewest assumptions about
Azure possible. It's less work too.
>
> Second thing: the tests nearly all have the same set-up code for the fake
> transport. I think we need a factored test helper for this as all the setup
> detracts from the test code's intention. I don't think it should be done in
> this branch though, a follow-up one is better.
Yeah, it's ugly. We can extract some of that stuff, but it may come
with a loss of clarity. I've seen enough of this code for now. Given
time I may come back to it and see a good way to clean this up; the
same goes for any of us.
>
> Also - I am wondering if we need some factory methods to create all these huge
> blobs of XML we're passing around? It's not entirely clear what we need
> though.
Yeah :-/ Same goes as for the set-up.
>
> Everything else looks pretty good to me, thanks for all the extra test cases!
Thanks for the review!
Julian Edwards (julian-edwards) wrote : | # |
On Tuesday 11 Jun 2013 13:30:38 you wrote:
> > (You gotta wonder why Go doesn't have default args :/)
> >
> > The first thing that popped into my head was, do you think that we should
> > put mandatory parameters as separate args to the ListAllBlobs function,
> > and reserve ListBlobsRequest for optional params only? It seems as
> > though you don't check for the mandatory params anyway, but it would
> > obviate that param checking and is more explicit.
>
> Now that we have a struct to represent the request, I think it should
> be the place that it's represented. We could add a Validate() method
> to the struct to check always-required parameters or something, but
> I'm more inclined to just throw at Azure whatever is passed in, and
> let Azure do the checking. Let's make the fewest assumptions about
> Azure possible. It's less work too.
My only worry with that is that it break the "fail as early as possible"
paradigm. The later you fail, the harder it is to work out the source of the
problem. Especially so with Go.
Please seriously consider adding some validation. It's tied to the API
version used anyway so it's not going to suddenly break at the other end.
>
> > Second thing: the tests nearly all have the same set-up code for the fake
> > transport. I think we need a factored test helper for this as all the
> > setup detracts from the test code's intention. I don't think it should
> > be done in this branch though, a follow-up one is better.
>
> Yeah, it's ugly. We can extract some of that stuff, but it may come
> with a loss of clarity.
I don't think clarity can get much worse from what it is now :)
> I've seen enough of this code for now. Given
> time I may come back to it and see a good way to clean this up; the
> same goes for any of us.
Ok, please file a tech debt bug.
>
> > Also - I am wondering if we need some factory methods to create all these
> > huge blobs of XML we're passing around? It's not entirely clear what we
> > need though.
>
> Yeah :-/ Same goes as for the set-up.
>
> > Everything else looks pretty good to me, thanks for all the extra test
> > cases!
> Thanks for the review!
I'd like to say it was a pleasure looking at all this Go. I'd really like to.
:^)
Preview Diff
1 | === modified file 'example/storage/run.go' |
2 | --- example/storage/run.go 2013-04-18 15:03:42 +0000 |
3 | +++ example/storage/run.go 2013-06-10 15:57:40 +0000 |
4 | @@ -21,6 +21,7 @@ |
5 | var key string |
6 | var filename string |
7 | var container string |
8 | +var prefix string |
9 | |
10 | func checkContainerAndFilename() error { |
11 | if container == "" || filename == "" { |
12 | @@ -38,6 +39,7 @@ |
13 | flag.StringVar(&key, "key", "", "A valid storage account key (base64 encoded)") |
14 | flag.StringVar(&container, "container", "", "Name of the container to use") |
15 | flag.StringVar(&filename, "file", "", "File containing blob to upload/download") |
16 | + flag.StringVar(&prefix, "prefix", "", "Prefix to match when listing blobs") |
17 | flag.Parse() |
18 | |
19 | if account == "" || key == "" { |
20 | @@ -102,7 +104,9 @@ |
21 | } |
22 | |
23 | func list(storage gwacl.StorageContext) { |
24 | - res, err := storage.ListBlobs(container) |
25 | + request := &gwacl.ListBlobsRequest{ |
26 | + Container: container, Prefix: prefix} |
27 | + res, err := storage.ListAllBlobs(request) |
28 | if err != nil { |
29 | dumpError(err) |
30 | return |
31 | |
32 | === modified file 'storage.go' |
33 | --- storage.go 2013-04-22 16:17:23 +0000 |
34 | +++ storage.go 2013-06-10 15:57:40 +0000 |
35 | @@ -11,6 +11,7 @@ |
36 | "io" |
37 | . "launchpad.net/gwacl/logging" |
38 | "strconv" |
39 | + "strings" |
40 | ) |
41 | |
42 | // UploadBlockBlob uses PutBlock and PutBlockList API operations to upload |
43 | @@ -45,3 +46,37 @@ |
44 | Debugf("Committing %d blocks.\n", len(blockList.Items)) |
45 | return context.PutBlockList(container, filename, blockList) |
46 | } |
47 | + |
48 | +// ListAllBlobs requests from the API a list of blobs in a container. |
49 | +func (context *StorageContext) ListAllBlobs(request *ListBlobsRequest) (*BlobEnumerationResults, error) { |
50 | + blobs := make([]Blob, 0) |
51 | + var batch *BlobEnumerationResults |
52 | + |
53 | + // Request the initial result, using the empty marker. Then, for as long |
54 | + // as the result has a nonempty NextMarker, request the next batch using |
55 | + // that marker. |
56 | + // This loop is very similar to the one in ListContainers(). |
57 | + for marker, nextMarker := "", "x"; nextMarker != ""; marker = nextMarker { |
58 | + var err error |
59 | + // Don't use := here or you'll shadow variables from the function's |
60 | + // outer scopes. |
61 | + request.Marker = marker |
62 | + batch, err = context.ListBlobs(request) |
63 | + if err != nil { |
64 | + return nil, err |
65 | + } |
66 | + // The response may contain a NextMarker field, to let us request a |
67 | + // subsequent batch of results. The XML parser won't trim whitespace out |
68 | + // of the marker tag, so we do that here. |
69 | + nextMarker = strings.TrimSpace(batch.NextMarker) |
70 | + blobs = append(blobs, batch.Blobs...) |
71 | + } |
72 | + |
73 | + // There's more in a BlobsEnumerationResults than just the blobs. |
74 | + // Return the latest batch, but give it the full cumulative blobs list |
75 | + // instead of just the last batch. |
76 | + // To the caller, this will look like they made one call to Azure's |
77 | + // List Blobs method, but batch size was unlimited. |
78 | + batch.Blobs = blobs |
79 | + return batch, nil |
80 | +} |
81 | |
82 | === modified file 'storage_base.go' |
83 | --- storage_base.go 2013-05-01 05:16:08 +0000 |
84 | +++ storage_base.go 2013-06-10 15:57:40 +0000 |
85 | @@ -383,18 +383,27 @@ |
86 | return batch, nil |
87 | } |
88 | |
89 | -// getListBlobsBatch calls the "List Blobs" operation on the storage API, and |
90 | -// returns a single batch of results. |
91 | -// The marker argument should be empty for a new List Blobs request. For |
92 | -// subsequent calls to get additional batches of the same result, pass the |
93 | +type ListBlobsRequest struct { |
94 | + Container string |
95 | + Marker string |
96 | + Prefix string |
97 | +} |
98 | + |
99 | +// ListBlobs calls the "List Blobs" operation on the storage API, and returns |
100 | +// a single batch of results. |
101 | +// The request.Marker argument should be empty for a new List Blobs request. |
102 | +// For subsequent calls to get additional batches of the same result, pass the |
103 | // NextMarker from the previous call's result. |
104 | -func (context *StorageContext) getListBlobsBatch(container, marker string) (*BlobEnumerationResults, error) { |
105 | +func (context *StorageContext) ListBlobs(request *ListBlobsRequest) (*BlobEnumerationResults, error) { |
106 | uri := addURLQueryParams( |
107 | - context.getContainerURL(container), |
108 | + context.getContainerURL(request.Container), |
109 | "restype", "container", |
110 | "comp", "list") |
111 | - if marker != "" { |
112 | - uri = addURLQueryParams(uri, "marker", marker) |
113 | + if request.Marker != "" { |
114 | + uri = addURLQueryParams(uri, "marker", request.Marker) |
115 | + } |
116 | + if request.Prefix != "" { |
117 | + uri = addURLQueryParams(uri, "prefix", request.Prefix) |
118 | } |
119 | blobs := BlobEnumerationResults{} |
120 | _, err := context.performRequest(requestParams{ |
121 | @@ -410,39 +419,6 @@ |
122 | return &blobs, err |
123 | } |
124 | |
125 | -// ListBlobs requests from the API a list of blobs in a container. |
126 | -func (context *StorageContext) ListBlobs(container string) (*BlobEnumerationResults, error) { |
127 | - blobs := make([]Blob, 0) |
128 | - var batch *BlobEnumerationResults |
129 | - |
130 | - // Request the initial result, using the empty marker. Then, for as long |
131 | - // as the result has a nonempty NextMarker, request the next batch using |
132 | - // that marker. |
133 | - // This loop is very similar to the one in ListContainers(). |
134 | - for marker, nextMarker := "", "x"; nextMarker != ""; marker = nextMarker { |
135 | - var err error |
136 | - // Don't use := here or you'll shadow variables from the function's |
137 | - // outer scopes. |
138 | - batch, err = context.getListBlobsBatch(container, marker) |
139 | - if err != nil { |
140 | - return nil, err |
141 | - } |
142 | - // The response may contain a NextMarker field, to let us request a |
143 | - // subsequent batch of results. The XML parser won't trim whitespace out |
144 | - // of the marker tag, so we do that here. |
145 | - nextMarker = strings.TrimSpace(batch.NextMarker) |
146 | - blobs = append(blobs, batch.Blobs...) |
147 | - } |
148 | - |
149 | - // There's more in a BlobsEnumerationResults than just the blobs. |
150 | - // Return the latest batch, but give it the full cumulative blobs list |
151 | - // instead of just the last batch. |
152 | - // To the caller, this will look like they made one call to Azure's |
153 | - // List Blobs method, but batch size was unlimited. |
154 | - batch.Blobs = blobs |
155 | - return batch, nil |
156 | -} |
157 | - |
158 | // Send a request to the storage service to create a new container. If the |
159 | // request fails, error is non-nil. |
160 | func (context *StorageContext) CreateContainer(container string) error { |
161 | |
162 | === modified file 'storage_base_test.go' |
163 | --- storage_base_test.go 2013-04-30 14:48:05 +0000 |
164 | +++ storage_base_test.go 2013-06-10 15:57:40 +0000 |
165 | @@ -533,7 +533,8 @@ |
166 | } |
167 | transport := &TestTransport{Response: response} |
168 | context.client = &http.Client{Transport: transport} |
169 | - results, err := context.ListBlobs("container") |
170 | + request := &ListBlobsRequest{Container: "container"} |
171 | + results, err := context.ListBlobs(request) |
172 | c.Assert(err, IsNil) |
173 | c.Check(transport.Request.URL.String(), Matches, context.getContainerURL("container")+"?.*") |
174 | c.Check(transport.Request.URL.Query(), DeepEquals, url.Values{ |
175 | @@ -550,7 +551,8 @@ |
176 | transport := &TestTransport{Error: error} |
177 | context := makeStorageContext() |
178 | context.client = &http.Client{Transport: transport} |
179 | - _, err := context.ListBlobs("container") |
180 | + request := &ListBlobsRequest{Container: "container"} |
181 | + _, err := context.ListBlobs(request) |
182 | c.Assert(err, NotNil) |
183 | } |
184 | |
185 | @@ -563,63 +565,21 @@ |
186 | transport := &TestTransport{Response: response} |
187 | context := makeStorageContext() |
188 | context.client = &http.Client{Transport: transport} |
189 | - _, err := context.ListBlobs("container") |
190 | + request := &ListBlobsRequest{Container: "container"} |
191 | + _, err := context.ListBlobs(request) |
192 | c.Assert(err, NotNil) |
193 | } |
194 | |
195 | -// ListBlobs combines multiple batches of output. |
196 | -func (suite *TestListBlobs) TestBatchedResult(c *C) { |
197 | - firstBlob := "blob1" |
198 | - lastBlob := "blob2" |
199 | - marker := "moreplease" |
200 | - firstBatch := http.Response{ |
201 | - StatusCode: http.StatusOK, |
202 | - Body: makeResponseBody(fmt.Sprintf(` |
203 | - <EnumerationResults> |
204 | - <Blobs> |
205 | - <Blob> |
206 | - <Name>%s</Name> |
207 | - </Blob> |
208 | - </Blobs> |
209 | - <NextMarker>%s</NextMarker> |
210 | - </EnumerationResults> |
211 | - `, firstBlob, marker)), |
212 | - } |
213 | - lastBatch := http.Response{ |
214 | - StatusCode: http.StatusOK, |
215 | - Body: makeResponseBody(fmt.Sprintf(` |
216 | - <EnumerationResults> |
217 | - <Blobs> |
218 | - <Blob> |
219 | - <Name>%s</Name> |
220 | - </Blob> |
221 | - </Blobs> |
222 | - </EnumerationResults> |
223 | - `, lastBlob)), |
224 | - } |
225 | - transport := TestTransport2{} |
226 | - transport.AddExchange(&firstBatch, nil) |
227 | - transport.AddExchange(&lastBatch, nil) |
228 | - context := makeStorageContext() |
229 | - context.client = &http.Client{Transport: &transport} |
230 | - |
231 | - blobs, err := context.ListBlobs("mycontainer") |
232 | - c.Assert(err, IsNil) |
233 | - |
234 | - c.Check(len(blobs.Blobs), Equals, 2) |
235 | - c.Check(blobs.Blobs[0].Name, Equals, firstBlob) |
236 | - c.Check(blobs.Blobs[1].Name, Equals, lastBlob) |
237 | -} |
238 | - |
239 | -func (suite *TestListBlobs) TestGetListBlobsBatchPassesMarker(c *C) { |
240 | +func (suite *TestListBlobs) TestListBlobsPassesMarker(c *C) { |
241 | transport := TestTransport2{} |
242 | transport.AddExchange(&http.Response{StatusCode: http.StatusOK, Body: Empty}, nil) |
243 | context := makeStorageContext() |
244 | context.client = &http.Client{Transport: &transport} |
245 | |
246 | - // Call getListBlobsBatch. This will fail because of the empty |
247 | + // Call ListBlobs. This will fail because of the empty |
248 | // response, but no matter. We only care about the request. |
249 | - _, err := context.getListBlobsBatch("mycontainer", "thismarkerhere") |
250 | + request := &ListBlobsRequest{Container: "mycontainer", Marker: "thismarkerhere"} |
251 | + _, err := context.ListBlobs(request) |
252 | c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*") |
253 | c.Assert(transport.ExchangeCount, Equals, 1) |
254 | |
255 | @@ -629,14 +589,15 @@ |
256 | c.Check(values["marker"], DeepEquals, []string{"thismarkerhere"}) |
257 | } |
258 | |
259 | -func (suite *TestListBlobs) TestGetListBlobsBatchDoesNotPassEmptyMarker(c *C) { |
260 | +func (suite *TestListBlobs) TestListBlobsDoesNotPassEmptyMarker(c *C) { |
261 | transport := TestTransport2{} |
262 | transport.AddExchange(&http.Response{StatusCode: http.StatusOK, Body: Empty}, nil) |
263 | context := makeStorageContext() |
264 | context.client = &http.Client{Transport: &transport} |
265 | |
266 | // The error is OK. We only care about the request. |
267 | - _, err := context.getListBlobsBatch("mycontainer", "") |
268 | + request := &ListBlobsRequest{Container: "mycontainer"} |
269 | + _, err := context.ListBlobs(request) |
270 | c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*") |
271 | c.Assert(transport.ExchangeCount, Equals, 1) |
272 | |
273 | @@ -648,6 +609,45 @@ |
274 | c.Check(marker, DeepEquals, []string(nil)) |
275 | } |
276 | |
277 | +func (suite *TestListBlobs) TestListBlobsPassesPrefix(c *C) { |
278 | + transport := TestTransport2{} |
279 | + transport.AddExchange(&http.Response{StatusCode: http.StatusOK, Body: Empty}, nil) |
280 | + context := makeStorageContext() |
281 | + context.client = &http.Client{Transport: &transport} |
282 | + |
283 | + // Call ListBlobs. This will fail because of the empty |
284 | + // response, but no matter. We only care about the request. |
285 | + request := &ListBlobsRequest{Container: "mycontainer", Prefix: "thisprefixhere"} |
286 | + _, err := context.ListBlobs(request) |
287 | + c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*") |
288 | + c.Assert(transport.ExchangeCount, Equals, 1) |
289 | + |
290 | + query := transport.Exchanges[0].Request.URL.RawQuery |
291 | + values, err := url.ParseQuery(query) |
292 | + c.Assert(err, IsNil) |
293 | + c.Check(values["prefix"], DeepEquals, []string{"thisprefixhere"}) |
294 | +} |
295 | + |
296 | +func (suite *TestListBlobs) TestListBlobsDoesNotPassEmptyPrefix(c *C) { |
297 | + transport := TestTransport2{} |
298 | + transport.AddExchange(&http.Response{StatusCode: http.StatusOK, Body: Empty}, nil) |
299 | + context := makeStorageContext() |
300 | + context.client = &http.Client{Transport: &transport} |
301 | + |
302 | + // The error is OK. We only care about the request. |
303 | + request := &ListBlobsRequest{Container: "mycontainer"} |
304 | + _, err := context.ListBlobs(request) |
305 | + c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*") |
306 | + c.Assert(transport.ExchangeCount, Equals, 1) |
307 | + |
308 | + query := transport.Exchanges[0].Request.URL.RawQuery |
309 | + values, err := url.ParseQuery(query) |
310 | + c.Assert(err, IsNil) |
311 | + prefix, present := values["prefix"] |
312 | + c.Check(present, Equals, false) |
313 | + c.Check(prefix, DeepEquals, []string(nil)) |
314 | +} |
315 | + |
316 | type TestCreateContainer struct{} |
317 | |
318 | var _ = Suite(&TestCreateContainer{}) |
319 | |
320 | === modified file 'storage_test.go' |
321 | --- storage_test.go 2013-04-29 11:52:04 +0000 |
322 | +++ storage_test.go 2013-06-10 15:57:40 +0000 |
323 | @@ -91,3 +91,144 @@ |
324 | expected += "</BlockList>" |
325 | c.Check(string(body), Equals, expected) |
326 | } |
327 | + |
328 | +type testListAllBlobs struct{} |
329 | + |
330 | +var _ = Suite(&testListAllBlobs{}) |
331 | + |
332 | +// The ListAllBlobs Storage API call returns a BlobEnumerationResults struct |
333 | +// on success. |
334 | +func (suite *testListAllBlobs) Test(c *C) { |
335 | + response_body := ` |
336 | + <?xml version="1.0" encoding="utf-8"?> |
337 | + <EnumerationResults ContainerName="http://myaccount.blob.core.windows.net/mycontainer"> |
338 | + <Prefix>prefix</Prefix> |
339 | + <Marker>marker</Marker> |
340 | + <MaxResults>maxresults</MaxResults> |
341 | + <Delimiter>delimiter</Delimiter> |
342 | + <Blobs> |
343 | + <Blob> |
344 | + <Name>blob-name</Name> |
345 | + <Snapshot>snapshot-date-time</Snapshot> |
346 | + <Url>blob-address</Url> |
347 | + <Properties> |
348 | + <Last-Modified>last-modified</Last-Modified> |
349 | + <Etag>etag</Etag> |
350 | + <Content-Length>size-in-bytes</Content-Length> |
351 | + <Content-Type>blob-content-type</Content-Type> |
352 | + <Content-Encoding /> |
353 | + <Content-Language /> |
354 | + <Content-MD5 /> |
355 | + <Cache-Control /> |
356 | + <x-ms-blob-sequence-number>sequence-number</x-ms-blob-sequence-number> |
357 | + <BlobType>blobtype</BlobType> |
358 | + <LeaseStatus>leasestatus</LeaseStatus> |
359 | + <LeaseState>leasestate</LeaseState> |
360 | + <LeaseDuration>leasesduration</LeaseDuration> |
361 | + <CopyId>id</CopyId> |
362 | + <CopyStatus>copystatus</CopyStatus> |
363 | + <CopySource>copysource</CopySource> |
364 | + <CopyProgress>copyprogress</CopyProgress> |
365 | + <CopyCompletionTime>copycompletiontime</CopyCompletionTime> |
366 | + <CopyStatusDescription>copydesc</CopyStatusDescription> |
367 | + </Properties> |
368 | + <Metadata> |
369 | + <MetaName1>metadataname1</MetaName1> |
370 | + <MetaName2>metadataname2</MetaName2> |
371 | + </Metadata> |
372 | + </Blob> |
373 | + <BlobPrefix> |
374 | + <Name>blob-prefix</Name> |
375 | + </BlobPrefix> |
376 | + </Blobs> |
377 | + <NextMarker /> |
378 | + </EnumerationResults>` |
379 | + context := makeStorageContext() |
380 | + response := &http.Response{ |
381 | + Status: fmt.Sprintf("%d", http.StatusOK), |
382 | + StatusCode: http.StatusOK, |
383 | + Body: makeResponseBody(response_body), |
384 | + } |
385 | + transport := &TestTransport{Response: response} |
386 | + context.client = &http.Client{Transport: transport} |
387 | + request := &ListBlobsRequest{Container: "container"} |
388 | + results, err := context.ListAllBlobs(request) |
389 | + c.Assert(err, IsNil) |
390 | + c.Check(transport.Request.URL.String(), Matches, context.getContainerURL("container")+"?.*") |
391 | + c.Check(transport.Request.URL.Query(), DeepEquals, url.Values{ |
392 | + "restype": {"container"}, |
393 | + "comp": {"list"}, |
394 | + }) |
395 | + c.Check(transport.Request.Header.Get("Authorization"), Not(Equals), "") |
396 | + c.Assert(results, NotNil) |
397 | +} |
398 | + |
399 | +// Client-side errors from the HTTP client are propagated back to the caller. |
400 | +func (suite *testListAllBlobs) TestError(c *C) { |
401 | + error := fmt.Errorf("canned-error") |
402 | + transport := &TestTransport{Error: error} |
403 | + context := makeStorageContext() |
404 | + context.client = &http.Client{Transport: transport} |
405 | + request := &ListBlobsRequest{Container: "container"} |
406 | + _, err := context.ListAllBlobs(request) |
407 | + c.Assert(err, NotNil) |
408 | +} |
409 | + |
410 | +// Server-side errors are propagated back to the caller. |
411 | +func (suite *testListAllBlobs) TestErrorResponse(c *C) { |
412 | + response := &http.Response{ |
413 | + Status: fmt.Sprintf("%d", http.StatusNotFound), |
414 | + StatusCode: http.StatusNotFound, |
415 | + } |
416 | + transport := &TestTransport{Response: response} |
417 | + context := makeStorageContext() |
418 | + context.client = &http.Client{Transport: transport} |
419 | + request := &ListBlobsRequest{Container: "container"} |
420 | + _, err := context.ListAllBlobs(request) |
421 | + c.Assert(err, NotNil) |
422 | +} |
423 | + |
424 | +// ListAllBlobs combines multiple batches of output. |
425 | +func (suite *testListAllBlobs) TestBatchedResult(c *C) { |
426 | + firstBlob := "blob1" |
427 | + lastBlob := "blob2" |
428 | + marker := "moreplease" |
429 | + firstBatch := http.Response{ |
430 | + StatusCode: http.StatusOK, |
431 | + Body: makeResponseBody(fmt.Sprintf(` |
432 | + <EnumerationResults> |
433 | + <Blobs> |
434 | + <Blob> |
435 | + <Name>%s</Name> |
436 | + </Blob> |
437 | + </Blobs> |
438 | + <NextMarker>%s</NextMarker> |
439 | + </EnumerationResults> |
440 | + `, firstBlob, marker)), |
441 | + } |
442 | + lastBatch := http.Response{ |
443 | + StatusCode: http.StatusOK, |
444 | + Body: makeResponseBody(fmt.Sprintf(` |
445 | + <EnumerationResults> |
446 | + <Blobs> |
447 | + <Blob> |
448 | + <Name>%s</Name> |
449 | + </Blob> |
450 | + </Blobs> |
451 | + </EnumerationResults> |
452 | + `, lastBlob)), |
453 | + } |
454 | + transport := TestTransport2{} |
455 | + transport.AddExchange(&firstBatch, nil) |
456 | + transport.AddExchange(&lastBatch, nil) |
457 | + context := makeStorageContext() |
458 | + context.client = &http.Client{Transport: &transport} |
459 | + |
460 | + request := &ListBlobsRequest{Container: "mycontainer"} |
461 | + blobs, err := context.ListAllBlobs(request) |
462 | + c.Assert(err, IsNil) |
463 | + |
464 | + c.Check(len(blobs.Blobs), Equals, 2) |
465 | + c.Check(blobs.Blobs[0].Name, Equals, firstBlob) |
466 | + c.Check(blobs.Blobs[1].Name, Equals, lastBlob) |
467 | +} |
(You gotta wonder why Go doesn't have default args :/)
The first thing that popped into my head was, do you think that we should put mandatory parameters as separate args to the ListAllBlobs function, and reserve ListBlobsRequest for optional params only? It seems as though you don't check for the mandatory params anyway, but it would obviate that param checking and is more explicit.
Second thing: the tests nearly all have the same set-up code for the fake transport. I think we need a factored test helper for this as all the setup detracts from the test code's intention. I don't think it should be done in this branch though, a follow-up one is better.
Also - I am wondering if we need some factory methods to create all these huge blobs of XML we're passing around? It's not entirely clear what we need though.
Everything else looks pretty good to me, thanks for all the extra test cases!