mirror of https://github.com/maxcnunes/gaper.git
Improve test coverage
This commit is contained in:
parent
55cf00b791
commit
f578db31da
|
@ -7,3 +7,4 @@ coverage.out
|
||||||
.DS_Store
|
.DS_Store
|
||||||
testdata/server/server
|
testdata/server/server
|
||||||
dist
|
dist
|
||||||
|
test-srv
|
||||||
|
|
|
@ -46,9 +46,18 @@
|
||||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||||
version = "v1.0.0"
|
version = "v1.0.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/stretchr/objx"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "477a77ecc69700c7cdeb1fa9e129548e1c1c393c"
|
||||||
|
version = "v0.1.1"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/stretchr/testify"
|
name = "github.com/stretchr/testify"
|
||||||
packages = ["assert"]
|
packages = [
|
||||||
|
"assert",
|
||||||
|
"mock"
|
||||||
|
]
|
||||||
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
||||||
version = "v1.2.2"
|
version = "v1.2.2"
|
||||||
|
|
||||||
|
@ -67,6 +76,6 @@
|
||||||
[solve-meta]
|
[solve-meta]
|
||||||
analyzer-name = "dep"
|
analyzer-name = "dep"
|
||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
inputs-digest = "501c303fff1c8cdb5806d0ebb0c92b671095cbc2049f5b8d2286df401f5efce5"
|
inputs-digest = "8446ff85ebcb6bc802d3f444727c3910444a405f3c073c1095eb5cc8c497138b"
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
solver-version = 1
|
solver-version = 1
|
||||||
|
|
4
Makefile
4
Makefile
|
@ -1,6 +1,6 @@
|
||||||
OS := $(shell uname -s)
|
OS := $(shell uname -s)
|
||||||
TEST_PACKAGES := $(shell go list ./...)
|
TEST_PACKAGES := $(shell go list ./... | grep -v cmd)
|
||||||
COVER_PACKAGES := $(shell go list ./... | paste -sd "," -)
|
COVER_PACKAGES := $(shell go list ./... | grep -v cmd | paste -sd "," -)
|
||||||
LINTER := $(shell command -v gometalinter 2> /dev/null)
|
LINTER := $(shell command -v gometalinter 2> /dev/null)
|
||||||
|
|
||||||
.PHONY: setup
|
.PHONY: setup
|
||||||
|
|
|
@ -27,7 +27,6 @@ func main() {
|
||||||
PollInterval: c.Int("poll-interval"),
|
PollInterval: c.Int("poll-interval"),
|
||||||
Extensions: c.StringSlice("extensions"),
|
Extensions: c.StringSlice("extensions"),
|
||||||
NoRestartOn: c.String("no-restart-on"),
|
NoRestartOn: c.String("no-restart-on"),
|
||||||
ExitOnSIGINT: true,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +37,10 @@ func main() {
|
||||||
|
|
||||||
app.Action = func(c *cli.Context) {
|
app.Action = func(c *cli.Context) {
|
||||||
args := parseArgs(c)
|
args := parseArgs(c)
|
||||||
if err := gaper.Run(args); err != nil {
|
chOSSiginal := make(chan os.Signal, 2)
|
||||||
|
logger.Verbose(args.Verbose)
|
||||||
|
|
||||||
|
if err := gaper.Run(args, chOSSiginal); err != nil {
|
||||||
logger.Error(err)
|
logger.Error(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
134
gaper.go
134
gaper.go
|
@ -5,7 +5,6 @@ package gaper
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
@ -23,6 +22,13 @@ var DefaultExtensions = []string{"go"}
|
||||||
// DefaultPoolInterval is the time in ms used by the watcher to wait between scans
|
// DefaultPoolInterval is the time in ms used by the watcher to wait between scans
|
||||||
var DefaultPoolInterval = 500
|
var DefaultPoolInterval = 500
|
||||||
|
|
||||||
|
// No restart types
|
||||||
|
var (
|
||||||
|
NoRestartOnError = "error"
|
||||||
|
NoRestartOnSuccess = "success"
|
||||||
|
NoRestartOnExit = "exit"
|
||||||
|
)
|
||||||
|
|
||||||
var logger = NewLogger("gaper")
|
var logger = NewLogger("gaper")
|
||||||
|
|
||||||
// exit statuses
|
// exit statuses
|
||||||
|
@ -43,71 +49,53 @@ type Config struct {
|
||||||
Extensions []string
|
Extensions []string
|
||||||
NoRestartOn string
|
NoRestartOn string
|
||||||
Verbose bool
|
Verbose bool
|
||||||
ExitOnSIGINT bool
|
WorkingDirectory string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run in the gaper high level API
|
// Run starts the whole gaper process watching for file changes or exit codes
|
||||||
// It starts the whole gaper process watching for file changes or exit codes
|
|
||||||
// and restarting the program
|
// and restarting the program
|
||||||
func Run(cfg *Config) error { // nolint: gocyclo
|
func Run(cfg *Config, chOSSiginal chan os.Signal) error {
|
||||||
var err error
|
|
||||||
logger.Verbose(cfg.Verbose)
|
|
||||||
logger.Debug("Starting gaper")
|
logger.Debug("Starting gaper")
|
||||||
|
|
||||||
if len(cfg.BuildPath) == 0 {
|
if err := setupConfig(cfg); err != nil {
|
||||||
cfg.BuildPath = DefaultBuildPath
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.BuildArgs, err = parseInnerArgs(cfg.BuildArgs, cfg.BuildArgsMerged)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.ProgramArgs, err = parseInnerArgs(cfg.ProgramArgs, cfg.ProgramArgsMerged)
|
builder := NewBuilder(cfg.BuildPath, cfg.BinName, cfg.WorkingDirectory, cfg.BuildArgs)
|
||||||
if err != nil {
|
runner := NewRunner(os.Stdout, os.Stderr, filepath.Join(cfg.WorkingDirectory, builder.Binary()), cfg.ProgramArgs)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
wd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.WatchItems) == 0 {
|
|
||||||
cfg.WatchItems = append(cfg.WatchItems, cfg.BuildPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
builder := NewBuilder(cfg.BuildPath, cfg.BinName, wd, cfg.BuildArgs)
|
|
||||||
runner := NewRunner(os.Stdout, os.Stderr, filepath.Join(wd, builder.Binary()), cfg.ProgramArgs)
|
|
||||||
|
|
||||||
if err = builder.Build(); err != nil {
|
|
||||||
return fmt.Errorf("build error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdown(runner, cfg.ExitOnSIGINT)
|
|
||||||
|
|
||||||
if _, err = runner.Run(); err != nil {
|
|
||||||
return fmt.Errorf("run error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
watcher, err := NewWatcher(cfg.PollInterval, cfg.WatchItems, cfg.IgnoreItems, cfg.Extensions)
|
watcher, err := NewWatcher(cfg.PollInterval, cfg.WatchItems, cfg.IgnoreItems, cfg.Extensions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("watcher error: %v", err)
|
return fmt.Errorf("watcher error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return run(cfg, chOSSiginal, builder, runner, watcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(cfg *Config, chOSSiginal chan os.Signal, builder Builder, runner Runner, watcher Watcher) error { // nolint: gocyclo
|
||||||
|
if err := builder.Build(); err != nil {
|
||||||
|
return fmt.Errorf("build error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// listen for OS signals
|
||||||
|
signal.Notify(chOSSiginal, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
if _, err := runner.Run(); err != nil {
|
||||||
|
return fmt.Errorf("run error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// flag to know if an exit was caused by a restart from a file changing
|
// flag to know if an exit was caused by a restart from a file changing
|
||||||
changeRestart := false
|
changeRestart := false
|
||||||
|
|
||||||
go watcher.Watch()
|
go watcher.Watch()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case event := <-watcher.Events:
|
case event := <-watcher.Events():
|
||||||
logger.Debug("Detected new changed file: ", event)
|
logger.Debug("Detected new changed file: ", event)
|
||||||
changeRestart = true
|
changeRestart = true
|
||||||
if err := restart(builder, runner); err != nil {
|
if err := restart(builder, runner); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case err := <-watcher.Errors:
|
case err := <-watcher.Errors():
|
||||||
return fmt.Errorf("error on watching files: %v", err)
|
return fmt.Errorf("error on watching files: %v", err)
|
||||||
case err := <-runner.Errors():
|
case err := <-runner.Errors():
|
||||||
logger.Debug("Detected program exit: ", err)
|
logger.Debug("Detected program exit: ", err)
|
||||||
|
@ -121,6 +109,14 @@ func Run(cfg *Config) error { // nolint: gocyclo
|
||||||
if err = handleProgramExit(builder, runner, err, cfg.NoRestartOn); err != nil {
|
if err = handleProgramExit(builder, runner, err, cfg.NoRestartOn); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case signal := <-chOSSiginal:
|
||||||
|
logger.Debug("Got signal: ", signal)
|
||||||
|
|
||||||
|
if err := runner.Kill(); err != nil {
|
||||||
|
logger.Error("Error killing: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("OS signal: %v", signal)
|
||||||
default:
|
default:
|
||||||
time.Sleep(time.Duration(cfg.PollInterval) * time.Millisecond)
|
time.Sleep(time.Duration(cfg.PollInterval) * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
@ -149,49 +145,53 @@ func restart(builder Builder, runner Runner) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleProgramExit(builder Builder, runner Runner, err error, noRestartOn string) error {
|
func handleProgramExit(builder Builder, runner Runner, err error, noRestartOn string) error {
|
||||||
var exitStatus int
|
exitStatus := runner.ExitStatus(err)
|
||||||
if exiterr, ok := err.(*exec.ExitError); ok {
|
|
||||||
status, oks := exiterr.Sys().(syscall.WaitStatus)
|
|
||||||
if !oks {
|
|
||||||
return fmt.Errorf("couldn't resolve exit status: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
exitStatus = status.ExitStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
// if "error", an exit code of 0 will still restart.
|
// if "error", an exit code of 0 will still restart.
|
||||||
if noRestartOn == "error" && exitStatus == exitStatusError {
|
if noRestartOn == NoRestartOnError && exitStatus == exitStatusError {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if "success", no restart only if exit code is 0.
|
// if "success", no restart only if exit code is 0.
|
||||||
if noRestartOn == "success" && exitStatus == exitStatusSuccess {
|
if noRestartOn == NoRestartOnSuccess && exitStatus == exitStatusSuccess {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if "exit", no restart regardless of exit code.
|
// if "exit", no restart regardless of exit code.
|
||||||
if noRestartOn == "exit" {
|
if noRestartOn == NoRestartOnExit {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return restart(builder, runner)
|
return restart(builder, runner)
|
||||||
}
|
}
|
||||||
|
|
||||||
func shutdown(runner Runner, exitOnSIGINT bool) {
|
func setupConfig(cfg *Config) error {
|
||||||
c := make(chan os.Signal, 2)
|
var err error
|
||||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
|
||||||
go func() {
|
|
||||||
s := <-c
|
|
||||||
logger.Debug("Got signal: ", s)
|
|
||||||
|
|
||||||
if err := runner.Kill(); err != nil {
|
if len(cfg.BuildPath) == 0 {
|
||||||
logger.Error("Error killing: ", err)
|
cfg.BuildPath = DefaultBuildPath
|
||||||
}
|
}
|
||||||
|
|
||||||
if exitOnSIGINT {
|
cfg.BuildArgs, err = parseInnerArgs(cfg.BuildArgs, cfg.BuildArgsMerged)
|
||||||
os.Exit(0)
|
if err != nil {
|
||||||
}
|
return err
|
||||||
}()
|
}
|
||||||
|
|
||||||
|
cfg.ProgramArgs, err = parseInnerArgs(cfg.ProgramArgs, cfg.ProgramArgsMerged)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.WorkingDirectory, err = os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.WatchItems) == 0 {
|
||||||
|
cfg.WatchItems = append(cfg.WatchItems, cfg.BuildPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseInnerArgs(args []string, argsm string) ([]string, error) {
|
func parseInnerArgs(args []string, argsm string) ([]string, error) {
|
||||||
|
|
236
gaper_test.go
236
gaper_test.go
|
@ -1,9 +1,241 @@
|
||||||
package gaper
|
package gaper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/maxcnunes/gaper/testdata"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGaper(t *testing.T) {
|
func TestGaperRunStopOnSGINT(t *testing.T) {
|
||||||
// TODO: add test to gaper high level API
|
args := &Config{
|
||||||
|
BuildPath: filepath.Join("testdata", "server"),
|
||||||
|
}
|
||||||
|
|
||||||
|
chOSSiginal := make(chan os.Signal, 2)
|
||||||
|
go func() {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
chOSSiginal <- syscall.SIGINT
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := Run(args, chOSSiginal)
|
||||||
|
assert.NotNil(t, err, "build error")
|
||||||
|
assert.Equal(t, "OS signal: interrupt", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGaperSetupConfigNoParams(t *testing.T) {
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
args := &Config{}
|
||||||
|
err := setupConfig(args)
|
||||||
|
assert.Nil(t, err, "build error")
|
||||||
|
assert.Equal(t, args.BuildPath, ".")
|
||||||
|
assert.Equal(t, args.WorkingDirectory, cwd)
|
||||||
|
assert.Equal(t, args.WatchItems, []string{"."})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGaperWatcherError(t *testing.T) {
|
||||||
|
mockBuilder := new(testdata.MockBuilder)
|
||||||
|
mockBuilder.On("Build").Return(nil)
|
||||||
|
|
||||||
|
mockRunner := new(testdata.MockRunner)
|
||||||
|
cmd := &exec.Cmd{}
|
||||||
|
runnerErrorsChan := make(chan error)
|
||||||
|
mockRunner.On("Run").Return(cmd, nil)
|
||||||
|
mockRunner.On("Errors").Return(runnerErrorsChan)
|
||||||
|
|
||||||
|
mockWatcher := new(testdata.MockWacther)
|
||||||
|
watcherErrorsChan := make(chan error)
|
||||||
|
watcherEvetnsChan := make(chan string)
|
||||||
|
mockWatcher.On("Errors").Return(watcherErrorsChan)
|
||||||
|
mockWatcher.On("Events").Return(watcherEvetnsChan)
|
||||||
|
|
||||||
|
dir := filepath.Join("testdata", "server")
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
BinName: "test-srv",
|
||||||
|
BuildPath: dir,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
watcherErrorsChan <- errors.New("watcher-error")
|
||||||
|
}()
|
||||||
|
chOSSiginal := make(chan os.Signal, 2)
|
||||||
|
err := run(cfg, chOSSiginal, mockBuilder, mockRunner, mockWatcher)
|
||||||
|
assert.NotNil(t, err, "build error")
|
||||||
|
assert.Equal(t, "error on watching files: watcher-error", err.Error())
|
||||||
|
mockBuilder.AssertExpectations(t)
|
||||||
|
mockRunner.AssertExpectations(t)
|
||||||
|
mockWatcher.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGaperProgramExit(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
exitStatus int
|
||||||
|
noRestartOn string
|
||||||
|
restart bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no restart on exit error with no-restart-on=error",
|
||||||
|
exitStatus: exitStatusError,
|
||||||
|
noRestartOn: NoRestartOnError,
|
||||||
|
restart: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no restart on exit success with no-restart-on=success",
|
||||||
|
exitStatus: exitStatusSuccess,
|
||||||
|
noRestartOn: NoRestartOnSuccess,
|
||||||
|
restart: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no restart on exit error with no-restart-on=exit",
|
||||||
|
exitStatus: exitStatusError,
|
||||||
|
noRestartOn: NoRestartOnExit,
|
||||||
|
restart: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no restart on exit success with no-restart-on=exit",
|
||||||
|
exitStatus: exitStatusSuccess,
|
||||||
|
noRestartOn: NoRestartOnExit,
|
||||||
|
restart: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "restart on exit error with disabled no-restart-on",
|
||||||
|
exitStatus: exitStatusError,
|
||||||
|
restart: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "restart on exit success with disabled no-restart-on",
|
||||||
|
exitStatus: exitStatusSuccess,
|
||||||
|
restart: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
mockBuilder := new(testdata.MockBuilder)
|
||||||
|
mockBuilder.On("Build").Return(nil)
|
||||||
|
|
||||||
|
mockRunner := new(testdata.MockRunner)
|
||||||
|
cmd := &exec.Cmd{}
|
||||||
|
runnerErrorsChan := make(chan error)
|
||||||
|
mockRunner.On("Run").Return(cmd, nil)
|
||||||
|
mockRunner.On("Kill").Return(nil)
|
||||||
|
mockRunner.On("Errors").Return(runnerErrorsChan)
|
||||||
|
mockRunner.On("ExitStatus").Return(tc.exitStatus)
|
||||||
|
if tc.restart {
|
||||||
|
mockRunner.On("Exited").Return(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
mockWatcher := new(testdata.MockWacther)
|
||||||
|
watcherErrorsChan := make(chan error)
|
||||||
|
watcherEvetnsChan := make(chan string)
|
||||||
|
mockWatcher.On("Errors").Return(watcherErrorsChan)
|
||||||
|
mockWatcher.On("Events").Return(watcherEvetnsChan)
|
||||||
|
|
||||||
|
dir := filepath.Join("testdata", "server")
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
BinName: "test-srv",
|
||||||
|
BuildPath: dir,
|
||||||
|
NoRestartOn: tc.noRestartOn,
|
||||||
|
}
|
||||||
|
|
||||||
|
chOSSiginal := make(chan os.Signal, 2)
|
||||||
|
go func() {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
runnerErrorsChan <- errors.New("runner-error")
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
chOSSiginal <- syscall.SIGINT
|
||||||
|
}()
|
||||||
|
err := run(cfg, chOSSiginal, mockBuilder, mockRunner, mockWatcher)
|
||||||
|
assert.NotNil(t, err, "build error")
|
||||||
|
assert.Equal(t, "OS signal: interrupt", err.Error())
|
||||||
|
mockBuilder.AssertExpectations(t)
|
||||||
|
mockRunner.AssertExpectations(t)
|
||||||
|
mockWatcher.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGaperRestartExited(t *testing.T) {
|
||||||
|
mockBuilder := new(testdata.MockBuilder)
|
||||||
|
mockBuilder.On("Build").Return(nil)
|
||||||
|
|
||||||
|
mockRunner := new(testdata.MockRunner)
|
||||||
|
cmd := &exec.Cmd{}
|
||||||
|
mockRunner.On("Run").Return(cmd, nil)
|
||||||
|
mockRunner.On("Exited").Return(true)
|
||||||
|
|
||||||
|
err := restart(mockBuilder, mockRunner)
|
||||||
|
assert.Nil(t, err, "restart error")
|
||||||
|
mockBuilder.AssertExpectations(t)
|
||||||
|
mockRunner.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGaperRestartNotExited(t *testing.T) {
|
||||||
|
mockBuilder := new(testdata.MockBuilder)
|
||||||
|
mockBuilder.On("Build").Return(nil)
|
||||||
|
|
||||||
|
mockRunner := new(testdata.MockRunner)
|
||||||
|
cmd := &exec.Cmd{}
|
||||||
|
mockRunner.On("Run").Return(cmd, nil)
|
||||||
|
mockRunner.On("Kill").Return(nil)
|
||||||
|
mockRunner.On("Exited").Return(false)
|
||||||
|
|
||||||
|
err := restart(mockBuilder, mockRunner)
|
||||||
|
assert.Nil(t, err, "restart error")
|
||||||
|
mockBuilder.AssertExpectations(t)
|
||||||
|
mockRunner.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGaperRestartNotExitedKillFail(t *testing.T) {
|
||||||
|
mockBuilder := new(testdata.MockBuilder)
|
||||||
|
|
||||||
|
mockRunner := new(testdata.MockRunner)
|
||||||
|
mockRunner.On("Kill").Return(errors.New("kill-error"))
|
||||||
|
mockRunner.On("Exited").Return(false)
|
||||||
|
|
||||||
|
err := restart(mockBuilder, mockRunner)
|
||||||
|
assert.NotNil(t, err, "restart error")
|
||||||
|
assert.Equal(t, "kill error: kill-error", err.Error())
|
||||||
|
mockBuilder.AssertExpectations(t)
|
||||||
|
mockRunner.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGaperRestartBuildFail(t *testing.T) {
|
||||||
|
mockBuilder := new(testdata.MockBuilder)
|
||||||
|
mockBuilder.On("Build").Return(errors.New("build-error"))
|
||||||
|
|
||||||
|
mockRunner := new(testdata.MockRunner)
|
||||||
|
mockRunner.On("Exited").Return(true)
|
||||||
|
|
||||||
|
err := restart(mockBuilder, mockRunner)
|
||||||
|
assert.NotNil(t, err, "restart error")
|
||||||
|
assert.Equal(t, "build error: build-error", err.Error())
|
||||||
|
mockBuilder.AssertExpectations(t)
|
||||||
|
mockRunner.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGaperRestartRunFail(t *testing.T) {
|
||||||
|
mockBuilder := new(testdata.MockBuilder)
|
||||||
|
mockBuilder.On("Build").Return(nil)
|
||||||
|
|
||||||
|
mockRunner := new(testdata.MockRunner)
|
||||||
|
cmd := &exec.Cmd{}
|
||||||
|
mockRunner.On("Run").Return(cmd, errors.New("run-error"))
|
||||||
|
mockRunner.On("Exited").Return(true)
|
||||||
|
|
||||||
|
err := restart(mockBuilder, mockRunner)
|
||||||
|
assert.NotNil(t, err, "restart error")
|
||||||
|
assert.Equal(t, "run error: run-error", err.Error())
|
||||||
|
mockBuilder.AssertExpectations(t)
|
||||||
|
mockRunner.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
14
runner.go
14
runner.go
|
@ -7,6 +7,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,6 +23,7 @@ type Runner interface {
|
||||||
Kill() error
|
Kill() error
|
||||||
Errors() chan error
|
Errors() chan error
|
||||||
Exited() bool
|
Exited() bool
|
||||||
|
ExitStatus(err error) int
|
||||||
}
|
}
|
||||||
|
|
||||||
type runner struct {
|
type runner struct {
|
||||||
|
@ -111,6 +113,18 @@ func (r *runner) Errors() chan error {
|
||||||
return r.errors
|
return r.errors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExitStatus resolves the exit status
|
||||||
|
func (r *runner) ExitStatus(err error) int {
|
||||||
|
var exitStatus int
|
||||||
|
if exiterr, ok := err.(*exec.ExitError); ok {
|
||||||
|
if status, oks := exiterr.Sys().(syscall.WaitStatus); oks {
|
||||||
|
exitStatus = status.ExitStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return exitStatus
|
||||||
|
}
|
||||||
|
|
||||||
func (r *runner) runBin() error {
|
func (r *runner) runBin() error {
|
||||||
r.command = exec.Command(r.bin, r.args...) // nolint gas
|
r.command = exec.Command(r.bin, r.args...) // nolint gas
|
||||||
stdout, err := r.command.StdoutPipe()
|
stdout, err := r.command.StdoutPipe()
|
||||||
|
|
|
@ -2,7 +2,9 @@ package gaper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -48,3 +50,32 @@ func TestRunnerSuccessKill(t *testing.T) {
|
||||||
errCmd := <-runner.Errors()
|
errCmd := <-runner.Errors()
|
||||||
assert.NotNil(t, errCmd, "kill program")
|
assert.NotNil(t, errCmd, "kill program")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunnerExitedNotStarted(t *testing.T) {
|
||||||
|
runner := NewRunner(os.Stdout, os.Stderr, "", nil)
|
||||||
|
assert.Equal(t, runner.Exited(), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunnerExitStatusNonExitError(t *testing.T) {
|
||||||
|
runner := NewRunner(os.Stdout, os.Stderr, "", nil)
|
||||||
|
err := errors.New("non exec.ExitError")
|
||||||
|
assert.Equal(t, runner.ExitStatus(err), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testExit() {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunnerExitStatusExitError(t *testing.T) {
|
||||||
|
if os.Getenv("TEST_EXIT") == "1" {
|
||||||
|
testExit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(os.Args[0], "-test.run=TestRunnerExitStatusExitError")
|
||||||
|
cmd.Env = append(os.Environ(), "TEST_EXIT=1")
|
||||||
|
err := cmd.Run()
|
||||||
|
|
||||||
|
runner := NewRunner(os.Stdout, os.Stderr, "", nil)
|
||||||
|
assert.Equal(t, runner.ExitStatus(err), 1)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
package testdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockBuilder ...
|
||||||
|
type MockBuilder struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build ...
|
||||||
|
func (m *MockBuilder) Build() error {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary ...
|
||||||
|
func (m *MockBuilder) Binary() string {
|
||||||
|
args := m.Called()
|
||||||
|
return args.String(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockRunner ...
|
||||||
|
type MockRunner struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run ...
|
||||||
|
func (m *MockRunner) Run() (*exec.Cmd, error) {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(*exec.Cmd), args.Error(1)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill ...
|
||||||
|
func (m *MockRunner) Kill() error {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors ...
|
||||||
|
func (m *MockRunner) Errors() chan error {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(chan error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exited ...
|
||||||
|
func (m *MockRunner) Exited() bool {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Bool(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExitStatus ...
|
||||||
|
func (m *MockRunner) ExitStatus(err error) int {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Int(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockWacther ...
|
||||||
|
type MockWacther struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch ...
|
||||||
|
func (m *MockWacther) Watch() {}
|
||||||
|
|
||||||
|
// Events ...
|
||||||
|
func (m *MockWacther) Events() chan string {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(chan string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors ...
|
||||||
|
func (m *MockWacther) Errors() chan error {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(chan error)
|
||||||
|
}
|
64
watcher.go
64
watcher.go
|
@ -12,17 +12,24 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Watcher is a interface for the watch process
|
// Watcher is a interface for the watch process
|
||||||
type Watcher struct {
|
type Watcher interface {
|
||||||
PollInterval int
|
Watch()
|
||||||
WatchItems map[string]bool
|
Errors() chan error
|
||||||
IgnoreItems map[string]bool
|
Events() chan string
|
||||||
AllowedExtensions map[string]bool
|
}
|
||||||
Events chan string
|
|
||||||
Errors chan error
|
// watcher is a interface for the watch process
|
||||||
|
type watcher struct {
|
||||||
|
pollInterval int
|
||||||
|
watchItems map[string]bool
|
||||||
|
ignoreItems map[string]bool
|
||||||
|
allowedExtensions map[string]bool
|
||||||
|
events chan string
|
||||||
|
errors chan error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWatcher creates a new watcher
|
// NewWatcher creates a new watcher
|
||||||
func NewWatcher(pollInterval int, watchItems []string, ignoreItems []string, extensions []string) (*Watcher, error) {
|
func NewWatcher(pollInterval int, watchItems []string, ignoreItems []string, extensions []string) (Watcher, error) {
|
||||||
if pollInterval == 0 {
|
if pollInterval == 0 {
|
||||||
pollInterval = DefaultPoolInterval
|
pollInterval = DefaultPoolInterval
|
||||||
}
|
}
|
||||||
|
@ -48,13 +55,13 @@ func NewWatcher(pollInterval int, watchItems []string, ignoreItems []string, ext
|
||||||
|
|
||||||
logger.Debugf("Resolved watch paths: %v", watchPaths)
|
logger.Debugf("Resolved watch paths: %v", watchPaths)
|
||||||
logger.Debugf("Resolved ignore paths: %v", ignorePaths)
|
logger.Debugf("Resolved ignore paths: %v", ignorePaths)
|
||||||
return &Watcher{
|
return &watcher{
|
||||||
Events: make(chan string),
|
events: make(chan string),
|
||||||
Errors: make(chan error),
|
errors: make(chan error),
|
||||||
PollInterval: pollInterval,
|
pollInterval: pollInterval,
|
||||||
WatchItems: watchPaths,
|
watchItems: watchPaths,
|
||||||
IgnoreItems: ignorePaths,
|
ignoreItems: ignorePaths,
|
||||||
AllowedExtensions: allowedExts,
|
allowedExtensions: allowedExts,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,26 +69,37 @@ var startTime = time.Now()
|
||||||
var errDetectedChange = errors.New("done")
|
var errDetectedChange = errors.New("done")
|
||||||
|
|
||||||
// Watch starts watching for file changes
|
// Watch starts watching for file changes
|
||||||
func (w *Watcher) Watch() {
|
func (w *watcher) Watch() {
|
||||||
for {
|
for {
|
||||||
for watchPath := range w.WatchItems {
|
for watchPath := range w.watchItems {
|
||||||
fileChanged, err := w.scanChange(watchPath)
|
fileChanged, err := w.scanChange(watchPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.Errors <- err
|
w.errors <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if fileChanged != "" {
|
if fileChanged != "" {
|
||||||
w.Events <- fileChanged
|
w.events <- fileChanged
|
||||||
startTime = time.Now()
|
startTime = time.Now()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(time.Duration(w.PollInterval) * time.Millisecond)
|
time.Sleep(time.Duration(w.pollInterval) * time.Millisecond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Watcher) scanChange(watchPath string) (string, error) {
|
// Events get events occurred during the watching
|
||||||
|
// these events are emited only a file changing is detected
|
||||||
|
func (w *watcher) Events() chan string {
|
||||||
|
return w.events
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors get errors occurred during the watching
|
||||||
|
func (w *watcher) Errors() chan error {
|
||||||
|
return w.errors
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watcher) scanChange(watchPath string) (string, error) {
|
||||||
logger.Debug("Watching ", watchPath)
|
logger.Debug("Watching ", watchPath)
|
||||||
|
|
||||||
var fileChanged string
|
var fileChanged string
|
||||||
|
@ -92,12 +110,12 @@ func (w *Watcher) scanChange(watchPath string) (string, error) {
|
||||||
return skipFile(info)
|
return skipFile(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ignored := w.IgnoreItems[path]; ignored {
|
if _, ignored := w.ignoreItems[path]; ignored {
|
||||||
return skipFile(info)
|
return skipFile(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
ext := filepath.Ext(path)
|
ext := filepath.Ext(path)
|
||||||
if _, ok := w.AllowedExtensions[ext]; ok && info.ModTime().After(startTime) {
|
if _, ok := w.allowedExtensions[ext]; ok && info.ModTime().After(startTime) {
|
||||||
fileChanged = path
|
fileChanged = path
|
||||||
return errDetectedChange
|
return errDetectedChange
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,18 +16,19 @@ func TestWatcherDefaultValues(t *testing.T) {
|
||||||
var ignoreItems []string
|
var ignoreItems []string
|
||||||
var extensions []string
|
var extensions []string
|
||||||
|
|
||||||
w, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions)
|
wt, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions)
|
||||||
|
|
||||||
expectedPath := "testdata/server"
|
expectedPath := "testdata/server"
|
||||||
if runtime.GOOS == OSWindows {
|
if runtime.GOOS == OSWindows {
|
||||||
expectedPath = "testdata\\server"
|
expectedPath = "testdata\\server"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
w := wt.(*watcher)
|
||||||
assert.Nil(t, err, "wacher error")
|
assert.Nil(t, err, "wacher error")
|
||||||
assert.Equal(t, 500, w.PollInterval)
|
assert.Equal(t, 500, w.pollInterval)
|
||||||
assert.Equal(t, map[string]bool{expectedPath: true}, w.WatchItems)
|
assert.Equal(t, map[string]bool{expectedPath: true}, w.watchItems)
|
||||||
assert.Len(t, w.IgnoreItems, 0)
|
assert.Len(t, w.ignoreItems, 0)
|
||||||
assert.Equal(t, map[string]bool{".go": true}, w.AllowedExtensions)
|
assert.Equal(t, map[string]bool{".go": true}, w.allowedExtensions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWatcherGlobPath(t *testing.T) {
|
func TestWatcherGlobPath(t *testing.T) {
|
||||||
|
@ -36,9 +37,10 @@ func TestWatcherGlobPath(t *testing.T) {
|
||||||
ignoreItems := []string{"./testdata/**/*_test.go"}
|
ignoreItems := []string{"./testdata/**/*_test.go"}
|
||||||
var extensions []string
|
var extensions []string
|
||||||
|
|
||||||
w, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions)
|
wt, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions)
|
||||||
assert.Nil(t, err, "wacher error")
|
assert.Nil(t, err, "wacher error")
|
||||||
assert.Equal(t, map[string]bool{"testdata/server/main_test.go": true}, w.IgnoreItems)
|
w := wt.(*watcher)
|
||||||
|
assert.Equal(t, map[string]bool{"testdata/server/main_test.go": true}, w.ignoreItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWatcherRemoveOverlapdPaths(t *testing.T) {
|
func TestWatcherRemoveOverlapdPaths(t *testing.T) {
|
||||||
|
@ -47,9 +49,10 @@ func TestWatcherRemoveOverlapdPaths(t *testing.T) {
|
||||||
ignoreItems := []string{"./testdata/**/*", "./testdata/server"}
|
ignoreItems := []string{"./testdata/**/*", "./testdata/server"}
|
||||||
var extensions []string
|
var extensions []string
|
||||||
|
|
||||||
w, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions)
|
wt, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions)
|
||||||
assert.Nil(t, err, "wacher error")
|
assert.Nil(t, err, "wacher error")
|
||||||
assert.Equal(t, map[string]bool{"./testdata/server": true}, w.IgnoreItems)
|
w := wt.(*watcher)
|
||||||
|
assert.Equal(t, map[string]bool{"./testdata/server": true}, w.ignoreItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWatcherWatchChange(t *testing.T) {
|
func TestWatcherWatchChange(t *testing.T) {
|
||||||
|
@ -83,9 +86,9 @@ func TestWatcherWatchChange(t *testing.T) {
|
||||||
os.Chtimes(mainfile, time.Now(), time.Now())
|
os.Chtimes(mainfile, time.Now(), time.Now())
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case event := <-w.Events:
|
case event := <-w.Events():
|
||||||
assert.Equal(t, mainfile, event)
|
assert.Equal(t, mainfile, event)
|
||||||
case err := <-w.Errors:
|
case err := <-w.Errors():
|
||||||
assert.Nil(t, err, "wacher event error")
|
assert.Nil(t, err, "wacher event error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue