Merge lp:~jtv/gwacl/list-images into lp:gwacl

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: 92
Merged at revision: 85
Proposed branch: lp:~jtv/gwacl/list-images
Merge into: lp:gwacl
Diff against target: 185 lines (+141/-2)
4 files modified
managementapi.go (+14/-0)
managementapi_test.go (+83/-0)
test_helpers.go (+2/-2)
xmlobjects.go (+42/-0)
To merge this branch: bzr merge lp:~jtv/gwacl/list-images
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+156329@code.launchpad.net

Commit message

Support listing of OS images.

Description of the change

I kept XML annotations minimal: we've been listing the XML tag names but they're optional where identical to the names of their respective struct fields. I continued our tradition of upper-casing acronyms in the struct names even if Microsoft sometimes (but not always!) writes them in mixed case. I may write up a separate branch ensuring consistency, so that we'll always capitalize these fields in a predictable way — even if we're not actually consistent with Microsoft's spelling.

For testing I went with a different approach than we've been doing: one test verifies that the function makes the expected request, and another verifies the call's outcome. I think that ends up being much easier to read than a flurry of checks at the end to assert very different aspects of what happened. Concentrating the tests into big "check everything the way I usually do" functions reduces check size but makes it harder to follow what is being checked where, or how to add new checks. It also harms flexibility.

What we see at work here is the old "big tests attract more code" antipattern. It's caused by expensive test setup, and by a lack of dependency injection forcing the unit-testing into integration-style tests. Go forces these styles on us, but I reduced test setup *per test* slightly in that now I only need to rig one fake dispatcher per test, with each serving a more specific goal.

Jeroen

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :

Nice change! There's quite a few niggling bits to change I think so I marked it needs fixing so I can take another look later.

8 +// ListOSImages retrieves the list of available operating system disk images
9 +// from the Azure management API.
10 +// Images are returned in the order in which Azure lists them.
11 +func (api *ManagementAPI) ListOSImages() ([]OSImage, error)

Can you please get into the habit of adding the URL to the Azure API reference in the comments for these exported API methods. It'll really help newcomers understand what it's doing, and help maintainers fix bugs quicker!

45 +func (suite *managementAPISuite) TestListOSImagesReturnsImages(c *C) {
46 + const (
47 + logicalSize = 199.0
48 + label = "osimagelabel"
49 + mediaLink = "http://storage.example.com/12345"
50 + name = "imagename"
51 + eula = "firstborn.txt"
52 + description = "Description."
53 + )

I don't find this const section useful at all, FWIW. I'd just throw the values into the string formatting args for Sprintf. At least if you're going to use consts/variables, I'd use MakeRandomString() and friends.

80 + c.Check(osimage.LogicalSizeInGB, Equals, float32(logicalSize))
81 + c.Check(osimage.Label, Equals, label)
82 + c.Check(osimage.MediaLink, Equals, mediaLink)
83 + c.Check(osimage.Name, Equals, name)
84 + c.Check(osimage.OS, Equals, "Linux")
85 + c.Check(osimage.EULA, Equals, eula)
86 + c.Check(osimage.Description, Equals, description)

You can do this in a single check if you make a single "expected" struct and do a DeepEquals, as is done for some of the storage tests. The code reads better that way and you also get a single failure listing all differences at once.

103 + c.Assert(len(listing), Equals, 3)
104 + c.Check(listing[0].Name, Equals, "image2")
105 + c.Check(listing[1].Name, Equals, "image1")
106 + c.Check(listing[2].Name, Equals, "image3")

Same here.

122 +// rigPreparedResponseDispatcher sets up a request dispatcher that returns,
123 +// for each consecutive request, the next of a series of prepared responses.

This reminds me of Mocker. Out of interest, do you prefer its record/replay mocking style over the simpler Mock?

+type OSImage struct {
154 + AffinityGroup string `xml:",omitempty"`
155 + Category string
156 + Label string
[snip]

You said you were leaving out the xml tags, but please don't do that. Gavin and I talked about this in Brisbane too, but we decided that we need a strong separation between external API names and internal code struct names - if the Azure API changes then it would either mean code changes everywhere or adding the tag anyway. Adding the tags also reinforces to the code reader that its an XML-filled property.

review: Needs Fixing
lp:~jtv/gwacl/list-images updated
92. By Jeroen T. Vermeulen

Review changes.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (4.0 KiB)

> Nice change! There's quite a few niggling bits to change I think so I marked
> it needs fixing so I can take another look later.
>
> 8 +// ListOSImages retrieves the list of available operating system disk
> images
> 9 +// from the Azure management API.
> 10 +// Images are returned in the order in which Azure lists them.
> 11 +func (api *ManagementAPI) ListOSImages() ([]OSImage, error)
>
> Can you please get into the habit of adding the URL to the Azure API reference
> in the comments for these exported API methods. It'll really help newcomers
> understand what it's doing, and help maintainers fix bugs quicker!

Done.

> 45 +func (suite *managementAPISuite) TestListOSImagesReturnsImages(c *C)
> {
> 46 + const (
> 47 + logicalSize = 199.0
> 48 + label = "osimagelabel"
> 49 + mediaLink = "http://storage.example.com/12345"
> 50 + name = "imagename"
> 51 + eula = "firstborn.txt"
> 52 + description = "Description."
> 53 + )
>
> I don't find this const section useful at all, FWIW. I'd just throw the
> values into the string formatting args for Sprintf. At least if you're going
> to use consts/variables, I'd use MakeRandomString() and friends.

Well the point is of course, as you saw right after writing this, that they had to be repeated exactly.

> 80 + c.Check(osimage.LogicalSizeInGB, Equals, float32(logicalSize))
> 81 + c.Check(osimage.Label, Equals, label)
> 82 + c.Check(osimage.MediaLink, Equals, mediaLink)
> 83 + c.Check(osimage.Name, Equals, name)
> 84 + c.Check(osimage.OS, Equals, "Linux")
> 85 + c.Check(osimage.EULA, Equals, eula)
> 86 + c.Check(osimage.Description, Equals, description)
>
> You can do this in a single check if you make a single "expected" struct and
> do a DeepEquals, as is done for some of the storage tests. The code reads
> better that way and you also get a single failure listing all differences at
> once.

Done.

> 103 + c.Assert(len(listing), Equals, 3)
> 104 + c.Check(listing[0].Name, Equals, "image2")
> 105 + c.Check(listing[1].Name, Equals, "image1")
> 106 + c.Check(listing[2].Name, Equals, "image3")
>
> Same here.

Can't DeepEquals the objects here, or the error output will be completely impossible to figure out. After all what's in the objects really doesn't matter — only the ordering does. So instead I created one array with the desired names and another with the received names, and compared those.

Extracting an array of Name attributes from the array of objects does make the code long and cumbersome, of course. But such is the price of Go: don't struggle, you'll just sink faster. The more generic and data-driven I make this test, the worse it gets.

> 122 +// rigPreparedResponseDispatcher sets up a request dispatcher that
> returns,
> 123 +// for each consecutive request, the next of a series of prepared
> responses.
>
> This reminds me of Mocker. Out of interest, do you prefer its record/replay
> mocking style over the simpler Mock?

I don't think I've ever used this...

Read more...

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Tuesday 02 Apr 2013 03:25:27 you wrote:
> Well the point is of course, as you saw right after writing this, that they
> had to be repeated exactly.
> > 80 + c.Check(osimage.LogicalSizeInGB, Equals,
> > float32(logicalSize))
> > 81 + c.Check(osimage.Label, Equals, label)
> > 82 + c.Check(osimage.MediaLink, Equals, mediaLink)
> > 83 + c.Check(osimage.Name, Equals, name)
> > 84 + c.Check(osimage.OS, Equals, "Linux")
> > 85 + c.Check(osimage.EULA, Equals, eula)
> > 86 + c.Check(osimage.Description, Equals, description)
> >
> > You can do this in a single check if you make a single "expected" struct
> > and do a DeepEquals, as is done for some of the storage tests. The code
> > reads better that way and you also get a single failure listing all
> > differences at once.
>
> Done.

What you've done is loads better now, thanks :)

>
> > 103 + c.Assert(len(listing), Equals, 3)
> > 104 + c.Check(listing[0].Name, Equals, "image2")
> > 105 + c.Check(listing[1].Name, Equals, "image1")
> > 106 + c.Check(listing[2].Name, Equals, "image3")
> >
> > Same here.
>
> Can't DeepEquals the objects here, or the error output will be completely
> impossible to figure out. After all what's in the objects really doesn't
> matter — only the ordering does. So instead I created one array with the
> desired names and another with the received names, and compared those.

Oh, arse :/

>
> Extracting an array of Name attributes from the array of objects does make
> the code long and cumbersome, of course. But such is the price of Go:
> don't struggle, you'll just sink faster. The more generic and data-driven
> I make this test, the worse it gets.

:(

Our best practices are much harder to implement in Go than we are used to with
Python.

> > 122 +// rigPreparedResponseDispatcher sets up a request dispatcher
> > that
> > returns,
> > 123 +// for each consecutive request, the next of a series of prepared
> > responses.
> >
> > This reminds me of Mocker. Out of interest, do you prefer its
> > record/replay mocking style over the simpler Mock?
>
> I don't think I've ever used this function myself, so I'm probably not the
> best person to ask. I just saw that its documentation was broken and fixed
> it.

Ok :)

> > +type OSImage struct {
> > 154 + AffinityGroup string `xml:",omitempty"`
> > 155 + Category string
> > 156 + Label string
> > [snip]
> >
> > You said you were leaving out the xml tags, but please don't do that.
> > Gavin and I talked about this in Brisbane too, but we decided that we
> > need a strong separation between external API names and internal code
> > struct names - if the Azure API changes then it would either mean code
> > changes everywhere or adding the tag anyway. Adding the tags also
> > reinforces to the code reader that its an XML-filled property.
>
> Okay, I put them in. A minor disadvantage is that this obscures the
> differences in capitalization between gwacl and Azure.

It does?

Thanks for the changes, go ahead and land!

Revision history for this message
Julian Edwards (julian-edwards) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'managementapi.go'
--- managementapi.go 2013-03-29 16:03:54 +0000
+++ managementapi.go 2013-04-02 03:19:21 +0000
@@ -65,6 +65,20 @@
65 return nil65 return nil
66}66}
6767
68// ListOSImages retrieves the list of available operating system disk images
69// from the Azure management API.
70// Images are returned in the order in which Azure lists them.
71// http://msdn.microsoft.com/en-us/library/windowsazure/jj157191.aspx
72func (api *ManagementAPI) ListOSImages() ([]OSImage, error) {
73 response, err := api.session.get("services/images")
74 if err != nil {
75 return nil, err
76 }
77 images := Images{}
78 err = xml.Unmarshal(response.Body, &images)
79 return images.Images, err
80}
81
68// ListHostedServiceDescriptors loads a list of HostedServiceDescriptor objects from the82// ListHostedServiceDescriptors loads a list of HostedServiceDescriptor objects from the
69// Azure management API. 83// Azure management API.
70// HostedServiceDescriptor objects contains a small subset of the fields present in84// HostedServiceDescriptor objects contains a small subset of the fields present in
7185
=== modified file 'managementapi_test.go'
--- managementapi_test.go 2013-03-29 16:03:54 +0000
+++ managementapi_test.go 2013-04-02 03:19:21 +0000
@@ -191,6 +191,89 @@
191 c.Check(err, ErrorMatches, ".*no operation header.*")191 c.Check(err, ErrorMatches, ".*no operation header.*")
192}192}
193193
194func (suite *managementAPISuite) TestListOSImagesRequestsListing(c *C) {
195 api := makeAPI(c)
196 rigFixedResponseDispatcher(&x509Response{StatusCode: http.StatusOK, Body: []byte("<Images></Images>")})
197 requests := make([]*x509Request, 0)
198 rigRecordingDispatcher(&requests)
199
200 _, err := api.ListOSImages()
201 c.Assert(err, IsNil)
202
203 c.Assert(len(requests), Equals, 1)
204 c.Check(requests[0].URL, Equals, api.session.composeURL("services/images"))
205}
206
207func (suite *managementAPISuite) TestListOSImagesReturnsImages(c *C) {
208 expectedImage := OSImage{
209 LogicalSizeInGB: 199.0,
210 Label: MakeRandomString(10),
211 MediaLink: "http://storage.example.com/" + MakeRandomString(10),
212 Name: MakeRandomString(10),
213 OS: "Linux",
214 EULA: "http://eula.example.com/" + MakeRandomString(10),
215 Description: MakeRandomString(10),
216 RecommendedVMSize: "Medium",
217 }
218 body := fmt.Sprintf(`
219 <Images>
220 <OSImage>
221 <LogicalSizeInGB>%f</LogicalSizeInGB>
222 <Label>%s</Label>
223 <MediaLink>%s</MediaLink>
224 <Name>%s</Name>
225 <OS>%s</OS>
226 <Eula>%s</Eula>
227 <Description>%s</Description>
228 <RecommendedVMSize>%s</RecommendedVMSize>
229 </OSImage>
230 </Images>
231 `,
232 expectedImage.LogicalSizeInGB, expectedImage.Label,
233 expectedImage.MediaLink, expectedImage.Name, expectedImage.OS,
234 expectedImage.EULA, expectedImage.Description,
235 expectedImage.RecommendedVMSize)
236 api := makeAPI(c)
237 rigFixedResponseDispatcher(&x509Response{
238 StatusCode: http.StatusOK,
239 Body: []byte(body),
240 })
241
242 listing, err := api.ListOSImages()
243 c.Assert(err, IsNil)
244
245 c.Assert(len(listing), Equals, 1)
246 c.Check(listing[0], DeepEquals, expectedImage)
247}
248
249func (suite *managementAPISuite) TestListOSImagesPreservesOrdering(c *C) {
250 imageNames := []string{
251 MakeRandomString(5),
252 MakeRandomString(5),
253 MakeRandomString(5),
254 }
255 body := fmt.Sprintf(`
256 <Images>
257 <OSImage><Name>%s</Name></OSImage>
258 <OSImage><Name>%s</Name></OSImage>
259 <OSImage><Name>%s</Name></OSImage>
260 </Images>
261 `,
262 imageNames[0], imageNames[1], imageNames[2])
263 api := makeAPI(c)
264 rigFixedResponseDispatcher(&x509Response{StatusCode: http.StatusOK, Body: []byte(body)})
265
266 listing, err := api.ListOSImages()
267 c.Assert(err, IsNil)
268
269 c.Assert(len(listing), Equals, 3)
270 receivedNames := make([]string, 3)
271 for index := range listing {
272 receivedNames[index] = listing[index].Name
273 }
274 c.Check(receivedNames, DeepEquals, imageNames)
275}
276
194func (suite *managementAPISuite) TestListHostedServiceDescriptors(c *C) {277func (suite *managementAPISuite) TestListHostedServiceDescriptors(c *C) {
195 api := makeAPI(c)278 api := makeAPI(c)
196 fixedResponse := x509Response{279 fixedResponse := x509Response{
197280
=== modified file 'test_helpers.go'
--- test_helpers.go 2013-03-29 01:27:37 +0000
+++ test_helpers.go 2013-04-02 03:19:21 +0000
@@ -217,8 +217,8 @@
217 errorObject error217 errorObject error
218}218}
219219
220// rigPreparedResponseDispatcher sets up a request dispatcher that all the provided220// rigPreparedResponseDispatcher sets up a request dispatcher that returns,
221// reponses, one after the other each time the dispatcher is called.221// for each consecutive request, the next of a series of prepared responses.
222func rigPreparedResponseDispatcher(responses []DispatcherResponse) {222func rigPreparedResponseDispatcher(responses []DispatcherResponse) {
223 index := 0223 index := 0
224 _X509Dispatcher = func(*x509Session, *x509Request) (*x509Response, error) {224 _X509Dispatcher = func(*x509Session, *x509Request) (*x509Response, error) {
225225
=== modified file 'xmlobjects.go'
--- xmlobjects.go 2013-03-29 10:23:07 +0000
+++ xmlobjects.go 2013-04-02 03:19:21 +0000
@@ -74,6 +74,48 @@
74}74}
7575
76//76//
77// Images bits
78//
79
80// Images is a series of OSImages.
81type Images struct {
82 Images []OSImage `xml:"OSImage"`
83}
84
85func (i *Images) Deserialize(data []byte) error {
86 return xml.Unmarshal(data, i)
87}
88
89//
90// OSImage bits
91//
92
93// OSImage represents a disk image containing an operating system.
94// Confusingly, the Azure API documentation also calls it a VM image.
95type OSImage struct {
96 AffinityGroup string `xml:"AffinityGroup,omitempty"`
97 Category string `xml:"Category"`
98 Label string `xml:"Label"`
99 Location string `xml:"Location,omitempty"`
100 LogicalSizeInGB float32 `xml:"LogicalSizeInGB"`
101 MediaLink string `xml:"MediaLink"`
102 Name string `xml:"Name"`
103 OS string `xml:"OS"`
104 EULA string `xml:"Eula,omitempty"`
105 Description string `xml:"Description,omitempty"`
106 ImageFamily string `xml:"ImageFamily,omitempty"`
107 PublishedDate string `xml:"PublishedDate,omitempty"`
108 IsPremium string `xml:"IsPremium,omitempty"` // Reserved for future use. Seriously. In XML.
109 PrivacyURI string `xml:"PrivacyUri,omitempty"`
110 RecommendedVMSize string `xml:"RecommendedVMSize,omitempty"`
111 PublisherName string `xml:"PublisherName"`
112}
113
114func (i *OSImage) Deserialize(data []byte) error {
115 return xml.Unmarshal(data, i)
116}
117
118//
77// OSVirtualHardDisk bits119// OSVirtualHardDisk bits
78//120//
79121

Subscribers

People subscribed via source and target branches