Merge lp:~jtv/gwacl/http-error into lp:gwacl

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: 69
Merged at revision: 68
Proposed branch: lp:~jtv/gwacl/http-error
Merge into: lp:gwacl
Diff against target: 485 lines (+192/-90)
9 files modified
example/live_example_managementapi.go (+2/-1)
httperror.go (+77/-0)
httperror_test.go (+68/-0)
storage.go (+14/-25)
storage_test.go (+19/-5)
x509session.go (+6/-18)
x509session_test.go (+6/-6)
xmlobjects.go (+0/-17)
xmlobjects_test.go (+0/-18)
To merge this branch: bzr merge lp:~jtv/gwacl/http-error
Reviewer Review Type Date Requested Status
Raphaël Badin (community) Approve
Review via email: mp+155679@code.launchpad.net

Commit message

Harmonize "error based on http response" types.

Description of the change

This code replaces our existing Error and ServerError structs with one interface: HTTPError. It's an "error" but with an added Status() for the HTTP status code.

Underneath, there are two implementations: ServerError represents any HTTP error response. AzureError is an error message from Azure itself, with more details embedded as XML. For now we don't have any need to distinguish between them, so Error() and Status() are the only methods we need. A single factory selects and produces the right implementation, so all kinds of HTTP errors are automatically handled wherever we use the new types. We get better Azure errors in the management API as a free part of the bargain.

You may wonder how HTTPStatus works. It's a type based on int, but it adds a Status() method. And so both ServerError and AzureError implement their Status() methods simply by embedding an HTTPStatus. Effectively it's a member whose name matches its type and whose methods get added to those of the surrounding struct.

There are a few more places I can clean up now, because send() now returns an "error" instead of a "*Error" — so there's no more risk of accidentally converting a nil "*Error" to a non-nil "error." But this branch touches a lot of code (mostly unavoidable changes that just came along) so let's stop it from growing even further!

Jeroen

To post a comment you must log in.
lp:~jtv/gwacl/http-error updated
67. By Jeroen T. Vermeulen

Merge trunk, resolve conflict.

68. By Jeroen T. Vermeulen

Add missing error check that messed up my real-world tests.

Revision history for this message
Raphaël Badin (rvb) wrote :

Looks good, a very nice improvement!

[0]

28 +// HTTPError is an extended version of the standard "error" interface. It
29 +// adds an HTTP status code.
30 +type HTTPError interface {
31 + error
32 + Status() int
33 +}

For clarity, I'd be in favor of renaming Status() into StatusCode().

[1]

253 + c.Check(err, ErrorMatches, ".*102.*")
254 + c.Check(err, ErrorMatches, ".*Frotzed.*")

and

265 + c.Check(err, ErrorMatches, ".*102.*")
266 + c.Check(err, ErrorMatches, ".*Frotzed.*")

and

277 + c.Check(err, ErrorMatches, ".*102.*")
278 + c.Check(err, ErrorMatches, ".*Frotzed.*")

and

289 + c.Check(err, ErrorMatches, ".*146.*")
290 + c.Check(err, ErrorMatches, ".*Frotzed.*")

and

300 + c.Check(err, ErrorMatches, ".*246.*")
301 + c.Check(err, ErrorMatches, ".*Frotzed.*")

I think you should fold these into on statement like this:
c.Check(err, ErrorMatches, ".*102 Frotzed.*")

because the status of the response is "102 Frotzed":

    response := &http.Response{
        Status: "102 Frotzed",
        StatusCode: 102,
        Body: ioutil.NopCloser(strings.NewReader("<Error><Code>Frotzed</Code><Message>failed to put blob</Message></Error>")),
    }

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

On Wednesday 27 Mar 2013 10:27:19 you wrote:
> Review: Approve
>
> Looks good, a very nice improvement!
>
> [0]
>
> 28 +// HTTPError is an extended version of the standard "error" interface.
> It 29 +// adds an HTTP status code.
> 30 +type HTTPError interface {
> 31 + error
> 32 + Status() int
> 33 +}
>
> For clarity, I'd be in favor of renaming Status() into StatusCode().
>
> [1]
>
> 253 + c.Check(err, ErrorMatches, ".*102.*")
> 254 + c.Check(err, ErrorMatches, ".*Frotzed.*")
>
> and
>
> 265 + c.Check(err, ErrorMatches, ".*102.*")
> 266 + c.Check(err, ErrorMatches, ".*Frotzed.*")
>
> and
>
> 277 + c.Check(err, ErrorMatches, ".*102.*")
> 278 + c.Check(err, ErrorMatches, ".*Frotzed.*")
>
> and
>
> 289 + c.Check(err, ErrorMatches, ".*146.*")
> 290 + c.Check(err, ErrorMatches, ".*Frotzed.*")
>
> and
>
> 300 + c.Check(err, ErrorMatches, ".*246.*")
> 301 + c.Check(err, ErrorMatches, ".*Frotzed.*")
>
> I think you should fold these into on statement like this:
> c.Check(err, ErrorMatches, ".*102 Frotzed.*")
>
> because the status of the response is "102 Frotzed":
>
> response := &http.Response{
> Status: "102 Frotzed",
> StatusCode: 102,
> Body:
> ioutil.NopCloser(strings.NewReader("<Error><Code>Frotzed</Code><Message>fai
> led to put blob</Message></Error>")), }

Mmmmm. The actual error should be what's in <Message>. The "102 Frotzed" is
largely irrelevant.

lp:~jtv/gwacl/http-error updated
69. By Jeroen T. Vermeulen

Review suggestion: renamed Status() to StatusCode().

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

> 28 +// HTTPError is an extended version of the standard "error"
> interface. It
> 29 +// adds an HTTP status code.
> 30 +type HTTPError interface {
> 31 + error
> 32 + Status() int
> 33 +}
>
> For clarity, I'd be in favor of renaming Status() into StatusCode().

Done.

> 253 + c.Check(err, ErrorMatches, ".*102.*")
> 254 + c.Check(err, ErrorMatches, ".*Frotzed.*")

> I think you should fold these into on statement like this:
> c.Check(err, ErrorMatches, ".*102 Frotzed.*")
>
> because the status of the response is "102 Frotzed":

That particular status string does not enter into the picture, because http.Response doesn't. The HTTPError factory had to work with both the http package and go-curl.

So instead I use the numerical status code, and add the Go standard library's name for that status code. I don't want all these tests dependent on details of how the error string is composed — there are separate tests for that!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'example/live_example_managementapi.go'
2--- example/live_example_managementapi.go 2013-03-22 07:01:27 +0000
3+++ example/live_example_managementapi.go 2013-03-27 16:36:22 +0000
4@@ -130,7 +130,8 @@
5 role := gwacl.NewRole("ExtraSmall", roleName, []gwacl.LinuxProvisioningConfiguration{*configurationSet}, OSVirtualHardDisk)
6 machineName := makeRandomIdentifier("gwaclmachine", 20)
7 deployment := gwacl.NewDeploymentForCreateVMDeployment(machineName, "Staging", machineName, []gwacl.Role{*role}, "")
8- api.AddDeployment(deployment, hostServiceName)
9+ err = api.AddDeployment(deployment, hostServiceName)
10+ checkError(err)
11 fmt.Println("Done adding VM deployment\n")
12
13 // Wait for the deployment to be created.
14
15=== added file 'httperror.go'
16--- httperror.go 1970-01-01 00:00:00 +0000
17+++ httperror.go 2013-03-27 16:36:22 +0000
18@@ -0,0 +1,77 @@
19+package gwacl
20+
21+import (
22+ "encoding/xml"
23+ "errors"
24+ "fmt"
25+ "net/http"
26+)
27+
28+// HTTPError is an extended version of the standard "error" interface. It
29+// adds an HTTP status code.
30+type HTTPError interface {
31+ error
32+ StatusCode() int
33+}
34+
35+// HTTPStatus is an HTTP status code.
36+type HTTPStatus int
37+
38+// Status returns the HTTP status code as an int.
39+func (s HTTPStatus) StatusCode() int {
40+ return int(s)
41+}
42+
43+// AzureError is an HTTPError returned by the Azure API. It contains an
44+// error message, and an Azure-defined error code.
45+type AzureError struct {
46+ error `xml:"-"`
47+ HTTPStatus `xml:"-"`
48+ Code string `xml:"Code"`
49+ Message string `xml:"Message"`
50+}
51+
52+// *AzureError implements HTTPError.
53+var _ HTTPError = &AzureError{}
54+
55+func (e *AzureError) Error() string {
56+ description := e.error.Error()
57+ status := e.StatusCode()
58+ name := http.StatusText(status)
59+ return fmt.Sprintf("%s: %s - %s (http code %d: %s)", description, e.Code, e.Message, status, name)
60+}
61+
62+// ServerError is a generic HTTPError, without any further helpful information
63+// from the server that we can count on.
64+type ServerError struct {
65+ error
66+ HTTPStatus
67+}
68+
69+// *ServerError implements HTTPError.
70+var _ HTTPError = &ServerError{}
71+
72+func (e *ServerError) Error() string {
73+ description := e.error.Error()
74+ status := e.StatusCode()
75+ name := http.StatusText(status)
76+ return fmt.Sprintf("%s (%d: %s)", description, status, name)
77+}
78+
79+// NewHTTPError returns the appropriate HTTPError implementation for a given
80+// HTTP response.
81+// It takes a status code and response body, rather than just a standard
82+// http.Response object, because it must work with go-curl as well as Go's
83+// standard http package.
84+func newHTTPError(status int, body []byte, description string) HTTPError {
85+ httpStatus := HTTPStatus(status)
86+ baseErr := errors.New(description)
87+ azureError := AzureError{error: baseErr, HTTPStatus: httpStatus}
88+ err := xml.Unmarshal(body, &azureError)
89+ if err != nil {
90+ // It's OK if the response body wasn't actually XML... That just means
91+ // it wasn't a proper AzureError. We have another error type for that.
92+ return &ServerError{error: baseErr, HTTPStatus: httpStatus}
93+ }
94+ return &azureError
95+}
96
97=== added file 'httperror_test.go'
98--- httperror_test.go 1970-01-01 00:00:00 +0000
99+++ httperror_test.go 2013-03-27 16:36:22 +0000
100@@ -0,0 +1,68 @@
101+package gwacl
102+
103+import (
104+ "errors"
105+ "fmt"
106+ . "launchpad.net/gocheck"
107+)
108+
109+type httpErrorSuite struct{}
110+
111+var _ = Suite(&httpErrorSuite{})
112+
113+func (suite *httpErrorSuite) TestNewHTTPErrorParsesAzureError(c *C) {
114+ description := "upload failed"
115+ status := 415
116+ code := "CannotUpload"
117+ message := "Unknown data format"
118+ xml := fmt.Sprintf(`<Error>
119+ <Code>%s</Code>
120+ <Message>%s</Message>
121+ </Error>`,
122+ code, message)
123+
124+ httpErr := newHTTPError(status, []byte(xml), description)
125+
126+ azureErr, ok := httpErr.(*AzureError)
127+ c.Assert(ok, Equals, true)
128+ c.Check(azureErr.StatusCode(), Equals, status)
129+ c.Check(azureErr.Code, Equals, code)
130+ c.Check(azureErr.Message, Equals, message)
131+ c.Check(httpErr, ErrorMatches, ".*"+description+".*")
132+ c.Check(httpErr, ErrorMatches, ".*415: Unsupported Media Type.*")
133+}
134+
135+func (suite *httpErrorSuite) TestNewHTTPErrorResortsToServerError(c *C) {
136+ description := "could not talk to server"
137+ status := 505
138+
139+ httpErr := newHTTPError(status, []byte{}, description)
140+
141+ _, ok := httpErr.(*ServerError)
142+ c.Assert(ok, Equals, true)
143+ c.Check(httpErr.StatusCode(), Equals, status)
144+ c.Check(httpErr, ErrorMatches, ".*505: HTTP Version Not Supported.*")
145+ c.Check(httpErr, ErrorMatches, ".*"+description+".*")
146+}
147+
148+func (suite *httpErrorSuite) TestAzureErrorComposesError(c *C) {
149+ description := "something failed"
150+ status := 410
151+ httpErr := AzureError{
152+ error: errors.New(description),
153+ HTTPStatus: HTTPStatus(status),
154+ Code: "MissingError",
155+ Message: "Your object has disappeared",
156+ }
157+ c.Check(httpErr.Error(), Equals, "something failed: MissingError - Your object has disappeared (http code 410: Gone)")
158+}
159+
160+func (suite *httpErrorSuite) TestServerErrorComposesError(c *C) {
161+ description := "something failed"
162+ status := 501
163+ httpErr := ServerError{
164+ error: errors.New(description),
165+ HTTPStatus: HTTPStatus(status),
166+ }
167+ c.Check(httpErr.Error(), Equals, "something failed (501: Not Implemented)")
168+}
169
170=== modified file 'storage.go'
171--- storage.go 2013-03-27 03:47:21 +0000
172+++ storage.go 2013-03-27 16:36:22 +0000
173@@ -218,40 +218,30 @@
174 // Send a request to the storage service and process the response.
175 // The "res" parameter is typically an XML struct that will deserialize the
176 // raw XML into the struct data. The http Response object is returned.
177-// If the response's HTTP status code is not the same as "expected_status" then
178-// Error will be non-nil and the received status will be set in Error.status.
179-// If any error occurrs and if more specific error data is available from
180-// Azure, it is returned in Error.Code/Error.Message. Error.status will always
181-// contain the HTTP response code if a response is received. If the request
182-// itself failed, Error.Code is the empty string and a textual failure reason
183-// will be in Error.Message.
184+//
185+// If the response's HTTP status code is not the same as "expected_status"
186+// then an HTTPError will be returned as the error. When the returned error
187+// is an HTTPError, the request response is also returned. In other error
188+// cases, the returned response may be the one received from the server or
189+// it may be nil.
190 func (context *StorageContext) send(req *http.Request, res Deserializer, expected_status int) (*http.Response, error) {
191 client := context.getClient()
192 resp, err := client.Do(req)
193 if err != nil {
194- return nil, &Error{
195- status: 0, Code: "", Message: err.Error()}
196+ return nil, err
197 }
198
199- errmsg := &Error{}
200- errmsg.status = resp.StatusCode
201 var data []byte
202
203 if resp.StatusCode != expected_status {
204 if resp.Body != nil {
205 data, err = ioutil.ReadAll(resp.Body)
206 if err != nil {
207- errmsg.Message = fmt.Sprintf(
208- "Failed to read response data: %s", err)
209- return resp, errmsg
210+ return resp, err
211 }
212 }
213- err := errmsg.Deserialize(data)
214- if err != nil {
215- errmsg.Message = fmt.Sprintf(
216- "request failed with status %d", resp.StatusCode)
217- }
218- return resp, errmsg
219+ msg := newHTTPError(resp.StatusCode, data, "Azure request failed")
220+ return resp, msg
221 }
222
223 // If the caller didn't supply an object to deserialize the message into
224@@ -263,14 +253,13 @@
225 // TODO: deserialize response headers into the "res" object.
226 data, err = ioutil.ReadAll(resp.Body)
227 if err != nil {
228- errmsg.Message = fmt.Sprintf(
229- "Failed to read response data: %s", err)
230- return resp, errmsg
231+ msg := fmt.Errorf("failed to read response data: %s", err)
232+ return resp, msg
233 }
234 err = res.Deserialize(data)
235 if err != nil {
236- errmsg.Message = fmt.Sprintf("Failed to deserialize data: %s", err)
237- return resp, errmsg
238+ msg := fmt.Errorf("Failed to deserialize data: %s", err)
239+ return resp, msg
240 }
241
242 return resp, nil
243
244=== modified file 'storage_test.go'
245--- storage_test.go 2013-03-26 06:58:54 +0000
246+++ storage_test.go 2013-03-27 16:36:22 +0000
247@@ -542,7 +542,10 @@
248 context := makeStorageContext()
249 context.client = &http.Client{Transport: transport}
250 err := context.PutBlob("container", "blobname")
251- c.Assert(err.Error(), Equals, "102: Frotzed, failed to put blob")
252+ c.Assert(err, NotNil)
253+ c.Check(err, ErrorMatches, ".*102.*")
254+ c.Check(err, ErrorMatches, ".*Frotzed.*")
255+ c.Check(err, ErrorMatches, ".*failed to put blob.*")
256 }
257
258 type TestPutBlock struct{}
259@@ -599,7 +602,10 @@
260 context.client = &http.Client{Transport: transport}
261 data_reader := bytes.NewReader(MakeRandomByteSlice(10))
262 err := context.PutBlock("container", "blobname", "blockid", data_reader)
263- c.Assert(err.Error(), Equals, "102: Frotzed, failed to put block")
264+ c.Assert(err, NotNil)
265+ c.Check(err, ErrorMatches, ".*102.*")
266+ c.Check(err, ErrorMatches, ".*Frotzed.*")
267+ c.Check(err, ErrorMatches, ".*failed to put block.*")
268 }
269
270 type TestPutBlockList struct{}
271@@ -659,7 +665,10 @@
272 context.client = &http.Client{Transport: transport}
273 blocklist := &BlockList{}
274 err := context.PutBlockList("container", "blobname", blocklist)
275- c.Assert(err.Error(), Equals, "102: Frotzed, failed to put blocklist")
276+ c.Assert(err, NotNil)
277+ c.Check(err, ErrorMatches, ".*102.*")
278+ c.Check(err, ErrorMatches, ".*Frotzed.*")
279+ c.Check(err, ErrorMatches, ".*failed to put blocklist.*")
280 }
281
282 type TestGetBlockList struct{}
283@@ -770,7 +779,10 @@
284 context := makeStorageContext()
285 context.client = &http.Client{Transport: transport}
286 err := context.DeleteBlob("container", "blobname")
287- c.Assert(err.Error(), Equals, "146: Frotzed, failed to delete blob")
288+ c.Assert(err, NotNil)
289+ c.Check(err, ErrorMatches, ".*146.*")
290+ c.Check(err, ErrorMatches, ".*Frotzed.*")
291+ c.Check(err, ErrorMatches, ".*failed to delete blob.*")
292 }
293
294 type TestGetBlob struct{}
295@@ -827,5 +839,7 @@
296 reader, err := context.GetBlob("container", "blobname")
297 c.Check(reader, IsNil)
298 c.Assert(err, NotNil)
299- c.Assert(err.Error(), Equals, "246: Frotzed, failed to get blob")
300+ c.Check(err, ErrorMatches, ".*246.*")
301+ c.Check(err, ErrorMatches, ".*Frotzed.*")
302+ c.Check(err, ErrorMatches, ".*failed to get blob.*")
303 }
304
305=== modified file 'x509session.go'
306--- x509session.go 2013-03-26 06:50:50 +0000
307+++ x509session.go 2013-03-27 16:36:22 +0000
308@@ -4,7 +4,6 @@
309 package gwacl
310
311 import (
312- "fmt"
313 "net/http"
314 )
315
316@@ -31,22 +30,11 @@
317 // fakes.
318 var _X509Dispatcher = performX509CurlRequest
319
320-// ServerError is an error implementation representing an http error response
321-// from the server. In addition to being an "error," it also contains the
322-// http status code.
323-type ServerError struct {
324- error
325- StatusCode int
326-}
327-
328-var _ error = &ServerError{}
329-
330 // getServerError returns a ServerError matching the given server response
331 // status, or nil if the server response indicates success.
332-func (session *x509Session) getServerError(status int, description string) error {
333+func (session *x509Session) getServerError(status int, body []byte, description string) error {
334 if status < http.StatusOK || status >= http.StatusMultipleChoices {
335- failure := fmt.Errorf("%s (http status code %d)", description, status)
336- return &ServerError{error: failure, StatusCode: status}
337+ return newHTTPError(status, body, description)
338 }
339 return nil
340 }
341@@ -62,7 +50,7 @@
342 if err != nil {
343 return nil, err
344 }
345- err = session.getServerError(response.StatusCode, "GET request failed")
346+ err = session.getServerError(response.StatusCode, response.Body, "GET request failed")
347 return response.Body, err
348 }
349
350@@ -77,7 +65,7 @@
351 if err != nil {
352 return nil, err
353 }
354- err = session.getServerError(response.StatusCode, "POST request failed")
355+ err = session.getServerError(response.StatusCode, response.Body, "POST request failed")
356 return response.Body, err
357 }
358
359@@ -89,7 +77,7 @@
360 if err != nil {
361 return err
362 }
363- return session.getServerError(response.StatusCode, "DELETE request failed")
364+ return session.getServerError(response.StatusCode, response.Body, "DELETE request failed")
365 }
366
367 // put performs a PUT request to the Azure management API.
368@@ -100,5 +88,5 @@
369 if err != nil {
370 return err
371 }
372- return session.getServerError(response.StatusCode, "POST request failed")
373+ return session.getServerError(response.StatusCode, response.Body, "POST request failed")
374 }
375
376=== modified file 'x509session_test.go'
377--- x509session_test.go 2013-03-26 10:23:51 +0000
378+++ x509session_test.go 2013-03-27 16:36:22 +0000
379@@ -145,12 +145,12 @@
380 session, err := newX509Session("subscriptionid", "azure.pem")
381 c.Assert(err, IsNil)
382
383- err = session.getServerError(status, msg)
384+ err = session.getServerError(status, []byte{}, msg)
385 c.Assert(err, NotNil)
386
387 c.Check(err, ErrorMatches, ".*"+msg+".*")
388 serverError := err.(*ServerError)
389- c.Check(serverError.StatusCode, Equals, status)
390+ c.Check(serverError.StatusCode(), Equals, status)
391 }
392
393 func (suite *x509SessionSuite) TestGetServerErrorLikes20x(c *C) {
394@@ -162,7 +162,7 @@
395 c.Assert(err, IsNil)
396
397 for _, status := range goodCodes {
398- c.Check(session.getServerError(status, ""), IsNil)
399+ c.Check(session.getServerError(status, []byte{}, ""), IsNil)
400 }
401 }
402
403@@ -180,7 +180,7 @@
404 c.Assert(err, IsNil)
405
406 for _, status := range badCodes {
407- c.Check(session.getServerError(status, ""), NotNil)
408+ c.Check(session.getServerError(status, []byte{}, ""), NotNil)
409 }
410 }
411
412@@ -232,7 +232,7 @@
413 c.Assert(err, NotNil)
414
415 serverError := err.(*ServerError)
416- c.Check(serverError.StatusCode, Equals, fixedResponse.StatusCode)
417+ c.Check(serverError.StatusCode(), Equals, fixedResponse.StatusCode)
418 c.Check(body, DeepEquals, fixedResponse.Body)
419 }
420
421@@ -288,7 +288,7 @@
422 c.Assert(err, NotNil)
423
424 serverError := err.(*ServerError)
425- c.Check(serverError.StatusCode, Equals, fixedResponse.StatusCode)
426+ c.Check(serverError.StatusCode(), Equals, fixedResponse.StatusCode)
427 c.Check(body, DeepEquals, fixedResponse.Body)
428 }
429
430
431=== modified file 'xmlobjects.go'
432--- xmlobjects.go 2013-03-26 01:02:04 +0000
433+++ xmlobjects.go 2013-03-27 16:36:22 +0000
434@@ -29,23 +29,6 @@
435 }
436
437 //
438-// Error messages
439-//
440-type Error struct {
441- status int // HTTP status returned.
442- Code string `xml:"Code"`
443- Message string `xml:"Message"`
444-}
445-
446-func (e *Error) Error() string {
447- return fmt.Sprintf("%d: %s, %s", e.status, e.Code, e.Message)
448-}
449-
450-func (e *Error) Deserialize(data []byte) error {
451- return xml.Unmarshal(data, e)
452-}
453-
454-//
455 // LinuxProvisioningConfiguration bits
456 //
457
458
459=== modified file 'xmlobjects_test.go'
460--- xmlobjects_test.go 2013-03-26 07:00:46 +0000
461+++ xmlobjects_test.go 2013-03-27 16:36:22 +0000
462@@ -19,24 +19,6 @@
463 // Tests for Marshallers
464 //
465
466-func (suite *xmlSuite) TestErrorDeserialize(c *C) {
467- input := `
468- <Error>
469- <Code>InvalidMusicVolume</Code>
470- <Message>Van Halen played too quietly</Message>
471- </Error>`
472-
473- observed := &Error{}
474- err := observed.Deserialize([]byte(input))
475- c.Assert(err, IsNil)
476-
477- expected := &Error{
478- Code: "InvalidMusicVolume",
479- Message: "Van Halen played too quietly",
480- }
481- c.Check(expected, DeepEquals, observed)
482-}
483-
484 func (suite *xmlSuite) TestLinuxProvisioningConfiguration(c *C) {
485 config := makeLinuxProvisioningConfiguration()
486

Subscribers

People subscribed via source and target branches