Merge lp:~wallyworld/goose/control-test-doubles into lp:~gophers/goose/trunk

Proposed by Ian Booth
Status: Merged
Merged at revision: 54
Proposed branch: lp:~wallyworld/goose/control-test-doubles
Merge into: lp:~gophers/goose/trunk
Diff against target: 434 lines (+230/-26)
8 files modified
client/local_test.go (+2/-2)
nova/local_test.go (+2/-2)
swift/local_test.go (+3/-3)
testservices/novaservice/service.go (+4/-0)
testservices/openstackservice/openstack.go (+11/-11)
testservices/service.go (+71/-0)
testservices/service_test.go (+105/-0)
tools/secgroup-delete-all/main_test.go (+32/-8)
To merge this branch: bzr merge lp:~wallyworld/goose/control-test-doubles
Reviewer Review Type Date Requested Status
John A Meinel (community) Approve
Review via email: mp+144838@code.launchpad.net

Description of the change

Error testing infrastructure for service doubles

This branch provides infrastructure to allow tests to specify that the service
doubles should return an error at various points.
The basic premise is that the test provides a hook which is run at the specfied point in time eg when a specific function runs.
The hook is passed the service double and any function arguments. It can inspect the state of the service and the function args and
can decide to return an error if it wants to.

The diff conatins a practical example of this being used - 3 security groups are
created and an error is raised when the second one is deleted.

This work doesn't have to be used to raise an error - the hook can also be used to modify the sate of the service double and simply exit without error.

https://codereview.appspot.com/7204055/

To post a comment you must log in.
Revision history for this message
Ian Booth (wallyworld) wrote :

Reviewers: mp+144838_code.launchpad.net,

Message:
Please take a look.

Description:
Error testing infrastructure for service doubles

This branch provides infrastructure to allow tests to specify that the
service
doubles should return an error at various points.
The basic premise is that the test provides a hook which is run at the
specfied point in time eg when a specific function runs.
The hook is passed the service double and any function arguments. It can
inspect the state of the service and the function args and
can decide to return an error if it wants to.

The diff conatins a practical example of this being used - 3 security
groups are
created and an error is raised when the second one is deleted.

This work doesn't have to be used to raise an error - the hook can also
be used to modify the sate of the service double and simply exit without
error.

https://code.launchpad.net/~wallyworld/goose/control-test-doubles/+merge/144838

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/7204055/

Affected files:
   A [revision details]
   M testservices/novaservice/service.go
   M testservices/openstack/openstack.go
   M testservices/service.go
   A testservices/service_test.go
   M tools/secgroup-delete-all/main_test.go

Revision history for this message
John A Meinel (jameinel) wrote :
Download full text (3.4 KiB)

overall LGTM.

A couple small things that we can tweak, but I like the overall idea of
a hook point that you can use to trace what is going on, and to inject
the logic you want.

https://codereview.appspot.com/7204055/diff/1/testservices/service.go
File testservices/service.go (right):

https://codereview.appspot.com/7204055/diff/1/testservices/service.go#newcode65
testservices/service.go:65: // }
I can't help but feel that something like "nil" would be clearer than
the empty string. But I think we're stuck with what we have.

Alternatively, if most of the time we're just going to be grabbing the
function name, maybe we want to make that the obvious one, and add a
separate function when we have to?

https://codereview.appspot.com/7204055/diff/1/testservices/service_test.go
File testservices/service_test.go (right):

https://codereview.appspot.com/7204055/diff/1/testservices/service_test.go#newcode50
testservices/service_test.go:50: func nonFunctionControlHook(s
ServiceControl, args ...interface{}) error {
I don't quite understand why this is "nonFunctionControlHook", is it
meant to be "named" hook, or something like that?

https://codereview.appspot.com/7204055/diff/1/tools/secgroup-delete-all/main_test.go
File tools/secgroup-delete-all/main_test.go (left):

https://codereview.appspot.com/7204055/diff/1/tools/secgroup-delete-all/main_test.go#oldcode47
tools/secgroup-delete-all/main_test.go:47: openstack :=
openstack.New(creds)
I'd like us to generally avoid using variable names that match module
names. It gets confusing to figure out which "openstack" thing is being
operated on.

Not that you added this, but I've seen it in a couple places, and was a
bit confused by it.

https://codereview.appspot.com/7204055/diff/1/tools/secgroup-delete-all/main_test.go
File tools/secgroup-delete-all/main_test.go (right):

https://codereview.appspot.com/7204055/diff/1/tools/secgroup-delete-all/main_test.go#newcode75
tools/secgroup-delete-all/main_test.go:75: if groupId == 2 {
This seems a bit risky to have it always work (comparing an interface{}
to an integer).
Also, it would be nicer if we at least compared to the group name,
rather than to an integer id. Is it possible to get more context for the
hook? I realize this is what we have today, but it seems like it would
be really easy to accidentally break (changing the numbering of the
groups, adding a default group, etc.)

Maybe if we did it using a local function, so we can use the result from
nova.CreateSecurityGroup rather than just '2'.

https://codereview.appspot.com/7204055/diff/1/tools/secgroup-delete-all/main_test.go#newcode84
tools/secgroup-delete-all/main_test.go:84:
nova.CreateSecurityGroup("group-b", "Another group")
I think this would end up looking like:

doNotDelete = nova.CreateSecurityGroup("group-b", "Another group")

deleteError = func(s testServices.ServiceControl, args ...interface{})
error {
   groupId := args[0]
   if groupId == doNotDelete.Id {
     return fmt.Errorf("cannot delete group %d", groupId)
   }
   return nil
}
os.Nova.RegisterControlPoint("removeSecurityGroup", deleteError)
defer os.Nova.RegisterControlPoint("removeSecurityGroup", nil)

I also wonder if it would be nice...

Read more...

54. By Ian Booth

Code review fixes

Revision history for this message
Ian Booth (wallyworld) wrote :
Download full text (4.1 KiB)

Please take a look.

https://codereview.appspot.com/7204055/diff/1/testservices/service.go
File testservices/service.go (right):

https://codereview.appspot.com/7204055/diff/1/testservices/service.go#newcode65
testservices/service.go:65: // }
On 2013/01/28 06:13:58, jameinel wrote:
> I can't help but feel that something like "nil" would be clearer than
the empty
> string. But I think we're stuck with what we have.

Welcome to Go :-(

> Alternatively, if most of the time we're just going to be grabbing the
function
> name, maybe we want to make that the obvious one, and add a separate
function
> when we have to?

I thought about it (and cursed and lamented the lack of default
parameters in Go). The Go Way seems to be though to stick to a single
method and pass in default parameter values all the time, from what I
can tell.

https://codereview.appspot.com/7204055/diff/1/testservices/service_test.go
File testservices/service_test.go (right):

https://codereview.appspot.com/7204055/diff/1/testservices/service_test.go#newcode50
testservices/service_test.go:50: func nonFunctionControlHook(s
ServiceControl, args ...interface{}) error {
On 2013/01/28 06:13:58, jameinel wrote:
> I don't quite understand why this is "nonFunctionControlHook", is it
meant to be
> "named" hook, or something like that?

"namedControlHook" is much nicer, will use that

https://codereview.appspot.com/7204055/diff/1/tools/secgroup-delete-all/main_test.go
File tools/secgroup-delete-all/main_test.go (left):

https://codereview.appspot.com/7204055/diff/1/tools/secgroup-delete-all/main_test.go#oldcode47
tools/secgroup-delete-all/main_test.go:47: openstack :=
openstack.New(creds)
On 2013/01/28 06:13:58, jameinel wrote:
> I'd like us to generally avoid using variable names that match module
names. It
> gets confusing to figure out which "openstack" thing is being operated
on.

> Not that you added this, but I've seen it in a couple places, and was
a bit
> confused by it.

No, it was me who added it in a previous branch from memory.
I'll change the variable name. Perhaps I should have called the package
"testopenstack" but then there's already a "testservices" in the import
path and it seems redundant. We call the nova double "novaservice" but
"openstackservice" seems too long. But in the juju-core code, the
production openstack provider package is just called "openstack" and so
disambiguation is needed there. So just "openstack" in this case does
seem like it is not the best choice in hindsight.

I'll change it to "openstackservice" and see if I get agreement on that.

https://codereview.appspot.com/7204055/diff/1/tools/secgroup-delete-all/main_test.go
File tools/secgroup-delete-all/main_test.go (right):

https://codereview.appspot.com/7204055/diff/1/tools/secgroup-delete-all/main_test.go#newcode75
tools/secgroup-delete-all/main_test.go:75: if groupId == 2 {
On 2013/01/28 06:13:58, jameinel wrote:
> This seems a bit risky to have it always work (comparing an
interface{} to an
> integer).

I've added a cast. Note though that my expectation here is that the
author of the hook is expected to know the signature of the function
being processed and hence what the arguments will be.

> ...

Read more...

Revision history for this message
John A Meinel (jameinel) wrote :

> I'll change it to "openstackservice" and see if I get agreement on that.

I'm not terribly concerned about the name of the module. "openstackservice" is fine with me.

What I'm more concerned with is that we don't name a variable that matches the name of an imported module.

The line in question was:

openstack = openstack.New(creds)

Note that this isn't "s.openstack = " or anything like that. Just doing "os = openstack.New()" would be enough for me.

+var doNotDelete *nova.SecurityGroup

I generally prefer to use a closure (eg func() ...) rather than global state like this.

Otherwise LGTM. Just a note that you need to use 'lbox propose' again to update the reitveld diff. It will also push to launchpad to update the MP.

review: Approve
Revision history for this message
John A Meinel (jameinel) wrote :

I must have clicked on the wrong link, it looks like the reitveld diff is updated.

Revision history for this message
John A Meinel (jameinel) wrote :
Revision history for this message
Ian Booth (wallyworld) wrote :

*** Submitted:

Error testing infrastructure for service doubles

This branch provides infrastructure to allow tests to specify that the
service
doubles should return an error at various points.
The basic premise is that the test provides a hook which is run at the
specfied point in time eg when a specific function runs.
The hook is passed the service double and any function arguments. It can
inspect the state of the service and the function args and
can decide to return an error if it wants to.

The diff conatins a practical example of this being used - 3 security
groups are
created and an error is raised when the second one is deleted.

This work doesn't have to be used to raise an error - the hook can also
be used to modify the sate of the service double and simply exit without
error.

R=jameinel
CC=
https://codereview.appspot.com/7204055

https://codereview.appspot.com/7204055/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'client/local_test.go'
2--- client/local_test.go 2013-01-24 03:15:19 +0000
3+++ client/local_test.go 2013-01-29 00:47:20 +0000
4@@ -6,7 +6,7 @@
5 "launchpad.net/goose/testing/httpsuite"
6 "launchpad.net/goose/testservices"
7 "launchpad.net/goose/testservices/identityservice"
8- "launchpad.net/goose/testservices/openstack"
9+ "launchpad.net/goose/testservices/openstackservice"
10 )
11
12 func registerLocalTests(authMethods []identity.AuthMethod) {
13@@ -43,7 +43,7 @@
14 panic("Invalid authentication method")
15 case identity.AuthUserPass:
16 // The openstack test service sets up userpass authentication.
17- s.service = openstack.New(s.cred)
18+ s.service = openstackservice.New(s.cred)
19 case identity.AuthLegacy:
20 legacy := identityservice.NewLegacy()
21 legacy.AddUser(s.cred.User, s.cred.Secrets, s.cred.TenantName)
22
23=== modified file 'nova/local_test.go'
24--- nova/local_test.go 2013-01-24 01:01:35 +0000
25+++ nova/local_test.go 2013-01-29 00:47:20 +0000
26@@ -7,7 +7,7 @@
27 "launchpad.net/goose/errors"
28 "launchpad.net/goose/identity"
29 "launchpad.net/goose/nova"
30- "launchpad.net/goose/testservices/openstack"
31+ "launchpad.net/goose/testservices/openstackservice"
32 "log"
33 "net/http"
34 "net/http/httptest"
35@@ -45,7 +45,7 @@
36 Region: "some region",
37 TenantName: "tenant",
38 }
39- openstack := openstack.New(s.cred)
40+ openstack := openstackservice.New(s.cred)
41 openstack.SetupHTTP(s.Mux)
42
43 s.LiveTests.SetUpSuite(c)
44
45=== modified file 'swift/local_test.go'
46--- swift/local_test.go 2013-01-24 01:01:35 +0000
47+++ swift/local_test.go 2013-01-29 00:47:20 +0000
48@@ -4,7 +4,7 @@
49 . "launchpad.net/gocheck"
50 "launchpad.net/goose/identity"
51 "launchpad.net/goose/testing/httpsuite"
52- "launchpad.net/goose/testservices/openstack"
53+ "launchpad.net/goose/testservices/openstackservice"
54 )
55
56 func registerLocalTests() {
57@@ -18,7 +18,7 @@
58 LiveTestsPublicContainer
59 // The following attributes are for using testing doubles.
60 httpsuite.HTTPSuite
61- openstack *openstack.Openstack
62+ openstack *openstackservice.Openstack
63 }
64
65 func (s *localLiveSuite) SetUpSuite(c *C) {
66@@ -33,7 +33,7 @@
67 TenantName: "tenant",
68 }
69 s.LiveTestsPublicContainer.cred = s.LiveTests.cred
70- s.openstack = openstack.New(s.LiveTests.cred)
71+ s.openstack = openstackservice.New(s.LiveTests.cred)
72
73 s.LiveTests.SetUpSuite(c)
74 s.LiveTestsPublicContainer.SetUpSuite(c)
75
76=== modified file 'testservices/novaservice/service.go'
77--- testservices/novaservice/service.go 2013-01-24 03:15:19 +0000
78+++ testservices/novaservice/service.go 2013-01-29 00:47:20 +0000
79@@ -91,6 +91,7 @@
80 VersionPath: versionPath,
81 TenantId: tenantId,
82 Region: region,
83+ ControlHooks: make(map[string]testservices.ControlProcessor),
84 },
85 }
86 if identityService != nil {
87@@ -340,6 +341,9 @@
88
89 // removeSecurityGroup deletes an existing group.
90 func (n *Nova) removeSecurityGroup(groupId int) error {
91+ if err := n.ProcessControlHook("", n, groupId); err != nil {
92+ return err
93+ }
94 if _, err := n.securityGroup(groupId); err != nil {
95 return err
96 }
97
98=== renamed directory 'testservices/openstack' => 'testservices/openstackservice'
99=== modified file 'testservices/openstackservice/openstack.go'
100--- testservices/openstack/openstack.go 2013-01-24 01:01:35 +0000
101+++ testservices/openstackservice/openstack.go 2013-01-29 00:47:20 +0000
102@@ -1,4 +1,4 @@
103-package openstack
104+package openstackservice
105
106 import (
107 "launchpad.net/goose/identity"
108@@ -10,29 +10,29 @@
109
110 // Openstack provides an Openstack service double implementation.
111 type Openstack struct {
112- identity identityservice.IdentityService
113- nova *novaservice.Nova
114- swift *swiftservice.Swift
115+ Identity identityservice.IdentityService
116+ Nova *novaservice.Nova
117+ Swift *swiftservice.Swift
118 }
119
120 // New creates an instance of a full Openstack service double.
121 // An initial user with the specified credentials is registered with the identity service.
122 func New(cred *identity.Credentials) *Openstack {
123 openstack := Openstack{
124- identity: identityservice.NewUserPass(),
125+ Identity: identityservice.NewUserPass(),
126 }
127- userInfo := openstack.identity.AddUser(cred.User, cred.Secrets, cred.TenantName)
128+ userInfo := openstack.Identity.AddUser(cred.User, cred.Secrets, cred.TenantName)
129 if cred.TenantName == "" {
130 panic("Openstack service double requires a tenant to be specified.")
131 }
132- openstack.nova = novaservice.New(cred.URL, "v2", userInfo.TenantId, cred.Region, openstack.identity)
133- openstack.swift = swiftservice.New(cred.URL, "v1", userInfo.TenantId, cred.Region, openstack.identity)
134+ openstack.Nova = novaservice.New(cred.URL, "v2", userInfo.TenantId, cred.Region, openstack.Identity)
135+ openstack.Swift = swiftservice.New(cred.URL, "v1", userInfo.TenantId, cred.Region, openstack.Identity)
136 return &openstack
137 }
138
139 // setupHTTP attaches all the needed handlers to provide the HTTP API for the Openstack service..
140 func (openstack *Openstack) SetupHTTP(mux *http.ServeMux) {
141- openstack.identity.SetupHTTP(mux)
142- openstack.nova.SetupHTTP(mux)
143- openstack.swift.SetupHTTP(mux)
144+ openstack.Identity.SetupHTTP(mux)
145+ openstack.Nova.SetupHTTP(mux)
146+ openstack.Swift.SetupHTTP(mux)
147 }
148
149=== modified file 'testservices/service.go'
150--- testservices/service.go 2013-01-24 03:15:19 +0000
151+++ testservices/service.go 2013-01-29 00:47:20 +0000
152@@ -3,6 +3,8 @@
153 import (
154 "launchpad.net/goose/testservices/identityservice"
155 "net/http"
156+ "runtime"
157+ "strings"
158 )
159
160 // An HttpService provides the HTTP API for a service double.
161@@ -13,9 +15,78 @@
162 // A ServiceInstance is an Openstack module, one of nova, swift, glance.
163 type ServiceInstance struct {
164 identityservice.ServiceProvider
165+ ServiceControl
166 IdentityService identityservice.IdentityService
167 Hostname string
168 VersionPath string
169 TenantId string
170 Region string
171+ // Hooks to run when specified control points are reached in the service business logic.
172+ ControlHooks map[string]ControlProcessor
173+}
174+
175+// ControlProcessor defines a function that is run when a specified control point is reached in the service
176+// business logic. The function receives the service instance so internal state can be inspected, plus for any
177+// arguments passed to the currently executing service function.
178+type ControlProcessor func(sc ServiceControl, args ...interface{}) error
179+
180+// ControlHookCleanup defines a function used to remove a control hook.
181+type ControlHookCleanup func()
182+
183+// ServiceControl instances allow hooks to be registered for execution at the specified point of execution.
184+// The control point name can be a function name or a logical execution point meaningful to the service.
185+// If name is "", the hook for the currently executing function is executed.
186+// Returns a function which can be used to remove the hook.
187+type ServiceControl interface {
188+ RegisterControlPoint(name string, controller ControlProcessor) ControlHookCleanup
189+}
190+
191+// currentServiceMethodName returns the method executing on the service when ProcessControlHook was invoked.
192+func (s *ServiceInstance) currentServiceMethodName() string {
193+ pc, _, _, ok := runtime.Caller(2)
194+ if !ok {
195+ panic("current method name cannot be found")
196+ }
197+ return unqualifiedMethodName(pc)
198+}
199+
200+func unqualifiedMethodName(pc uintptr) string {
201+ f := runtime.FuncForPC(pc)
202+ fullName := f.Name()
203+ nameParts := strings.Split(fullName, ".")
204+ return nameParts[len(nameParts)-1]
205+}
206+
207+// ProcessControlHook retrieves the ControlProcessor for the specified hook name and runs it, returning any error.
208+// Use it like this with a "" hookName to invoke a hook registered for the current function:
209+// if err := n.ProcessControlHook("", <serviceinstance>, <somearg1>, <somearg2>); err != nil {
210+// return err
211+// }
212+//
213+// Use it like this to invoke a hook registered for some arbitrary control point:
214+// if err := n.ProcessControlHook("foobar", <serviceinstance>, <somearg1>, <somearg2>); err != nil {
215+// return err
216+// }
217+func (s *ServiceInstance) ProcessControlHook(hookName string, sc ServiceControl, args ...interface{}) error {
218+ if hookName == "" {
219+ hookName = s.currentServiceMethodName()
220+ }
221+ if hook, ok := s.ControlHooks[hookName]; ok {
222+ return hook(sc, args...)
223+ }
224+ return nil
225+}
226+
227+// RegisterControlPoint assigns the specified controller to the named hook. If nil, any existing controller for the
228+// hook is removed.
229+// hookName is the name of a function on the service or some arbitrarily named control point.
230+func (s *ServiceInstance) RegisterControlPoint(hookName string, controller ControlProcessor) ControlHookCleanup {
231+ if controller == nil {
232+ delete(s.ControlHooks, hookName)
233+ } else {
234+ s.ControlHooks[hookName] = controller
235+ }
236+ return func() {
237+ s.RegisterControlPoint(hookName, nil)
238+ }
239 }
240
241=== added file 'testservices/service_test.go'
242--- testservices/service_test.go 1970-01-01 00:00:00 +0000
243+++ testservices/service_test.go 2013-01-29 00:47:20 +0000
244@@ -0,0 +1,105 @@
245+package testservices
246+
247+import (
248+ "fmt"
249+ . "launchpad.net/gocheck"
250+ "testing"
251+)
252+
253+func Test(t *testing.T) {
254+ TestingT(t)
255+}
256+
257+var _ = Suite(&ServiceSuite{})
258+
259+type ServiceSuite struct {
260+ ts *testService
261+}
262+
263+func (s *ServiceSuite) SetUpTest(c *C) {
264+ s.ts = newTestService()
265+ // This hook is called based on the function name.
266+ s.ts.RegisterControlPoint("foo", functionControlHook)
267+ // This hook is called based on a user specified hook name.
268+ s.ts.RegisterControlPoint("foobar", namedControlHook)
269+}
270+
271+type testService struct {
272+ ServiceInstance
273+ label string
274+}
275+
276+func newTestService() *testService {
277+ return &testService{
278+ ServiceInstance: ServiceInstance{
279+ ControlHooks: make(map[string]ControlProcessor),
280+ },
281+ }
282+}
283+
284+func functionControlHook(s ServiceControl, args ...interface{}) error {
285+ label := args[0].(string)
286+ returnError := args[1].(bool)
287+ if returnError {
288+ return fmt.Errorf("An error occurred")
289+ }
290+ s.(*testService).label = label
291+ return nil
292+}
293+
294+func namedControlHook(s ServiceControl, args ...interface{}) error {
295+ s.(*testService).label = "foobar"
296+ return nil
297+}
298+
299+func (s *testService) foo(label string, returnError bool) error {
300+ if err := s.ProcessControlHook("", s, label, returnError); err != nil {
301+ return err
302+ }
303+ return nil
304+}
305+
306+func (s *testService) bar() error {
307+ if err := s.ProcessControlHook("foobar", s); err != nil {
308+ return err
309+ }
310+ return nil
311+}
312+
313+func (s *ServiceSuite) TestFunctionHookNoError(c *C) {
314+ err := s.ts.foo("success", false)
315+ c.Assert(err, IsNil)
316+ c.Assert(s.ts.label, Equals, "success")
317+}
318+
319+func (s *ServiceSuite) TestHookWithError(c *C) {
320+ err := s.ts.foo("success", true)
321+ c.Assert(err, Not(IsNil))
322+ c.Assert(s.ts.label, Equals, "")
323+}
324+
325+func (s *ServiceSuite) TestNamedHook(c *C) {
326+ err := s.ts.bar()
327+ c.Assert(err, IsNil)
328+ c.Assert(s.ts.label, Equals, "foobar")
329+}
330+
331+func (s *ServiceSuite) TestHookCleanup(c *C) {
332+ // Manually delete the existing control point.
333+ s.ts.RegisterControlPoint("foo", nil)
334+ // Register a new hook and ensure it works.
335+ cleanup := s.ts.RegisterControlPoint("foo", functionControlHook)
336+ err := s.ts.foo("cleanuptest", false)
337+ c.Assert(err, IsNil)
338+ c.Assert(s.ts.label, Equals, "cleanuptest")
339+ // Use the cleanup func to remove the hook and check the result.
340+ cleanup()
341+ err = s.ts.foo("again", false)
342+ c.Assert(err, IsNil)
343+ c.Assert(s.ts.label, Equals, "cleanuptest")
344+ // Ensure that only the specified hook was removed and the other remaining one still works.
345+ err = s.ts.bar()
346+ c.Assert(err, IsNil)
347+ c.Assert(s.ts.label, Equals, "foobar")
348+
349+}
350
351=== modified file 'tools/secgroup-delete-all/main_test.go'
352--- tools/secgroup-delete-all/main_test.go 2013-01-24 01:01:35 +0000
353+++ tools/secgroup-delete-all/main_test.go 2013-01-29 00:47:20 +0000
354@@ -2,12 +2,14 @@
355
356 import (
357 "bytes"
358+ "fmt"
359 . "launchpad.net/gocheck"
360 "launchpad.net/goose/client"
361 "launchpad.net/goose/identity"
362 "launchpad.net/goose/nova"
363 "launchpad.net/goose/testing/httpsuite"
364- "launchpad.net/goose/testservices/openstack"
365+ "launchpad.net/goose/testservices"
366+ "launchpad.net/goose/testservices/openstackservice"
367 tool "launchpad.net/goose/tools/secgroup-delete-all"
368 "testing"
369 )
370@@ -36,7 +38,7 @@
371 return nova.New(osc)
372 }
373
374-func (s *ToolSuite) makeServices(c *C) *nova.Client {
375+func (s *ToolSuite) makeServices(c *C) (*openstackservice.Openstack, *nova.Client) {
376 creds := &identity.Credentials{
377 URL: s.Server.URL,
378 User: username,
379@@ -44,13 +46,13 @@
380 Region: region,
381 TenantName: tenant,
382 }
383- openstack := openstack.New(creds)
384+ openstack := openstackservice.New(creds)
385 openstack.SetupHTTP(s.Mux)
386- return createNovaClient(creds)
387+ return openstack, createNovaClient(creds)
388 }
389
390 func (s *ToolSuite) TestNoGroups(c *C) {
391- nova := s.makeServices(c)
392+ _, nova := s.makeServices(c)
393 var buf bytes.Buffer
394 err := tool.DeleteAll(&buf, nova)
395 c.Assert(err, IsNil)
396@@ -58,7 +60,7 @@
397 }
398
399 func (s *ToolSuite) TestTwoGroups(c *C) {
400- nova := s.makeServices(c)
401+ _, nova := s.makeServices(c)
402 nova.CreateSecurityGroup("group-a", "A group")
403 nova.CreateSecurityGroup("group-b", "Another group")
404 var buf bytes.Buffer
405@@ -67,5 +69,27 @@
406 c.Assert(string(buf.Bytes()), Equals, "2 security groups deleted.\n")
407 }
408
409-// GZ 2013-01-21: Should also test undeleteable groups, but can't induce
410-// novaservice errors currently.
411+// This group is one for which we will simulate a deletion error in the following test.
412+var doNotDelete *nova.SecurityGroup
413+
414+// deleteGroupError hook raises an error if a group with id 2 is deleted.
415+func deleteGroupError(s testservices.ServiceControl, args ...interface{}) error {
416+ groupId := args[0].(int)
417+ if groupId == doNotDelete.Id {
418+ return fmt.Errorf("cannot delete group %d", groupId)
419+ }
420+ return nil
421+}
422+
423+func (s *ToolSuite) TestUndeleteableGroup(c *C) {
424+ os, nova := s.makeServices(c)
425+ nova.CreateSecurityGroup("group-a", "A group")
426+ doNotDelete, _ = nova.CreateSecurityGroup("group-b", "Another group")
427+ nova.CreateSecurityGroup("group-c", "Yet another group")
428+ cleanup := os.Nova.RegisterControlPoint("removeSecurityGroup", deleteGroupError)
429+ defer cleanup()
430+ var buf bytes.Buffer
431+ err := tool.DeleteAll(&buf, nova)
432+ c.Assert(err, IsNil)
433+ c.Assert(string(buf.Bytes()), Equals, "2 security groups deleted.\n1 security groups could not be deleted.\n")
434+}

Subscribers

People subscribed via source and target branches