Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
N
nebula
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
exchain
nebula
Commits
9ec88925
Unverified
Commit
9ec88925
authored
Oct 05, 2023
by
protolambda
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
op-service: command lifecycle testing with mock app and substitute process signals
parent
cb453762
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
241 additions
and
9 deletions
+241
-9
lifecycle.go
op-service/cliapp/lifecycle.go
+32
-9
lifecycle_test.go
op-service/cliapp/lifecycle_test.go
+209
-0
No files found.
op-service/cliapp/lifecycle.go
View file @
9ec88925
...
@@ -4,6 +4,7 @@ import (
...
@@ -4,6 +4,7 @@ import (
"context"
"context"
"errors"
"errors"
"fmt"
"fmt"
"os"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2"
...
@@ -34,23 +35,39 @@ type LifecycleAction func(ctx *cli.Context, close context.CancelCauseFunc) (Life
...
@@ -34,23 +35,39 @@ type LifecycleAction func(ctx *cli.Context, close context.CancelCauseFunc) (Life
// The app may continue to run post-processing until fully shutting down.
// The app may continue to run post-processing until fully shutting down.
// The user can force an early shut-down during post-processing by sending a second interruption signal.
// The user can force an early shut-down during post-processing by sending a second interruption signal.
func
LifecycleCmd
(
fn
LifecycleAction
)
cli
.
ActionFunc
{
func
LifecycleCmd
(
fn
LifecycleAction
)
cli
.
ActionFunc
{
return
lifecycleCmd
(
fn
,
opio
.
BlockOnInterruptsContext
)
}
type
waitSignalFn
func
(
ctx
context
.
Context
,
signals
...
os
.
Signal
)
var
interruptErr
=
errors
.
New
(
"interrupt signal"
)
func
lifecycleCmd
(
fn
LifecycleAction
,
blockOnInterrupt
waitSignalFn
)
cli
.
ActionFunc
{
return
func
(
ctx
*
cli
.
Context
)
error
{
return
func
(
ctx
*
cli
.
Context
)
error
{
hostCtx
:=
ctx
.
Context
hostCtx
:=
ctx
.
Context
appCtx
,
appCancel
:=
context
.
WithCancelCause
(
hostCtx
)
appCtx
,
appCancel
:=
context
.
WithCancelCause
(
hostCtx
)
ctx
.
Context
=
appCtx
ctx
.
Context
=
appCtx
go
func
()
{
go
func
()
{
opio
.
BlockOnInterruptsContex
t
(
appCtx
)
blockOnInterrup
t
(
appCtx
)
appCancel
(
errors
.
New
(
"interrupt signal"
)
)
appCancel
(
interruptErr
)
}()
}()
appLifecycle
,
err
:=
fn
(
ctx
,
appCancel
)
appLifecycle
,
err
:=
fn
(
ctx
,
appCancel
)
if
err
!=
nil
{
if
err
!=
nil
{
return
fmt
.
Errorf
(
"failed to setup: %w"
,
err
)
// join errors to include context cause (nil errors are dropped)
return
errors
.
Join
(
fmt
.
Errorf
(
"failed to setup: %w"
,
err
),
context
.
Cause
(
appCtx
),
)
}
}
if
err
:=
appLifecycle
.
Start
(
appCtx
);
err
!=
nil
{
if
err
:=
appLifecycle
.
Start
(
appCtx
);
err
!=
nil
{
return
fmt
.
Errorf
(
"failed to start: %w"
,
err
)
// join errors to include context cause (nil errors are dropped)
return
errors
.
Join
(
fmt
.
Errorf
(
"failed to start: %w"
,
err
),
context
.
Cause
(
appCtx
),
)
}
}
// wait for app to be closed (through interrupt, or app requests to be stopped by closing the context)
// wait for app to be closed (through interrupt, or app requests to be stopped by closing the context)
...
@@ -58,17 +75,23 @@ func LifecycleCmd(fn LifecycleAction) cli.ActionFunc {
...
@@ -58,17 +75,23 @@ func LifecycleCmd(fn LifecycleAction) cli.ActionFunc {
// Graceful stop context.
// Graceful stop context.
// This allows the service to idle before shutdown, if halted. User may interrupt.
// This allows the service to idle before shutdown, if halted. User may interrupt.
stopCtx
,
stopCancel
:=
context
.
WithCancel
(
hostCtx
)
stopCtx
,
stopCancel
:=
context
.
WithCancel
Cause
(
hostCtx
)
go
func
()
{
go
func
()
{
opio
.
BlockOnInterruptsContex
t
(
stopCtx
)
blockOnInterrup
t
(
stopCtx
)
stopCancel
()
stopCancel
(
interruptErr
)
}()
}()
// Execute graceful stop.
// Execute graceful stop.
stopErr
:=
appLifecycle
.
Stop
(
stopCtx
)
stopErr
:=
appLifecycle
.
Stop
(
stopCtx
)
stopCancel
()
stopCancel
(
nil
)
// note: Stop implementation may choose to suppress a context error,
// if it handles it well (e.g. stop idling after a halt).
if
stopErr
!=
nil
{
if
stopErr
!=
nil
{
return
fmt
.
Errorf
(
"failed to stop app: %w"
,
stopErr
)
// join errors to include context cause (nil errors are dropped)
return
errors
.
Join
(
fmt
.
Errorf
(
"failed to stop: %w"
,
stopErr
),
context
.
Cause
(
stopCtx
),
)
}
}
return
nil
return
nil
...
...
op-service/cliapp/lifecycle_test.go
0 → 100644
View file @
9ec88925
package
cliapp
import
(
"context"
"errors"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
)
type
fakeLifecycle
struct
{
startCh
,
stopCh
chan
error
stopped
bool
selfClose
context
.
CancelCauseFunc
}
func
(
f
*
fakeLifecycle
)
Start
(
ctx
context
.
Context
)
error
{
select
{
case
err
:=
<-
f
.
startCh
:
f
.
stopped
=
true
return
err
case
<-
ctx
.
Done
()
:
f
.
stopped
=
true
return
ctx
.
Err
()
}
}
func
(
f
*
fakeLifecycle
)
Stop
(
ctx
context
.
Context
)
error
{
select
{
case
err
:=
<-
f
.
stopCh
:
f
.
stopped
=
true
return
err
case
<-
ctx
.
Done
()
:
f
.
stopped
=
true
return
ctx
.
Err
()
}
}
func
(
f
*
fakeLifecycle
)
Stopped
()
bool
{
return
f
.
stopped
}
var
_
Lifecycle
=
(
*
fakeLifecycle
)(
nil
)
func
TestLifecycleCmd
(
t
*
testing
.
T
)
{
appSetup
:=
func
(
t
*
testing
.
T
,
shareApp
**
fakeLifecycle
)
(
signalCh
chan
struct
{},
initCh
,
startCh
,
stopCh
,
resultCh
chan
error
)
{
signalCh
=
make
(
chan
struct
{})
initCh
=
make
(
chan
error
)
startCh
=
make
(
chan
error
)
stopCh
=
make
(
chan
error
)
resultCh
=
make
(
chan
error
)
// mock an application that may fail at different stages of its lifecycle
mockAppFn
:=
func
(
ctx
*
cli
.
Context
,
close
context
.
CancelCauseFunc
)
(
Lifecycle
,
error
)
{
select
{
case
<-
ctx
.
Context
.
Done
()
:
return
nil
,
ctx
.
Context
.
Err
()
case
err
:=
<-
initCh
:
if
err
!=
nil
{
return
nil
,
err
}
}
app
:=
&
fakeLifecycle
{
startCh
:
startCh
,
stopCh
:
stopCh
,
stopped
:
false
,
selfClose
:
close
,
}
if
shareApp
!=
nil
{
*
shareApp
=
app
}
return
app
,
nil
}
// puppeteer a system signal waiter with a test signal channel
fakeSignalWaiter
:=
func
(
ctx
context
.
Context
,
signals
...
os
.
Signal
)
{
select
{
case
<-
ctx
.
Done
()
:
case
<-
signalCh
:
}
}
// turn our mock app and system signal into a lifecycle-managed command
actionFn
:=
lifecycleCmd
(
mockAppFn
,
fakeSignalWaiter
)
// try to shut the test down after being locked more than a minute
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
time
.
Minute
)
t
.
Cleanup
(
cancel
)
// create a fake CLI context to run our command with
cliCtx
:=
&
cli
.
Context
{
Context
:
ctx
,
App
:
&
cli
.
App
{
Name
:
"test-app"
,
Action
:
actionFn
,
},
Command
:
nil
,
}
// run the command async, it may block etc. The result will be sent back to the tester.
go
func
()
{
result
:=
actionFn
(
cliCtx
)
require
.
NoError
(
t
,
ctx
.
Err
(),
"expecting test context to be alive after end still"
)
// collect the result
resultCh
<-
result
}()
t
.
Cleanup
(
func
()
{
close
(
signalCh
)
close
(
initCh
)
close
(
startCh
)
close
(
stopCh
)
close
(
resultCh
)
})
return
}
t
.
Run
(
"interrupt int"
,
func
(
t
*
testing
.
T
)
{
signalCh
,
_
,
_
,
_
,
resultCh
:=
appSetup
(
t
,
nil
)
signalCh
<-
struct
{}{}
res
:=
<-
resultCh
require
.
ErrorIs
(
t
,
res
,
interruptErr
)
require
.
ErrorContains
(
t
,
res
,
"failed to setup"
)
})
t
.
Run
(
"failed init"
,
func
(
t
*
testing
.
T
)
{
_
,
initCh
,
_
,
_
,
resultCh
:=
appSetup
(
t
,
nil
)
v
:=
errors
.
New
(
"TEST INIT ERRROR"
)
initCh
<-
v
res
:=
<-
resultCh
require
.
ErrorIs
(
t
,
res
,
v
)
require
.
ErrorContains
(
t
,
res
,
"failed to setup"
)
})
t
.
Run
(
"interrupt start"
,
func
(
t
*
testing
.
T
)
{
var
app
*
fakeLifecycle
signalCh
,
initCh
,
_
,
_
,
resultCh
:=
appSetup
(
t
,
&
app
)
initCh
<-
nil
require
.
False
(
t
,
app
.
Stopped
())
signalCh
<-
struct
{}{}
res
:=
<-
resultCh
require
.
ErrorIs
(
t
,
res
,
interruptErr
)
require
.
ErrorContains
(
t
,
res
,
"failed to start"
)
require
.
True
(
t
,
app
.
Stopped
())
})
t
.
Run
(
"failed start"
,
func
(
t
*
testing
.
T
)
{
var
app
*
fakeLifecycle
_
,
initCh
,
startCh
,
_
,
resultCh
:=
appSetup
(
t
,
&
app
)
initCh
<-
nil
require
.
False
(
t
,
app
.
Stopped
())
v
:=
errors
.
New
(
"TEST START ERROR"
)
startCh
<-
v
res
:=
<-
resultCh
require
.
ErrorIs
(
t
,
res
,
v
)
require
.
ErrorContains
(
t
,
res
,
"failed to start"
)
require
.
True
(
t
,
app
.
Stopped
())
})
t
.
Run
(
"graceful shutdown"
,
func
(
t
*
testing
.
T
)
{
var
app
*
fakeLifecycle
signalCh
,
initCh
,
startCh
,
stopCh
,
resultCh
:=
appSetup
(
t
,
&
app
)
initCh
<-
nil
require
.
False
(
t
,
app
.
Stopped
())
startCh
<-
nil
signalCh
<-
struct
{}{}
// interrupt, but at an expected time
stopCh
<-
nil
// graceful shutdown after interrupt
require
.
NoError
(
t
,
<-
resultCh
,
nil
)
require
.
True
(
t
,
app
.
Stopped
())
})
t
.
Run
(
"interrupted shutdown"
,
func
(
t
*
testing
.
T
)
{
var
app
*
fakeLifecycle
signalCh
,
initCh
,
startCh
,
_
,
resultCh
:=
appSetup
(
t
,
&
app
)
initCh
<-
nil
require
.
False
(
t
,
app
.
Stopped
())
startCh
<-
nil
signalCh
<-
struct
{}{}
// start graceful shutdown
signalCh
<-
struct
{}{}
// interrupt before the shutdown process is allowed to complete
res
:=
<-
resultCh
require
.
ErrorIs
(
t
,
res
,
interruptErr
)
require
.
ErrorContains
(
t
,
res
,
"failed to stop"
)
require
.
True
(
t
,
app
.
Stopped
())
// still fully closes, interrupts only accelerate shutdown where possible.
})
t
.
Run
(
"failed shutdown"
,
func
(
t
*
testing
.
T
)
{
var
app
*
fakeLifecycle
signalCh
,
initCh
,
startCh
,
stopCh
,
resultCh
:=
appSetup
(
t
,
&
app
)
initCh
<-
nil
require
.
False
(
t
,
app
.
Stopped
())
startCh
<-
nil
signalCh
<-
struct
{}{}
// start graceful shutdown
v
:=
errors
.
New
(
"TEST STOP ERROR"
)
stopCh
<-
v
res
:=
<-
resultCh
require
.
ErrorIs
(
t
,
res
,
v
)
require
.
ErrorContains
(
t
,
res
,
"failed to stop"
)
require
.
True
(
t
,
app
.
Stopped
())
})
t
.
Run
(
"app self-close"
,
func
(
t
*
testing
.
T
)
{
var
app
*
fakeLifecycle
_
,
initCh
,
startCh
,
stopCh
,
resultCh
:=
appSetup
(
t
,
&
app
)
initCh
<-
nil
require
.
False
(
t
,
app
.
Stopped
())
startCh
<-
nil
v
:=
errors
.
New
(
"TEST SELF CLOSE ERROR"
)
app
.
selfClose
(
v
)
stopCh
<-
nil
require
.
NoError
(
t
,
<-
resultCh
,
"self-close is not considered an error"
)
require
.
True
(
t
,
app
.
Stopped
())
})
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment