Merge lp:~thumper/juju-core/refactor-uniter-tests into lp:~go-bot/juju-core/trunk
- refactor-uniter-tests
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Tim Penhey |
Approved revision: | no longer in the source branch. |
Merged at revision: | 2141 |
Proposed branch: | lp:~thumper/juju-core/refactor-uniter-tests |
Merge into: | lp:~go-bot/juju-core/trunk |
Prerequisite: | lp:~thumper/juju-core/juju-run-listener |
Diff against target: |
510 lines (+185/-112) 5 files modified
worker/uniter/context.go (+33/-16) worker/uniter/context_test.go (+7/-2) worker/uniter/export_test.go (+8/-0) worker/uniter/uniter.go (+112/-48) worker/uniter/uniter_test.go (+25/-46) |
To merge this branch: | bzr merge lp:~thumper/juju-core/refactor-uniter-tests |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+198668@code.launchpad.net |
Commit message
Refactor uniter tests
We needed to have a better way to match the hooks that had been executed by
the uniter in the tests. Instead of relying on log messages that the hooks
wrote out, instead we have the uniter optionally call out to an observer.
Part of this work also involved breaking up some of the larger methods in the
Context and the Uniter methods. These broken up bits are used by the code
that runs generic commands coming shortly.
A fundamental change is that now if a hook is missing, instead of silently
passing, the context now returns a missingHookError.
Description of the change
Refactor uniter tests
We needed to have a better way to match the hooks that had been executed by
the uniter in the tests. Instead of relying on log messages that the hooks
wrote out, instead we have the uniter optionally call out to an observer.
Part of this work also involved breaking up some of the larger methods in the
Context and the Uniter methods. These broken up bits are used by the code
that runs generic commands coming shortly.
A fundamental change is that now if a hook is missing, instead of silently
passing, the context now returns a missingHookError.
Tim Penhey (thumper) wrote : | # |
Ian Booth (wallyworld) wrote : | # |
LGTM with some tweaks
https:/
File worker/
https:/
worker/
Why not just make writeChanges a method param?
And set it accordingly in the caller if the err the caller has is nil?
https:/
File worker/
https:/
worker/
Doc string please
https:/
File worker/
https:/
worker/
*gc.C) (match bool, overshoot bool) {
Is this still the best method name?
Tim Penhey (thumper) wrote : | # |
Please take a look.
https:/
File worker/
https:/
worker/
On 2013/12/13 00:23:11, wallyworld wrote:
> Why not just make writeChanges a method param?
> And set it accordingly in the caller if the err the caller has is nil?
Because doing it this way makes the caller much cleaner, and we need the
error anyway as we return that if it is not-nil.
https:/
File worker/
https:/
worker/
On 2013/12/13 00:23:11, wallyworld wrote:
> Doc string please
Done.
https:/
File worker/
https:/
worker/
*gc.C) (match bool, overshoot bool) {
On 2013/12/13 00:23:11, wallyworld wrote:
> Is this still the best method name?
Haha, ah, no. I'll change to something more meaningful.
Preview Diff
1 | === modified file 'worker/uniter/context.go' | |||
2 | --- worker/uniter/context.go 2013-10-04 17:38:30 +0000 | |||
3 | +++ worker/uniter/context.go 2013-12-13 02:48:10 +0000 | |||
4 | @@ -22,6 +22,19 @@ | |||
5 | 22 | "launchpad.net/juju-core/worker/uniter/jujuc" | 22 | "launchpad.net/juju-core/worker/uniter/jujuc" |
6 | 23 | ) | 23 | ) |
7 | 24 | 24 | ||
8 | 25 | type missingHookError struct { | ||
9 | 26 | hookName string | ||
10 | 27 | } | ||
11 | 28 | |||
12 | 29 | func (e *missingHookError) Error() string { | ||
13 | 30 | return e.hookName + " does not exist" | ||
14 | 31 | } | ||
15 | 32 | |||
16 | 33 | func IsMissingHookError(err error) bool { | ||
17 | 34 | _, ok := err.(*missingHookError) | ||
18 | 35 | return ok | ||
19 | 36 | } | ||
20 | 37 | |||
21 | 25 | // HookContext is the implementation of jujuc.Context. | 38 | // HookContext is the implementation of jujuc.Context. |
22 | 26 | type HookContext struct { | 39 | type HookContext struct { |
23 | 27 | unit *uniter.Unit | 40 | unit *uniter.Unit |
24 | @@ -174,25 +187,14 @@ | |||
25 | 174 | return vars | 187 | return vars |
26 | 175 | } | 188 | } |
27 | 176 | 189 | ||
41 | 177 | // RunHook executes a hook in an environment which allows it to to call back | 190 | func (ctx *HookContext) finalizeContext(process string, err error) error { |
42 | 178 | // into ctx to execute jujuc tools. | 191 | writeChanges := err == nil |
30 | 179 | func (ctx *HookContext) RunHook(hookName, charmDir, toolsDir, socketPath string) error { | ||
31 | 180 | var err error | ||
32 | 181 | env := ctx.hookVars(charmDir, toolsDir, socketPath) | ||
33 | 182 | debugctx := unitdebug.NewHooksContext(ctx.unit.Name()) | ||
34 | 183 | if session, _ := debugctx.FindSession(); session != nil && session.MatchHook(hookName) { | ||
35 | 184 | logger.Infof("executing %s via debug-hooks", hookName) | ||
36 | 185 | err = session.RunHook(hookName, charmDir, env) | ||
37 | 186 | } else { | ||
38 | 187 | err = runCharmHook(hookName, charmDir, env) | ||
39 | 188 | } | ||
40 | 189 | write := err == nil | ||
43 | 190 | for id, rctx := range ctx.relations { | 192 | for id, rctx := range ctx.relations { |
45 | 191 | if write { | 193 | if writeChanges { |
46 | 192 | if e := rctx.WriteSettings(); e != nil { | 194 | if e := rctx.WriteSettings(); e != nil { |
47 | 193 | e = fmt.Errorf( | 195 | e = fmt.Errorf( |
48 | 194 | "could not write settings from %q to relation %d: %v", | 196 | "could not write settings from %q to relation %d: %v", |
50 | 195 | hookName, id, e, | 197 | process, id, e, |
51 | 196 | ) | 198 | ) |
52 | 197 | logger.Errorf("%v", e) | 199 | logger.Errorf("%v", e) |
53 | 198 | if err == nil { | 200 | if err == nil { |
54 | @@ -205,6 +207,21 @@ | |||
55 | 205 | return err | 207 | return err |
56 | 206 | } | 208 | } |
57 | 207 | 209 | ||
58 | 210 | // RunHook executes a hook in an environment which allows it to to call back | ||
59 | 211 | // into ctx to execute jujuc tools. | ||
60 | 212 | func (ctx *HookContext) RunHook(hookName, charmDir, toolsDir, socketPath string) error { | ||
61 | 213 | var err error | ||
62 | 214 | env := ctx.hookVars(charmDir, toolsDir, socketPath) | ||
63 | 215 | debugctx := unitdebug.NewHooksContext(ctx.unit.Name()) | ||
64 | 216 | if session, _ := debugctx.FindSession(); session != nil && session.MatchHook(hookName) { | ||
65 | 217 | logger.Infof("executing %s via debug-hooks", hookName) | ||
66 | 218 | err = session.RunHook(hookName, charmDir, env) | ||
67 | 219 | } else { | ||
68 | 220 | err = runCharmHook(hookName, charmDir, env) | ||
69 | 221 | } | ||
70 | 222 | return ctx.finalizeContext(hookName, err) | ||
71 | 223 | } | ||
72 | 224 | |||
73 | 208 | func runCharmHook(hookName, charmDir string, env []string) error { | 225 | func runCharmHook(hookName, charmDir string, env []string) error { |
74 | 209 | ps := exec.Command(filepath.Join(charmDir, "hooks", hookName)) | 226 | ps := exec.Command(filepath.Join(charmDir, "hooks", hookName)) |
75 | 210 | ps.Env = env | 227 | ps.Env = env |
76 | @@ -230,7 +247,7 @@ | |||
77 | 230 | if os.IsNotExist(ee.Err) { | 247 | if os.IsNotExist(ee.Err) { |
78 | 231 | // Missing hook is perfectly valid, but worth mentioning. | 248 | // Missing hook is perfectly valid, but worth mentioning. |
79 | 232 | logger.Infof("skipped %q hook (not implemented)", hookName) | 249 | logger.Infof("skipped %q hook (not implemented)", hookName) |
81 | 233 | return nil | 250 | return &missingHookError{hookName} |
82 | 234 | } | 251 | } |
83 | 235 | } | 252 | } |
84 | 236 | return err | 253 | return err |
85 | 237 | 254 | ||
86 | === modified file 'worker/uniter/context_test.go' | |||
87 | --- worker/uniter/context_test.go 2013-11-14 12:32:14 +0000 | |||
88 | +++ worker/uniter/context_test.go 2013-12-13 02:48:10 +0000 | |||
89 | @@ -19,6 +19,7 @@ | |||
90 | 19 | "launchpad.net/juju-core/state/api" | 19 | "launchpad.net/juju-core/state/api" |
91 | 20 | "launchpad.net/juju-core/state/api/params" | 20 | "launchpad.net/juju-core/state/api/params" |
92 | 21 | apiuniter "launchpad.net/juju-core/state/api/uniter" | 21 | apiuniter "launchpad.net/juju-core/state/api/uniter" |
93 | 22 | jc "launchpad.net/juju-core/testing/checkers" | ||
94 | 22 | "launchpad.net/juju-core/utils" | 23 | "launchpad.net/juju-core/utils" |
95 | 23 | "launchpad.net/juju-core/worker/uniter" | 24 | "launchpad.net/juju-core/worker/uniter" |
96 | 24 | "launchpad.net/juju-core/worker/uniter/jujuc" | 25 | "launchpad.net/juju-core/worker/uniter/jujuc" |
97 | @@ -206,9 +207,10 @@ | |||
98 | 206 | uuid, err := utils.NewUUID() | 207 | uuid, err := utils.NewUUID() |
99 | 207 | c.Assert(err, gc.IsNil) | 208 | c.Assert(err, gc.IsNil) |
100 | 208 | for i, t := range runHookTests { | 209 | for i, t := range runHookTests { |
102 | 209 | c.Logf("test %d: %s; perm %v", i, t.summary, t.spec.perm) | 210 | c.Logf("\ntest %d: %s; perm %v", i, t.summary, t.spec.perm) |
103 | 210 | ctx := s.GetHookContext(c, uuid.String(), t.relid, t.remote) | 211 | ctx := s.GetHookContext(c, uuid.String(), t.relid, t.remote) |
104 | 211 | var charmDir, outPath string | 212 | var charmDir, outPath string |
105 | 213 | var hookExists bool | ||
106 | 212 | if t.spec.perm == 0 { | 214 | if t.spec.perm == 0 { |
107 | 213 | charmDir = c.MkDir() | 215 | charmDir = c.MkDir() |
108 | 214 | } else { | 216 | } else { |
109 | @@ -216,12 +218,15 @@ | |||
110 | 216 | spec.name = "something-happened" | 218 | spec.name = "something-happened" |
111 | 217 | c.Logf("makeCharm %#v", spec) | 219 | c.Logf("makeCharm %#v", spec) |
112 | 218 | charmDir, outPath = makeCharm(c, spec) | 220 | charmDir, outPath = makeCharm(c, spec) |
113 | 221 | hookExists = true | ||
114 | 219 | } | 222 | } |
115 | 220 | toolsDir := c.MkDir() | 223 | toolsDir := c.MkDir() |
116 | 221 | t0 := time.Now() | 224 | t0 := time.Now() |
117 | 222 | err := ctx.RunHook("something-happened", charmDir, toolsDir, "/path/to/socket") | 225 | err := ctx.RunHook("something-happened", charmDir, toolsDir, "/path/to/socket") |
119 | 223 | if t.err == "" { | 226 | if t.err == "" && hookExists { |
120 | 224 | c.Assert(err, gc.IsNil) | 227 | c.Assert(err, gc.IsNil) |
121 | 228 | } else if !hookExists { | ||
122 | 229 | c.Assert(uniter.IsMissingHookError(err), jc.IsTrue) | ||
123 | 225 | } else { | 230 | } else { |
124 | 226 | c.Assert(err, gc.ErrorMatches, t.err) | 231 | c.Assert(err, gc.ErrorMatches, t.err) |
125 | 227 | } | 232 | } |
126 | 228 | 233 | ||
127 | === added file 'worker/uniter/export_test.go' | |||
128 | --- worker/uniter/export_test.go 1970-01-01 00:00:00 +0000 | |||
129 | +++ worker/uniter/export_test.go 2013-12-13 02:48:10 +0000 | |||
130 | @@ -0,0 +1,8 @@ | |||
131 | 1 | // Copyright 2013 Canonical Ltd. | ||
132 | 2 | // Licensed under the AGPLv3, see LICENCE file for details. | ||
133 | 3 | |||
134 | 4 | package uniter | ||
135 | 5 | |||
136 | 6 | func SetUniterObserver(u *Uniter, observer UniterExecutionObserver) { | ||
137 | 7 | u.observer = observer | ||
138 | 8 | } | ||
139 | 0 | 9 | ||
140 | === modified file 'worker/uniter/uniter.go' | |||
141 | --- worker/uniter/uniter.go 2013-11-14 12:32:14 +0000 | |||
142 | +++ worker/uniter/uniter.go 2013-12-13 02:48:10 +0000 | |||
143 | @@ -32,6 +32,14 @@ | |||
144 | 32 | 32 | ||
145 | 33 | var logger = loggo.GetLogger("juju.worker.uniter") | 33 | var logger = loggo.GetLogger("juju.worker.uniter") |
146 | 34 | 34 | ||
147 | 35 | // A UniterExecutionObserver gets the appropriate methods called when a hook | ||
148 | 36 | // is executed and either succeeds or fails. Missing hooks don't get reported | ||
149 | 37 | // in this way. | ||
150 | 38 | type UniterExecutionObserver interface { | ||
151 | 39 | HookCompleted(hookName string) | ||
152 | 40 | HookFailed(hookName string) | ||
153 | 41 | } | ||
154 | 42 | |||
155 | 35 | // Uniter implements the capabilities of the unit agent. It is not intended to | 43 | // Uniter implements the capabilities of the unit agent. It is not intended to |
156 | 36 | // implement the actual *behaviour* of the unit agent; that responsibility is | 44 | // implement the actual *behaviour* of the unit agent; that responsibility is |
157 | 37 | // delegated to Mode values, which are expected to react to events and direct | 45 | // delegated to Mode values, which are expected to react to events and direct |
158 | @@ -59,6 +67,9 @@ | |||
159 | 59 | hookLock *fslock.Lock | 67 | hookLock *fslock.Lock |
160 | 60 | 68 | ||
161 | 61 | ranConfigChanged bool | 69 | ranConfigChanged bool |
162 | 70 | // The execution observer is only used in tests at this stage. Should this | ||
163 | 71 | // need to be extended, perhaps a list of observers would be needed. | ||
164 | 72 | observer UniterExecutionObserver | ||
165 | 62 | } | 73 | } |
166 | 63 | 74 | ||
167 | 64 | // NewUniter creates a new Uniter which will install, run, and upgrade | 75 | // NewUniter creates a new Uniter which will install, run, and upgrade |
168 | @@ -274,23 +285,26 @@ | |||
169 | 274 | // operation is not affected by the error. | 285 | // operation is not affected by the error. |
170 | 275 | var errHookFailed = stderrors.New("hook execution failed") | 286 | var errHookFailed = stderrors.New("hook execution failed") |
171 | 276 | 287 | ||
189 | 277 | // runHook executes the supplied hook.Info in an appropriate hook context. If | 288 | func (u *Uniter) getHookContext(hctxId string, relationId int, remoteUnitName string) (context *HookContext, err error) { |
190 | 278 | // the hook itself fails to execute, it returns errHookFailed. | 289 | |
191 | 279 | func (u *Uniter) runHook(hi hook.Info) (err error) { | 290 | apiAddrs, err := u.st.APIAddresses() |
192 | 280 | // Prepare context. | 291 | if err != nil { |
193 | 281 | if err = hi.Validate(); err != nil { | 292 | return nil, err |
194 | 282 | return err | 293 | } |
195 | 283 | } | 294 | ownerTag, err := u.service.GetOwnerTag() |
196 | 284 | 295 | if err != nil { | |
197 | 285 | hookName := string(hi.Kind) | 296 | return nil, err |
198 | 286 | relationId := -1 | 297 | } |
199 | 287 | if hi.Kind.IsRelation() { | 298 | ctxRelations := map[int]*ContextRelation{} |
200 | 288 | relationId = hi.RelationId | 299 | for id, r := range u.relationers { |
201 | 289 | if hookName, err = u.relationers[relationId].PrepareHook(hi); err != nil { | 300 | ctxRelations[id] = r.Context() |
202 | 290 | return err | 301 | } |
203 | 291 | } | 302 | |
204 | 292 | } | 303 | return NewHookContext(u.unit, hctxId, u.uuid, relationId, remoteUnitName, |
205 | 293 | hctxId := fmt.Sprintf("%s:%s:%d", u.unit.Name(), hookName, u.rand.Int63()) | 304 | ctxRelations, apiAddrs, ownerTag) |
206 | 305 | } | ||
207 | 306 | |||
208 | 307 | func (u *Uniter) acquireHookLock(message string) (err error) { | ||
209 | 294 | // We want to make sure we don't block forever when locking, but take the | 308 | // We want to make sure we don't block forever when locking, but take the |
210 | 295 | // tomb into account. | 309 | // tomb into account. |
211 | 296 | checkTomb := func() error { | 310 | checkTomb := func() error { |
212 | @@ -302,48 +316,88 @@ | |||
213 | 302 | } | 316 | } |
214 | 303 | return nil | 317 | return nil |
215 | 304 | } | 318 | } |
241 | 305 | lockMessage := fmt.Sprintf("%s: running hook %q", u.unit.Name(), hookName) | 319 | if err = u.hookLock.LockWithFunc(message, checkTomb); err != nil { |
242 | 306 | if err = u.hookLock.LockWithFunc(lockMessage, checkTomb); err != nil { | 320 | return err |
243 | 307 | return err | 321 | } |
244 | 308 | } | 322 | return nil |
245 | 309 | defer u.hookLock.Unlock() | 323 | } |
246 | 310 | 324 | ||
247 | 311 | ctxRelations := map[int]*ContextRelation{} | 325 | func (u *Uniter) startJujucServer(context *HookContext) (*jujuc.Server, string, error) { |
223 | 312 | for id, r := range u.relationers { | ||
224 | 313 | ctxRelations[id] = r.Context() | ||
225 | 314 | } | ||
226 | 315 | apiAddrs, err := u.st.APIAddresses() | ||
227 | 316 | if err != nil { | ||
228 | 317 | return err | ||
229 | 318 | } | ||
230 | 319 | |||
231 | 320 | ownerTag, err := u.service.GetOwnerTag() | ||
232 | 321 | if err != nil { | ||
233 | 322 | return err | ||
234 | 323 | } | ||
235 | 324 | hctx, err := NewHookContext(u.unit, hctxId, u.uuid, relationId, hi.RemoteUnit, | ||
236 | 325 | ctxRelations, apiAddrs, ownerTag) | ||
237 | 326 | if err != nil { | ||
238 | 327 | return err | ||
239 | 328 | } | ||
240 | 329 | |||
248 | 330 | // Prepare server. | 326 | // Prepare server. |
249 | 331 | getCmd := func(ctxId, cmdName string) (cmd.Command, error) { | 327 | getCmd := func(ctxId, cmdName string) (cmd.Command, error) { |
250 | 332 | // TODO: switch to long-running server with single context; | 328 | // TODO: switch to long-running server with single context; |
251 | 333 | // use nonce in place of context id. | 329 | // use nonce in place of context id. |
254 | 334 | if ctxId != hctxId { | 330 | if ctxId != context.id { |
255 | 335 | return nil, fmt.Errorf("expected context id %q, got %q", hctxId, ctxId) | 331 | return nil, fmt.Errorf("expected context id %q, got %q", context.id, ctxId) |
256 | 336 | } | 332 | } |
258 | 337 | return jujuc.NewCommand(hctx, cmdName) | 333 | return jujuc.NewCommand(context, cmdName) |
259 | 338 | } | 334 | } |
260 | 339 | socketPath := filepath.Join(u.baseDir, "agent.socket") | 335 | socketPath := filepath.Join(u.baseDir, "agent.socket") |
261 | 340 | // Use abstract namespace so we don't get stale socket files. | 336 | // Use abstract namespace so we don't get stale socket files. |
262 | 341 | socketPath = "@" + socketPath | 337 | socketPath = "@" + socketPath |
263 | 342 | srv, err := jujuc.NewServer(getCmd, socketPath) | 338 | srv, err := jujuc.NewServer(getCmd, socketPath) |
264 | 343 | if err != nil { | 339 | if err != nil { |
266 | 344 | return err | 340 | return nil, "", err |
267 | 345 | } | 341 | } |
268 | 346 | go srv.Run() | 342 | go srv.Run() |
269 | 343 | return srv, socketPath, nil | ||
270 | 344 | } | ||
271 | 345 | |||
272 | 346 | func (u *Uniter) notifyHookInternal(hook string, hctx *HookContext, method func(string)) { | ||
273 | 347 | if r, ok := hctx.HookRelation(); ok { | ||
274 | 348 | remote, _ := hctx.RemoteUnitName() | ||
275 | 349 | if remote != "" { | ||
276 | 350 | remote = " " + remote | ||
277 | 351 | } | ||
278 | 352 | hook = hook + remote + " " + r.FakeId() | ||
279 | 353 | } | ||
280 | 354 | method(hook) | ||
281 | 355 | } | ||
282 | 356 | |||
283 | 357 | func (u *Uniter) notifyHookCompleted(hook string, hctx *HookContext) { | ||
284 | 358 | if u.observer != nil { | ||
285 | 359 | u.notifyHookInternal(hook, hctx, u.observer.HookCompleted) | ||
286 | 360 | } | ||
287 | 361 | } | ||
288 | 362 | |||
289 | 363 | func (u *Uniter) notifyHookFailed(hook string, hctx *HookContext) { | ||
290 | 364 | if u.observer != nil { | ||
291 | 365 | u.notifyHookInternal(hook, hctx, u.observer.HookFailed) | ||
292 | 366 | } | ||
293 | 367 | } | ||
294 | 368 | |||
295 | 369 | // runHook executes the supplied hook.Info in an appropriate hook context. If | ||
296 | 370 | // the hook itself fails to execute, it returns errHookFailed. | ||
297 | 371 | func (u *Uniter) runHook(hi hook.Info) (err error) { | ||
298 | 372 | // Prepare context. | ||
299 | 373 | if err = hi.Validate(); err != nil { | ||
300 | 374 | return err | ||
301 | 375 | } | ||
302 | 376 | |||
303 | 377 | hookName := string(hi.Kind) | ||
304 | 378 | relationId := -1 | ||
305 | 379 | if hi.Kind.IsRelation() { | ||
306 | 380 | relationId = hi.RelationId | ||
307 | 381 | if hookName, err = u.relationers[relationId].PrepareHook(hi); err != nil { | ||
308 | 382 | return err | ||
309 | 383 | } | ||
310 | 384 | } | ||
311 | 385 | hctxId := fmt.Sprintf("%s:%s:%d", u.unit.Name(), hookName, u.rand.Int63()) | ||
312 | 386 | |||
313 | 387 | lockMessage := fmt.Sprintf("%s: running hook %q", u.unit.Name(), hookName) | ||
314 | 388 | if err = u.acquireHookLock(lockMessage); err != nil { | ||
315 | 389 | return err | ||
316 | 390 | } | ||
317 | 391 | defer u.hookLock.Unlock() | ||
318 | 392 | |||
319 | 393 | hctx, err := u.getHookContext(hctxId, relationId, hi.RemoteUnit) | ||
320 | 394 | if err != nil { | ||
321 | 395 | return err | ||
322 | 396 | } | ||
323 | 397 | srv, socketPath, err := u.startJujucServer(hctx) | ||
324 | 398 | if err != nil { | ||
325 | 399 | return err | ||
326 | 400 | } | ||
327 | 347 | defer srv.Close() | 401 | defer srv.Close() |
328 | 348 | 402 | ||
329 | 349 | // Run the hook. | 403 | // Run the hook. |
330 | @@ -351,14 +405,24 @@ | |||
331 | 351 | return err | 405 | return err |
332 | 352 | } | 406 | } |
333 | 353 | logger.Infof("running %q hook", hookName) | 407 | logger.Infof("running %q hook", hookName) |
335 | 354 | if err := hctx.RunHook(hookName, u.charm.Path(), u.toolsDir, socketPath); err != nil { | 408 | ranHook := true |
336 | 409 | err = hctx.RunHook(hookName, u.charm.Path(), u.toolsDir, socketPath) | ||
337 | 410 | if IsMissingHookError(err) { | ||
338 | 411 | ranHook = false | ||
339 | 412 | } else if err != nil { | ||
340 | 355 | logger.Errorf("hook failed: %s", err) | 413 | logger.Errorf("hook failed: %s", err) |
341 | 414 | u.notifyHookFailed(hookName, hctx) | ||
342 | 356 | return errHookFailed | 415 | return errHookFailed |
343 | 357 | } | 416 | } |
344 | 358 | if err := u.writeState(RunHook, Done, &hi, nil); err != nil { | 417 | if err := u.writeState(RunHook, Done, &hi, nil); err != nil { |
345 | 359 | return err | 418 | return err |
346 | 360 | } | 419 | } |
348 | 361 | logger.Infof("ran %q hook", hookName) | 420 | if ranHook { |
349 | 421 | logger.Infof("ran %q hook", hookName) | ||
350 | 422 | u.notifyHookCompleted(hookName, hctx) | ||
351 | 423 | } else { | ||
352 | 424 | logger.Infof("skipped %q hook (missing)", hookName) | ||
353 | 425 | } | ||
354 | 362 | return u.commitHook(hi) | 426 | return u.commitHook(hi) |
355 | 363 | } | 427 | } |
356 | 364 | 428 | ||
357 | 365 | 429 | ||
358 | === modified file 'worker/uniter/uniter_test.go' | |||
359 | --- worker/uniter/uniter_test.go 2013-11-07 09:09:55 +0000 | |||
360 | +++ worker/uniter/uniter_test.go 2013-12-13 02:48:10 +0000 | |||
361 | @@ -14,9 +14,7 @@ | |||
362 | 14 | "os" | 14 | "os" |
363 | 15 | "os/exec" | 15 | "os/exec" |
364 | 16 | "path/filepath" | 16 | "path/filepath" |
365 | 17 | "regexp" | ||
366 | 18 | "strconv" | 17 | "strconv" |
367 | 19 | "strings" | ||
368 | 20 | stdtesting "testing" | 18 | stdtesting "testing" |
369 | 21 | "time" | 19 | "time" |
370 | 22 | 20 | ||
371 | @@ -32,7 +30,7 @@ | |||
372 | 32 | "launchpad.net/juju-core/state/api/params" | 30 | "launchpad.net/juju-core/state/api/params" |
373 | 33 | apiuniter "launchpad.net/juju-core/state/api/uniter" | 31 | apiuniter "launchpad.net/juju-core/state/api/uniter" |
374 | 34 | coretesting "launchpad.net/juju-core/testing" | 32 | coretesting "launchpad.net/juju-core/testing" |
376 | 35 | "launchpad.net/juju-core/testing/checkers" | 33 | jc "launchpad.net/juju-core/testing/checkers" |
377 | 36 | "launchpad.net/juju-core/utils" | 34 | "launchpad.net/juju-core/utils" |
378 | 37 | "launchpad.net/juju-core/utils/fslock" | 35 | "launchpad.net/juju-core/utils/fslock" |
379 | 38 | "launchpad.net/juju-core/worker" | 36 | "launchpad.net/juju-core/worker" |
380 | @@ -153,6 +151,18 @@ | |||
381 | 153 | relation *state.Relation | 151 | relation *state.Relation |
382 | 154 | relationUnits map[string]*state.RelationUnit | 152 | relationUnits map[string]*state.RelationUnit |
383 | 155 | subordinate *state.Unit | 153 | subordinate *state.Unit |
384 | 154 | |||
385 | 155 | hooksCompleted []string | ||
386 | 156 | } | ||
387 | 157 | |||
388 | 158 | var _ uniter.UniterExecutionObserver = (*context)(nil) | ||
389 | 159 | |||
390 | 160 | func (ctx *context) HookCompleted(hookName string) { | ||
391 | 161 | ctx.hooksCompleted = append(ctx.hooksCompleted, hookName) | ||
392 | 162 | } | ||
393 | 163 | |||
394 | 164 | func (ctx *context) HookFailed(hookName string) { | ||
395 | 165 | ctx.hooksCompleted = append(ctx.hooksCompleted, "fail-"+hookName) | ||
396 | 156 | } | 166 | } |
397 | 157 | 167 | ||
398 | 158 | func (ctx *context) run(c *gc.C, steps []stepper) { | 168 | func (ctx *context) run(c *gc.C, steps []stepper) { |
399 | @@ -189,49 +199,17 @@ | |||
400 | 189 | c.Assert(err, gc.IsNil) | 199 | c.Assert(err, gc.IsNil) |
401 | 190 | } | 200 | } |
402 | 191 | 201 | ||
438 | 192 | func (ctx *context) matchLogHooks(c *gc.C) (match bool, overshoot bool) { | 202 | func (ctx *context) matchHooks(c *gc.C) (match bool, overshoot bool) { |
439 | 193 | // TODO(thumper): fix this to actually use a log writer to catch interesting log messages. | 203 | c.Logf("ctx.hooksCompleted: %#v", ctx.hooksCompleted) |
440 | 194 | // hookPattern matches juju-log calls as generated by writeHook. | 204 | if len(ctx.hooksCompleted) < len(ctx.hooks) { |
406 | 195 | hookPattern := fmt.Sprintf(`^.* INFO juju `+ | ||
407 | 196 | `u/0(| [a-z0-9-]+:[0-9]+)`+ // juju-log badge; group matches relation id | ||
408 | 197 | `: %s`+ // JUJU_ENV_UUID (context badge; prevents cross-test pollution) | ||
409 | 198 | ` ([0-9a-z-/ ]+)$`, // foo-relation-joined bar/123 | ||
410 | 199 | ctx.uuid, | ||
411 | 200 | ) | ||
412 | 201 | // donePattern matches uniter logging that indicates a hook has run. | ||
413 | 202 | donePattern := `^.* (INFO|ERROR) juju\.worker\.uniter (ran "[a-z0-9-]+" hook|hook failed)` | ||
414 | 203 | hookRegexp := regexp.MustCompile(hookPattern) | ||
415 | 204 | doneRegexp := regexp.MustCompile(donePattern) | ||
416 | 205 | |||
417 | 206 | // pending is empty while we scan for a new hook, and holds a value while | ||
418 | 207 | // we scan for output indicating that hook execution has finished; at which | ||
419 | 208 | // point, we add it to... | ||
420 | 209 | pending := "" | ||
421 | 210 | // ...actual, which holds a list of executed hooks, with basic information | ||
422 | 211 | // about the relation context, and which will be compared against ctx's | ||
423 | 212 | // complete list of expected hooks. | ||
424 | 213 | var actual []string | ||
425 | 214 | for _, line := range strings.Split(c.GetTestLog(), "\n") { | ||
426 | 215 | if pending == "" { | ||
427 | 216 | if parts := hookRegexp.FindStringSubmatch(line); parts != nil { | ||
428 | 217 | // "hook-name[ JUJU_REMOTE_UNIT]" + "[ JUJU_RELATION_ID] | ||
429 | 218 | pending = parts[2] + parts[1] | ||
430 | 219 | } | ||
431 | 220 | } else if doneRegexp.MatchString(line) { | ||
432 | 221 | actual = append(actual, pending) | ||
433 | 222 | pending = "" | ||
434 | 223 | } | ||
435 | 224 | } | ||
436 | 225 | c.Logf("actual: %#v", actual) | ||
437 | 226 | if len(actual) < len(ctx.hooks) { | ||
441 | 227 | return false, false | 205 | return false, false |
442 | 228 | } | 206 | } |
443 | 229 | for i, e := range ctx.hooks { | 207 | for i, e := range ctx.hooks { |
445 | 230 | if actual[i] != e { | 208 | if ctx.hooksCompleted[i] != e { |
446 | 231 | return false, false | 209 | return false, false |
447 | 232 | } | 210 | } |
448 | 233 | } | 211 | } |
450 | 234 | return true, len(actual) > len(ctx.hooks) | 212 | return true, len(ctx.hooksCompleted) > len(ctx.hooks) |
451 | 235 | } | 213 | } |
452 | 236 | 214 | ||
453 | 237 | var startupTests = []uniterTest{ | 215 | var startupTests = []uniterTest{ |
454 | @@ -827,7 +805,7 @@ | |||
455 | 827 | 805 | ||
456 | 828 | // ignore should not (only in v1) | 806 | // ignore should not (only in v1) |
457 | 829 | _, err = os.Stat(filepath.Join(ctx.path, "charm", "ignore")) | 807 | _, err = os.Stat(filepath.Join(ctx.path, "charm", "ignore")) |
459 | 830 | c.Assert(err, checkers.Satisfies, os.IsNotExist) | 808 | c.Assert(err, jc.Satisfies, os.IsNotExist) |
460 | 831 | 809 | ||
461 | 832 | // data should contain what was written in the start hook | 810 | // data should contain what was written in the start hook |
462 | 833 | data, err := ioutil.ReadFile(filepath.Join(ctx.path, "charm", "data")) | 811 | data, err := ioutil.ReadFile(filepath.Join(ctx.path, "charm", "data")) |
463 | @@ -1257,6 +1235,7 @@ | |||
464 | 1257 | panic("API connection not established") | 1235 | panic("API connection not established") |
465 | 1258 | } | 1236 | } |
466 | 1259 | ctx.uniter = uniter.NewUniter(ctx.s.uniter, s.unitTag, ctx.dataDir) | 1237 | ctx.uniter = uniter.NewUniter(ctx.s.uniter, s.unitTag, ctx.dataDir) |
467 | 1238 | uniter.SetUniterObserver(ctx.uniter, ctx) | ||
468 | 1260 | } | 1239 | } |
469 | 1261 | 1240 | ||
470 | 1262 | type waitUniterDead struct { | 1241 | type waitUniterDead struct { |
471 | @@ -1487,7 +1466,7 @@ | |||
472 | 1487 | } | 1466 | } |
473 | 1488 | ctx.hooks = append(ctx.hooks, s...) | 1467 | ctx.hooks = append(ctx.hooks, s...) |
474 | 1489 | c.Logf("waiting for hooks: %#v", ctx.hooks) | 1468 | c.Logf("waiting for hooks: %#v", ctx.hooks) |
476 | 1490 | match, overshoot := ctx.matchLogHooks(c) | 1469 | match, overshoot := ctx.matchHooks(c) |
477 | 1491 | if overshoot && len(s) == 0 { | 1470 | if overshoot && len(s) == 0 { |
478 | 1492 | c.Fatalf("ran more hooks than expected") | 1471 | c.Fatalf("ran more hooks than expected") |
479 | 1493 | } | 1472 | } |
480 | @@ -1499,7 +1478,7 @@ | |||
481 | 1499 | ctx.s.BackingState.StartSync() | 1478 | ctx.s.BackingState.StartSync() |
482 | 1500 | select { | 1479 | select { |
483 | 1501 | case <-time.After(coretesting.ShortWait): | 1480 | case <-time.After(coretesting.ShortWait): |
485 | 1502 | if match, _ = ctx.matchLogHooks(c); match { | 1481 | if match, _ = ctx.matchHooks(c); match { |
486 | 1503 | return | 1482 | return |
487 | 1504 | } | 1483 | } |
488 | 1505 | case <-timeout: | 1484 | case <-timeout: |
489 | @@ -1680,7 +1659,7 @@ | |||
490 | 1680 | func (s relationState) step(c *gc.C, ctx *context) { | 1659 | func (s relationState) step(c *gc.C, ctx *context) { |
491 | 1681 | err := ctx.relation.Refresh() | 1660 | err := ctx.relation.Refresh() |
492 | 1682 | if s.removed { | 1661 | if s.removed { |
494 | 1683 | c.Assert(err, checkers.Satisfies, errors.IsNotFoundError) | 1662 | c.Assert(err, jc.Satisfies, errors.IsNotFoundError) |
495 | 1684 | return | 1663 | return |
496 | 1685 | } | 1664 | } |
497 | 1686 | c.Assert(err, gc.IsNil) | 1665 | c.Assert(err, gc.IsNil) |
498 | @@ -1908,10 +1887,10 @@ | |||
499 | 1908 | 1887 | ||
500 | 1909 | var verifyHookSyncLockUnlocked = custom{func(c *gc.C, ctx *context) { | 1888 | var verifyHookSyncLockUnlocked = custom{func(c *gc.C, ctx *context) { |
501 | 1910 | lock := createHookLock(c, ctx.dataDir) | 1889 | lock := createHookLock(c, ctx.dataDir) |
503 | 1911 | c.Assert(lock.IsLocked(), gc.Equals, false) | 1890 | c.Assert(lock.IsLocked(), jc.IsFalse) |
504 | 1912 | }} | 1891 | }} |
505 | 1913 | 1892 | ||
506 | 1914 | var verifyHookSyncLockLocked = custom{func(c *gc.C, ctx *context) { | 1893 | var verifyHookSyncLockLocked = custom{func(c *gc.C, ctx *context) { |
507 | 1915 | lock := createHookLock(c, ctx.dataDir) | 1894 | lock := createHookLock(c, ctx.dataDir) |
509 | 1916 | c.Assert(lock, checkers.Satisfies, (*fslock.Lock).IsLocked) | 1895 | c.Assert(lock.IsLocked(), jc.IsTrue) |
510 | 1917 | }} | 1896 | }} |
Reviewers: mp+198668_ code.launchpad. net,
Message:
Please take a look.
Description:
Refactor uniter tests
We needed to have a better way to match the hooks that had been executed
by
the uniter in the tests. Instead of relying on log messages that the
hooks
wrote out, instead we have the uniter optionally call out to an
observer.
Part of this work also involved breaking up some of the larger methods
in the
Context and the Uniter methods. These broken up bits are used by the
code
that runs generic commands coming shortly.
A fundamental change is that now if a hook is missing, instead of
silently
passing, the context now returns a missingHookError.
https:/ /code.launchpad .net/~thumper/ juju-core/ refactor- uniter- tests/+ merge/198668
Requires: /code.launchpad .net/~thumper/ juju-core/ juju-run- listener/ +merge/ 198662
https:/
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/41210043/
Affected files (+181, -109 lines): uniter/ context. go uniter/ context_ test.go uniter/ export_ test.go uniter/ uniter. go uniter/ uniter_ test.go
A [revision details]
M worker/
M worker/
A worker/
M worker/
M worker/