mirror of https://github.com/maxcnunes/gaper.git
Compare commits
No commits in common. "master" and "v1.0.0" have entirely different histories.
|
@ -1,37 +0,0 @@
|
||||||
name: goreleaser
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
# run only against tags
|
|
||||||
tags:
|
|
||||||
- '*'
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
goreleaser:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
-
|
|
||||||
name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
-
|
|
||||||
name: Fetch all tags
|
|
||||||
run: git fetch --force --tags
|
|
||||||
-
|
|
||||||
name: Set up Go
|
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
|
||||||
go-version: 1.19
|
|
||||||
-
|
|
||||||
name: Run GoReleaser
|
|
||||||
uses: goreleaser/goreleaser-action@v2
|
|
||||||
with:
|
|
||||||
distribution: goreleaser
|
|
||||||
version: latest
|
|
||||||
args: release --rm-dist
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
@ -1,39 +0,0 @@
|
||||||
name: dev-workflow
|
|
||||||
|
|
||||||
on:
|
|
||||||
- push
|
|
||||||
jobs:
|
|
||||||
run:
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os:
|
|
||||||
- ubuntu-latest
|
|
||||||
# - macos-latest
|
|
||||||
# - windows-latest
|
|
||||||
go:
|
|
||||||
- '1.19'
|
|
||||||
# - '1.18'
|
|
||||||
# - '1.17'
|
|
||||||
# - '1.16'
|
|
||||||
# - '1.15'
|
|
||||||
env:
|
|
||||||
OS: ${{ matrix.os }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@master
|
|
||||||
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: ${{ matrix.go }}
|
|
||||||
|
|
||||||
- name: golangci-lint
|
|
||||||
uses: golangci/golangci-lint-action@v3
|
|
||||||
with:
|
|
||||||
version: v1.48
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: make test
|
|
||||||
|
|
||||||
- name: Upload coverage to Codecov
|
|
||||||
uses: codecov/codecov-action@v3
|
|
|
@ -7,4 +7,3 @@ coverage.out
|
||||||
.DS_Store
|
.DS_Store
|
||||||
testdata/server/server
|
testdata/server/server
|
||||||
dist
|
dist
|
||||||
test-srv
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
# - 1.7.x
|
||||||
|
# - 1.8.x
|
||||||
|
# - 1.9.x
|
||||||
|
- 1.10.x
|
||||||
|
# - master
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- go version
|
||||||
|
- make setup
|
||||||
|
- make lint
|
||||||
|
|
||||||
|
script:
|
||||||
|
- make test
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
email: false
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
- provider: script
|
||||||
|
skip_cleanup: true
|
||||||
|
script: curl -sL http://git.io/goreleaser | bash
|
||||||
|
on:
|
||||||
|
tags: true
|
|
@ -28,13 +28,7 @@ make setup
|
||||||
### Running gaper in development
|
### Running gaper in development
|
||||||
|
|
||||||
```
|
```
|
||||||
make build && \
|
make build && ./gaper --verbose --bin-name srv --build-path ./testdata/server
|
||||||
./gaper \
|
|
||||||
--verbose \
|
|
||||||
--bin-name srv \
|
|
||||||
--build-path ./testdata/server \
|
|
||||||
--build-args="-ldflags=\"-X 'main.Version=v1.0.0'\"" \
|
|
||||||
--extensions "go,txt"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running lint
|
### Running lint
|
||||||
|
@ -55,6 +49,3 @@ A single test:
|
||||||
go test -run TestSimplePost ./...
|
go test -run TestSimplePost ./...
|
||||||
```
|
```
|
||||||
|
|
||||||
### Release
|
|
||||||
|
|
||||||
The release runs automatically with a Github action on pushed git tags.
|
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||||
|
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/davecgh/go-spew"
|
||||||
|
packages = ["spew"]
|
||||||
|
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||||
|
version = "v1.1.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/fatih/color"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
|
||||||
|
version = "v1.7.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/mattn/go-colorable"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||||
|
version = "v0.0.9"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/mattn/go-isatty"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||||
|
version = "v0.0.3"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/mattn/go-shellwords"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "02e3cf038dcea8290e44424da473dd12be796a8a"
|
||||||
|
version = "v1.0.3"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/mattn/go-zglob"
|
||||||
|
packages = [
|
||||||
|
".",
|
||||||
|
"fastwalk"
|
||||||
|
]
|
||||||
|
revision = "49693fbb3fe3c3a75fc4e4d6fb1d7cedcbdeb385"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/pmezard/go-difflib"
|
||||||
|
packages = ["difflib"]
|
||||||
|
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||||
|
version = "v1.0.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/stretchr/testify"
|
||||||
|
packages = ["assert"]
|
||||||
|
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
||||||
|
version = "v1.2.2"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/urfave/cli"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1"
|
||||||
|
version = "v1.20.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "golang.org/x/sys"
|
||||||
|
packages = ["unix"]
|
||||||
|
revision = "6c888cc515d3ed83fc103cf1d84468aad274b0a7"
|
||||||
|
|
||||||
|
[solve-meta]
|
||||||
|
analyzer-name = "dep"
|
||||||
|
analyzer-version = 1
|
||||||
|
inputs-digest = "501c303fff1c8cdb5806d0ebb0c92b671095cbc2049f5b8d2286df401f5efce5"
|
||||||
|
solver-name = "gps-cdcl"
|
||||||
|
solver-version = 1
|
|
@ -0,0 +1,23 @@
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/fatih/color"
|
||||||
|
version = "1.7.0"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/mattn/go-shellwords"
|
||||||
|
version = "1.0.3"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/urfave/cli"
|
||||||
|
version = "1.20.0"
|
||||||
|
|
||||||
|
[prune]
|
||||||
|
go-tests = true
|
||||||
|
unused-packages = true
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/mattn/go-zglob"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/stretchr/testify"
|
||||||
|
version = "1.2.2"
|
27
Makefile
27
Makefile
|
@ -1,16 +1,33 @@
|
||||||
OS := $(shell uname -s)
|
OS := $(shell uname -s)
|
||||||
TEST_PACKAGES := $(shell go list ./... | grep -v cmd)
|
TEST_PACKAGES := $(shell go list ./...)
|
||||||
COVER_PACKAGES := $(shell go list ./... | grep -v cmd | paste -sd "," -)
|
COVER_PACKAGES := $(shell go list ./... | paste -sd "," -)
|
||||||
LINTER := $(shell command -v gometalinter 2> /dev/null)
|
LINTER := $(shell command -v gometalinter 2> /dev/null)
|
||||||
|
|
||||||
|
.PHONY: setup
|
||||||
|
|
||||||
|
setup:
|
||||||
|
ifeq ($(OS), Darwin)
|
||||||
|
brew install dep
|
||||||
|
else
|
||||||
|
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
|
||||||
|
endif
|
||||||
|
dep ensure -vendor-only
|
||||||
|
ifndef LINTER
|
||||||
|
@echo "Installing linter"
|
||||||
|
@go get -u github.com/alecthomas/gometalinter
|
||||||
|
@gometalinter --install
|
||||||
|
endif
|
||||||
|
|
||||||
build:
|
build:
|
||||||
@go build -o ./gaper cmd/gaper/main.go
|
@go build -o ./gaper cmd/gaper/main.go
|
||||||
|
|
||||||
## lint: Validate golang code
|
## lint: Validate golang code
|
||||||
# Install it following this doc https://golangci-lint.run/usage/install/#local-installation,
|
|
||||||
# please use the same version from .github/workflows/workflow.yml.
|
|
||||||
lint:
|
lint:
|
||||||
@golangci-lint run
|
@gometalinter \
|
||||||
|
--deadline=120s \
|
||||||
|
--line-length=120 \
|
||||||
|
--enable-all \
|
||||||
|
--vendor ./...
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@go test -p=1 -coverpkg $(COVER_PACKAGES) \
|
@go test -p=1 -coverpkg $(COVER_PACKAGES) \
|
||||||
|
|
54
README.md
54
README.md
|
@ -18,23 +18,15 @@
|
||||||
[](https://goreportcard.com/report/github.com/maxcnunes/gaper)
|
[](https://goreportcard.com/report/github.com/maxcnunes/gaper)
|
||||||
[](https://github.com/goreleaser)
|
[](https://github.com/goreleaser)
|
||||||
|
|
||||||
## Changelog
|
|
||||||
|
|
||||||
See [Releases](https://github.com/maxcnunes/gaper/releases) for detailed history changes.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Using go tooling:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
go get -u github.com/maxcnunes/gaper/cmd/gaper
|
go get -u github.com/maxcnunes/gaper/cmd/gaper
|
||||||
```
|
```
|
||||||
|
|
||||||
Or, downloading the binary instead (example for version 1.1.0, make sure you are using the latest version though):
|
## Changelog
|
||||||
|
|
||||||
```
|
See [Releases](https://github.com/maxcnunes/gaper/releases) for detailed history changes.
|
||||||
curl -SL https://github.com/maxcnunes/gaper/releases/download/v1.1.0/gaper_1.1.0_linux_amd64.tar.gz | tar -xvzf - -C "${GOPATH}/bin"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
@ -57,11 +49,11 @@ GLOBAL OPTIONS:
|
||||||
--build-args value arguments used on building the program
|
--build-args value arguments used on building the program
|
||||||
--program-args value arguments used on executing the program
|
--program-args value arguments used on executing the program
|
||||||
--verbose turns on the verbose messages from gaper
|
--verbose turns on the verbose messages from gaper
|
||||||
--disable-default-ignore turns off default ignore for hidden files and folders, "*_test.go" files, and vendor folder
|
|
||||||
--watch value, -w value list of folders or files to watch for changes
|
--watch value, -w value list of folders or files to watch for changes
|
||||||
--ignore value, -i value list of folders or files to ignore for changes
|
--ignore value, -i value list of folders or files to ignore for changes
|
||||||
|
(always ignores all hidden files and directories)
|
||||||
--poll-interval value, -p value how often in milliseconds to poll watched files for changes (default: 500)
|
--poll-interval value, -p value how often in milliseconds to poll watched files for changes (default: 500)
|
||||||
--extensions value, -e value extensions to watch for changes (default: "go")
|
--extensions value, -e value a comma-delimited list of file extensions to watch for changes (default: "go")
|
||||||
--no-restart-on value, -n value don't automatically restart the supervised program if it ends:
|
--no-restart-on value, -n value don't automatically restart the supervised program if it ends:
|
||||||
if "error", an exit code of 0 will still restart.
|
if "error", an exit code of 0 will still restart.
|
||||||
if "exit", no restart regardless of exit code.
|
if "exit", no restart regardless of exit code.
|
||||||
|
@ -70,46 +62,12 @@ GLOBAL OPTIONS:
|
||||||
--version, -v print the version
|
--version, -v print the version
|
||||||
```
|
```
|
||||||
|
|
||||||
### Watch and Ignore paths
|
|
||||||
|
|
||||||
For those options Gaper supports static paths (e.g. `build/`, `seed.go`) or glob paths (e.g. `migrations/**/up.go`, `*_test.go`).
|
|
||||||
|
|
||||||
On using a path to a directory please add a `/` at the end (e.g. `build/`) to make sure Gaper won't include other matches that starts with that same value (e.g. `build/`, `build_settings.go`).
|
|
||||||
|
|
||||||
### Default ignore settings
|
|
||||||
|
|
||||||
Since in most projects there is no need to watch changes for:
|
|
||||||
|
|
||||||
* hidden files and folders
|
|
||||||
* test files (`*_test.go`)
|
|
||||||
* vendor folder
|
|
||||||
|
|
||||||
Gaper by default ignores those cases already. Although, if you need Gaper to watch those files anyway it is possible to disable this setting with `--disable-default-ignore` argument.
|
|
||||||
|
|
||||||
### Watch method
|
|
||||||
|
|
||||||
Currently Gaper uses polling to watch file changes. We have plans to [support fs events](https://github.com/maxcnunes/gaper/issues/12) though in a near future.
|
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
Using all defaults provided by Gaper:
|
Ignore watch over all test files:
|
||||||
|
|
||||||
```
|
```
|
||||||
gaper
|
--ignore './**/*_test.go'
|
||||||
```
|
|
||||||
|
|
||||||
Example providing a few custom configurations:
|
|
||||||
|
|
||||||
```
|
|
||||||
gaper \
|
|
||||||
--bin-name build/api-dev \
|
|
||||||
--build-path cmd/server \
|
|
||||||
--build-args "-ldflags=\"-X 'main.Version=dev'" \
|
|
||||||
-w 'public/**' -w '*.go' \
|
|
||||||
-e js -e css -e html \
|
|
||||||
--ignore './**/*_mock.go' \
|
|
||||||
--program-args "-arg1 ok -arg2=nope" \
|
|
||||||
--watch .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
version: "{build}"
|
||||||
|
|
||||||
|
# Source Config
|
||||||
|
|
||||||
|
clone_folder: c:\gopath\src\github.com\maxcnunes\gaper
|
||||||
|
|
||||||
|
# Build host
|
||||||
|
|
||||||
|
environment:
|
||||||
|
GOPATH: c:\gopath
|
||||||
|
GOBIN: c:\gopath\bin
|
||||||
|
|
||||||
|
init:
|
||||||
|
- git config --global core.autocrlf input
|
||||||
|
|
||||||
|
# Build
|
||||||
|
|
||||||
|
install:
|
||||||
|
- set Path=c:\go\bin;c:\gopath\bin;%Path%
|
||||||
|
- go version
|
||||||
|
- go env
|
||||||
|
- go get -u github.com/golang/dep/cmd/dep
|
||||||
|
- choco install make
|
||||||
|
- make setup
|
||||||
|
|
||||||
|
build: false
|
||||||
|
deploy: false
|
||||||
|
|
||||||
|
test_script:
|
||||||
|
- go build github.com/maxcnunes/gaper/cmd/gaper
|
||||||
|
- make test
|
|
@ -17,6 +17,7 @@ type Builder interface {
|
||||||
type builder struct {
|
type builder struct {
|
||||||
dir string
|
dir string
|
||||||
binary string
|
binary string
|
||||||
|
errors string
|
||||||
wd string
|
wd string
|
||||||
buildArgs []string
|
buildArgs []string
|
||||||
}
|
}
|
||||||
|
@ -55,7 +56,7 @@ func (b *builder) Build() error {
|
||||||
|
|
||||||
output, err := command.CombinedOutput()
|
output, err := command.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("build failed with %v\n%s", err, output)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !command.ProcessState.Success() {
|
if !command.ProcessState.Success() {
|
||||||
|
|
|
@ -41,10 +41,7 @@ func TestBuilderFailureBuild(t *testing.T) {
|
||||||
b := NewBuilder(dir, bin, wd, bArgs)
|
b := NewBuilder(dir, bin, wd, bArgs)
|
||||||
err = b.Build()
|
err = b.Build()
|
||||||
assert.NotNil(t, err, "build error")
|
assert.NotNil(t, err, "build error")
|
||||||
assert.Equal(t, err.Error(), "build failed with exit status 2\n"+
|
assert.Equal(t, err.Error(), "exit status 2")
|
||||||
"# github.com/maxcnunes/gaper/testdata/build-failure\n"+
|
|
||||||
"./main.go:4:6: func main must have no arguments and no return values\n"+
|
|
||||||
"./main.go:5:1: missing return\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuilderDefaultBinName(t *testing.T) {
|
func TestBuilderDefaultBinName(t *testing.T) {
|
||||||
|
|
|
@ -4,35 +4,30 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/maxcnunes/gaper"
|
"github.com/maxcnunes/gaper"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
// build info
|
// build info
|
||||||
var (
|
var (
|
||||||
// Version is hardcoded because when installing it through "go get/install"
|
version = "dev"
|
||||||
// the build tags are not available to override it.
|
|
||||||
// Update it after every release.
|
|
||||||
version = "1.0.3-dev"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var logger = gaper.NewLogger("gaper")
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
logger := gaper.Logger()
|
|
||||||
loggerVerbose := false
|
|
||||||
|
|
||||||
parseArgs := func(c *cli.Context) *gaper.Config {
|
parseArgs := func(c *cli.Context) *gaper.Config {
|
||||||
loggerVerbose = c.Bool("verbose")
|
|
||||||
|
|
||||||
return &gaper.Config{
|
return &gaper.Config{
|
||||||
BinName: c.String("bin-name"),
|
BinName: c.String("bin-name"),
|
||||||
BuildPath: c.String("build-path"),
|
BuildPath: c.String("build-path"),
|
||||||
BuildArgsMerged: c.String("build-args"),
|
BuildArgsMerged: c.String("build-args"),
|
||||||
ProgramArgsMerged: c.String("program-args"),
|
ProgramArgsMerged: c.String("program-args"),
|
||||||
DisableDefaultIgnore: c.Bool("disable-default-ignore"),
|
Verbose: c.Bool("verbose"),
|
||||||
WatchItems: c.StringSlice("watch"),
|
WatchItems: c.StringSlice("watch"),
|
||||||
IgnoreItems: c.StringSlice("ignore"),
|
IgnoreItems: c.StringSlice("ignore"),
|
||||||
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,61 +36,62 @@ func main() {
|
||||||
app.Usage = "Used to build and restart a Go project when it crashes or some watched file changes"
|
app.Usage = "Used to build and restart a Go project when it crashes or some watched file changes"
|
||||||
app.Version = version
|
app.Version = version
|
||||||
|
|
||||||
app.Action = func(c *cli.Context) error {
|
app.Action = func(c *cli.Context) {
|
||||||
args := parseArgs(c)
|
args := parseArgs(c)
|
||||||
chOSSiginal := make(chan os.Signal, 2)
|
if err := gaper.Run(args); err != nil {
|
||||||
logger.Verbose(loggerVerbose)
|
logger.Error(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return gaper.Run(args, chOSSiginal)
|
exts := make(cli.StringSlice, len(gaper.DefaultExtensions))
|
||||||
|
for i := range gaper.DefaultExtensions {
|
||||||
|
exts[i] = gaper.DefaultExtensions[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
// supported arguments
|
// supported arguments
|
||||||
app.Flags = []cli.Flag{
|
app.Flags = []cli.Flag{
|
||||||
&cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "bin-name",
|
Name: "bin-name",
|
||||||
Usage: "name for the binary built by gaper for the executed program (default current directory name)",
|
Usage: "name for the binary built by gaper for the executed program (default current directory name)",
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "build-path",
|
Name: "build-path",
|
||||||
Value: gaper.DefaultBuildPath,
|
Value: gaper.DefaultBuildPath,
|
||||||
Usage: "path to the program source code",
|
Usage: "path to the program source code",
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "build-args",
|
Name: "build-args",
|
||||||
Usage: "arguments used on building the program",
|
Usage: "arguments used on building the program",
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "program-args",
|
Name: "program-args",
|
||||||
Usage: "arguments used on executing the program",
|
Usage: "arguments used on executing the program",
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "verbose",
|
Name: "verbose",
|
||||||
Usage: "turns on the verbose messages from gaper",
|
Usage: "turns on the verbose messages from gaper",
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
cli.StringSliceFlag{
|
||||||
Name: "disable-default-ignore",
|
|
||||||
Usage: "turns off default ignore for hidden files and folders, \"*_test.go\" files, and vendor folder",
|
|
||||||
},
|
|
||||||
&cli.StringSliceFlag{
|
|
||||||
Name: "watch, w",
|
Name: "watch, w",
|
||||||
Usage: "list of folders or files to watch for changes",
|
Usage: "list of folders or files to watch for changes",
|
||||||
},
|
},
|
||||||
&cli.StringSliceFlag{
|
cli.StringSliceFlag{
|
||||||
Name: "ignore, i",
|
Name: "ignore, i",
|
||||||
Usage: "list of folders or files to ignore for changes\n" +
|
Usage: "list of folders or files to ignore for changes\n" +
|
||||||
"\t\t(always ignores all hidden files and directories)",
|
"\t\t(always ignores all hidden files and directories)",
|
||||||
},
|
},
|
||||||
&cli.IntFlag{
|
cli.IntFlag{
|
||||||
Name: "poll-interval, p",
|
Name: "poll-interval, p",
|
||||||
Value: gaper.DefaultPoolInterval,
|
Value: gaper.DefaultPoolInterval,
|
||||||
Usage: "how often in milliseconds to poll watched files for changes",
|
Usage: "how often in milliseconds to poll watched files for changes",
|
||||||
},
|
},
|
||||||
&cli.StringSliceFlag{
|
cli.StringSliceFlag{
|
||||||
Name: "extensions, e",
|
Name: "extensions, e",
|
||||||
Value: cli.NewStringSlice(gaper.DefaultExtensions...),
|
Value: &exts,
|
||||||
Usage: "a comma-delimited list of file extensions to watch for changes",
|
Usage: "a comma-delimited list of file extensions to watch for changes",
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "no-restart-on, n",
|
Name: "no-restart-on, n",
|
||||||
Usage: "don't automatically restart the supervised program if it ends:\n" +
|
Usage: "don't automatically restart the supervised program if it ends:\n" +
|
||||||
"\t\tif \"error\", an exit code of 0 will still restart.\n" +
|
"\t\tif \"error\", an exit code of 0 will still restart.\n" +
|
||||||
|
|
157
gaper.go
157
gaper.go
|
@ -5,9 +5,9 @@ package gaper
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -23,12 +23,7 @@ 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 logger = NewLogger("gaper")
|
||||||
var (
|
|
||||||
NoRestartOnError = "error"
|
|
||||||
NoRestartOnSuccess = "success"
|
|
||||||
NoRestartOnExit = "exit"
|
|
||||||
)
|
|
||||||
|
|
||||||
// exit statuses
|
// exit statuses
|
||||||
var exitStatusSuccess = 0
|
var exitStatusSuccess = 0
|
||||||
|
@ -47,74 +42,75 @@ type Config struct {
|
||||||
PollInterval int
|
PollInterval int
|
||||||
Extensions []string
|
Extensions []string
|
||||||
NoRestartOn string
|
NoRestartOn string
|
||||||
DisableDefaultIgnore bool
|
Verbose bool
|
||||||
WorkingDirectory string
|
ExitOnSIGINT bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run starts the whole gaper process watching for file changes or exit codes
|
// Run in the gaper high level API
|
||||||
|
// 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, chOSSiginal chan os.Signal) error {
|
func Run(cfg *Config) error { // nolint: gocyclo
|
||||||
|
var err error
|
||||||
|
logger.Verbose(cfg.Verbose)
|
||||||
logger.Debug("Starting gaper")
|
logger.Debug("Starting gaper")
|
||||||
|
|
||||||
if err := setupConfig(cfg); err != nil {
|
if len(cfg.BuildPath) == 0 {
|
||||||
|
cfg.BuildPath = DefaultBuildPath
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.BuildArgs, err = parseInnerArgs(cfg.BuildArgs, cfg.BuildArgsMerged)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debugf("Config: %+v", cfg)
|
cfg.ProgramArgs, err = parseInnerArgs(cfg.ProgramArgs, cfg.ProgramArgsMerged)
|
||||||
|
|
||||||
wCfg := WatcherConfig{
|
|
||||||
DefaultIgnore: !cfg.DisableDefaultIgnore,
|
|
||||||
PollInterval: cfg.PollInterval,
|
|
||||||
WatchItems: cfg.WatchItems,
|
|
||||||
IgnoreItems: cfg.IgnoreItems,
|
|
||||||
Extensions: cfg.Extensions,
|
|
||||||
}
|
|
||||||
|
|
||||||
builder := NewBuilder(cfg.BuildPath, cfg.BinName, cfg.WorkingDirectory, cfg.BuildArgs)
|
|
||||||
runner := NewRunner(os.Stdout, os.Stderr, filepath.Join(cfg.WorkingDirectory, builder.Binary()), cfg.ProgramArgs)
|
|
||||||
watcher, err := NewWatcher(wCfg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("watcher error: %v", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return run(cfg, chOSSiginal, builder, runner, watcher)
|
wd, err := os.Getwd()
|
||||||
}
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// nolint: gocyclo
|
if len(cfg.WatchItems) == 0 {
|
||||||
func run(cfg *Config, chOSSiginal chan os.Signal, builder Builder, runner Runner, watcher Watcher) error {
|
cfg.WatchItems = append(cfg.WatchItems, cfg.BuildPath)
|
||||||
if err := builder.Build(); err != nil {
|
}
|
||||||
|
|
||||||
|
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)
|
return fmt.Errorf("build error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// listen for OS signals
|
shutdown(runner, cfg.ExitOnSIGINT)
|
||||||
signal.Notify(chOSSiginal, os.Interrupt, syscall.SIGTERM)
|
|
||||||
|
|
||||||
if _, err := runner.Run(); err != nil {
|
if _, err = runner.Run(); err != nil {
|
||||||
return fmt.Errorf("run error: %v", err)
|
return fmt.Errorf("run error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watcher, err := NewWatcher(cfg.PollInterval, cfg.WatchItems, cfg.IgnoreItems, cfg.Extensions)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("watcher 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)
|
||||||
if changeRestart {
|
changeRestart = true
|
||||||
logger.Debug("Skip restart due to existing on going restart")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
changeRestart = runner.IsRunning()
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
// ignore exit by change
|
// ignore exit by change
|
||||||
if changeRestart {
|
if changeRestart {
|
||||||
|
@ -125,14 +121,6 @@ func run(cfg *Config, chOSSiginal chan os.Signal, builder Builder, runner Runner
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
@ -150,73 +138,60 @@ func restart(builder Builder, runner Runner) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := builder.Build(); err != nil {
|
if err := builder.Build(); err != nil {
|
||||||
logger.Error("Error building binary during a restart:", err)
|
return fmt.Errorf("build error: %v", err)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := runner.Run(); err != nil {
|
if _, err := runner.Run(); err != nil {
|
||||||
logger.Error("Error starting process during a restart:", err)
|
return fmt.Errorf("run error: %v", err)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleProgramExit(builder Builder, runner Runner, err error, noRestartOn string) error {
|
func handleProgramExit(builder Builder, runner Runner, err error, noRestartOn string) error {
|
||||||
exitStatus := runner.ExitStatus(err)
|
var exitStatus int
|
||||||
|
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 == NoRestartOnError && exitStatus == exitStatusError {
|
if noRestartOn == "error" && 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 == NoRestartOnSuccess && exitStatus == exitStatusSuccess {
|
if noRestartOn == "success" && exitStatus == exitStatusSuccess {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if "exit", no restart regardless of exit code.
|
// if "exit", no restart regardless of exit code.
|
||||||
if noRestartOn == NoRestartOnExit {
|
if noRestartOn == "exit" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return restart(builder, runner)
|
return restart(builder, runner)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupConfig(cfg *Config) error {
|
func shutdown(runner Runner, exitOnSIGINT bool) {
|
||||||
var err error
|
c := make(chan os.Signal, 2)
|
||||||
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
s := <-c
|
||||||
|
logger.Debug("Got signal: ", s)
|
||||||
|
|
||||||
if len(cfg.BuildPath) == 0 {
|
if err := runner.Kill(); err != nil {
|
||||||
cfg.BuildPath = DefaultBuildPath
|
logger.Error("Error killing: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.BuildArgs, err = parseInnerArgs(cfg.BuildArgs, cfg.BuildArgsMerged)
|
if exitOnSIGINT {
|
||||||
if err != nil {
|
os.Exit(0)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
var extensions []string
|
|
||||||
for i := range cfg.Extensions {
|
|
||||||
values := strings.Split(cfg.Extensions[i], ",")
|
|
||||||
extensions = append(extensions, values...)
|
|
||||||
}
|
|
||||||
cfg.Extensions = extensions
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseInnerArgs(args []string, argsm string) ([]string, error) {
|
func parseInnerArgs(args []string, argsm string) ([]string, error) {
|
||||||
|
|
285
gaper_test.go
285
gaper_test.go
|
@ -1,290 +1,9 @@
|
||||||
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 TestGaperRunStopOnSGINT(t *testing.T) {
|
func TestGaper(t *testing.T) {
|
||||||
args := &Config{
|
// TODO: add test to gaper high level API
|
||||||
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 TestGaperBuildError(t *testing.T) {
|
|
||||||
mockBuilder := new(testdata.MockBuilder)
|
|
||||||
mockBuilder.On("Build").Return(errors.New("build-error"))
|
|
||||||
mockRunner := new(testdata.MockRunner)
|
|
||||||
mockWatcher := new(testdata.MockWacther)
|
|
||||||
|
|
||||||
cfg := &Config{}
|
|
||||||
|
|
||||||
chOSSiginal := make(chan os.Signal, 2)
|
|
||||||
err := run(cfg, chOSSiginal, mockBuilder, mockRunner, mockWatcher)
|
|
||||||
assert.NotNil(t, err, "build error")
|
|
||||||
assert.Equal(t, "build error: build-error", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGaperRunError(t *testing.T) {
|
|
||||||
mockBuilder := new(testdata.MockBuilder)
|
|
||||||
mockBuilder.On("Build").Return(nil)
|
|
||||||
mockRunner := new(testdata.MockRunner)
|
|
||||||
mockRunner.On("Run").Return(nil, errors.New("runner-error"))
|
|
||||||
mockWatcher := new(testdata.MockWacther)
|
|
||||||
|
|
||||||
cfg := &Config{}
|
|
||||||
|
|
||||||
chOSSiginal := make(chan os.Signal, 2)
|
|
||||||
err := run(cfg, chOSSiginal, mockBuilder, mockRunner, mockWatcher)
|
|
||||||
assert.NotNil(t, err, "runner error")
|
|
||||||
assert.Equal(t, "run error: runner-error", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
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.Nil(t, err, "restart 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.Nil(t, err, "restart error")
|
|
||||||
mockBuilder.AssertExpectations(t)
|
|
||||||
mockRunner.AssertExpectations(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGaperFailBadBuildArgsMerged(t *testing.T) { // nolint: dupl
|
|
||||||
args := &Config{
|
|
||||||
BuildArgsMerged: "foo '",
|
|
||||||
}
|
|
||||||
chOSSiginal := make(chan os.Signal, 2)
|
|
||||||
|
|
||||||
err := Run(args, chOSSiginal)
|
|
||||||
assert.NotNil(t, err, "run error")
|
|
||||||
assert.Equal(t, "invalid command line string", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGaperFailBadProgramArgsMerged(t *testing.T) { // nolint: dupl
|
|
||||||
args := &Config{
|
|
||||||
ProgramArgsMerged: "foo '",
|
|
||||||
}
|
|
||||||
chOSSiginal := make(chan os.Signal, 2)
|
|
||||||
|
|
||||||
err := Run(args, chOSSiginal)
|
|
||||||
assert.NotNil(t, err, "run error")
|
|
||||||
assert.Equal(t, "invalid command line string", err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
16
go.mod
16
go.mod
|
@ -1,16 +0,0 @@
|
||||||
module github.com/maxcnunes/gaper
|
|
||||||
|
|
||||||
go 1.13
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/fatih/color v1.7.0
|
|
||||||
github.com/mattn/go-colorable v0.0.9 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.3 // indirect
|
|
||||||
github.com/mattn/go-shellwords v1.0.3
|
|
||||||
github.com/mattn/go-zglob v0.0.0-20180607075734-49693fbb3fe3
|
|
||||||
github.com/stretchr/objx v0.1.1 // indirect
|
|
||||||
github.com/stretchr/testify v1.4.0
|
|
||||||
github.com/urfave/cli/v2 v2.11.1
|
|
||||||
golang.org/x/sys v0.0.0-20180616030259-6c888cc515d3 // indirect
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
|
||||||
)
|
|
38
go.sum
38
go.sum
|
@ -1,38 +0,0 @@
|
||||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
|
||||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
|
||||||
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
|
||||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
|
||||||
github.com/mattn/go-shellwords v1.0.3 h1:K/VxK7SZ+cvuPgFSLKi5QPI9Vr/ipOf4C1gN+ntueUk=
|
|
||||||
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
|
||||||
github.com/mattn/go-zglob v0.0.0-20180607075734-49693fbb3fe3 h1:GWnsQiFbiQ7lREZbKkiJC6xxbymvny8GKtpdkPxjB6o=
|
|
||||||
github.com/mattn/go-zglob v0.0.0-20180607075734-49693fbb3fe3/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
||||||
github.com/urfave/cli/v2 v2.11.1 h1:UKK6SP7fV3eKOefbS87iT9YHefv7iB/53ih6e+GNAsE=
|
|
||||||
github.com/urfave/cli/v2 v2.11.1/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo=
|
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
|
||||||
golang.org/x/sys v0.0.0-20180616030259-6c888cc515d3 h1:FCfAlbS73+IQQJktaKGHldMdL2bGDVpm+OrCEbVz1f4=
|
|
||||||
golang.org/x/sys v0.0.0-20180616030259-6c888cc515d3/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
|
@ -1,39 +0,0 @@
|
||||||
package gaper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLoggerDefault(t *testing.T) {
|
|
||||||
l := newLogger("gaper-test")
|
|
||||||
assert.Equal(t, l.verbose, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoggerEnableVerbose(t *testing.T) {
|
|
||||||
l := newLogger("gaper-test")
|
|
||||||
l.Verbose(true)
|
|
||||||
assert.Equal(t, l.verbose, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoggerRunAllLogsWithoutVerbose(t *testing.T) {
|
|
||||||
// no asserts, just checking it doesn't crash
|
|
||||||
l := newLogger("gaper-test")
|
|
||||||
l.Debug("debug")
|
|
||||||
l.Debugf("%s", "debug")
|
|
||||||
l.Info("info")
|
|
||||||
l.Error("error")
|
|
||||||
l.Errorf("%s", "error")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoggerRunAllLogsWithVerbose(t *testing.T) {
|
|
||||||
// no asserts, just checking it doesn't crash
|
|
||||||
l := newLogger("gaper-test")
|
|
||||||
l.Verbose(true)
|
|
||||||
l.Debug("debug")
|
|
||||||
l.Debugf("%s", "debug")
|
|
||||||
l.Info("info")
|
|
||||||
l.Error("error")
|
|
||||||
l.Errorf("%s", "error")
|
|
||||||
}
|
|
|
@ -7,26 +7,18 @@ import (
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
// logger use by the whole package
|
// Logger used by gaper
|
||||||
var logger = newLogger("gaper")
|
type Logger struct {
|
||||||
|
|
||||||
// Logger give access to external packages to use gaper logger
|
|
||||||
func Logger() *LoggerEntity {
|
|
||||||
return logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoggerEntity used by gaper
|
|
||||||
type LoggerEntity struct {
|
|
||||||
verbose bool
|
verbose bool
|
||||||
logDebug *log.Logger
|
logDebug *log.Logger
|
||||||
logInfo *log.Logger
|
logInfo *log.Logger
|
||||||
logError *log.Logger
|
logError *log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// newLogger creates a new logger
|
// NewLogger creates a new logger
|
||||||
func newLogger(prefix string) *LoggerEntity {
|
func NewLogger(prefix string) *Logger {
|
||||||
prefix = "[" + prefix + "] "
|
prefix = "[" + prefix + "] "
|
||||||
return &LoggerEntity{
|
return &Logger{
|
||||||
verbose: false,
|
verbose: false,
|
||||||
logDebug: log.New(os.Stdout, prefix, 0),
|
logDebug: log.New(os.Stdout, prefix, 0),
|
||||||
logInfo: log.New(os.Stdout, color.CyanString(prefix), 0),
|
logInfo: log.New(os.Stdout, color.CyanString(prefix), 0),
|
||||||
|
@ -35,35 +27,35 @@ func newLogger(prefix string) *LoggerEntity {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verbose toggle this logger verbosity
|
// Verbose toggle this logger verbosity
|
||||||
func (l *LoggerEntity) Verbose(verbose bool) {
|
func (l *Logger) Verbose(verbose bool) {
|
||||||
l.verbose = verbose
|
l.verbose = verbose
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug logs a debug message
|
// Debug logs a debug message
|
||||||
func (l *LoggerEntity) Debug(v ...interface{}) {
|
func (l *Logger) Debug(v ...interface{}) {
|
||||||
if l.verbose {
|
if l.verbose {
|
||||||
l.logDebug.Println(v...)
|
l.logDebug.Println(v...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debugf logs a debug message with format
|
// Debugf logs a debug message with format
|
||||||
func (l *LoggerEntity) Debugf(format string, v ...interface{}) {
|
func (l *Logger) Debugf(format string, v ...interface{}) {
|
||||||
if l.verbose {
|
if l.verbose {
|
||||||
l.logDebug.Printf(format, v...)
|
l.logDebug.Printf(format, v...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info logs a info message
|
// Info logs a info message
|
||||||
func (l *LoggerEntity) Info(v ...interface{}) {
|
func (l *Logger) Info(v ...interface{}) {
|
||||||
l.logInfo.Println(v...)
|
l.logInfo.Println(v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error logs an error message
|
// Error logs an error message
|
||||||
func (l *LoggerEntity) Error(v ...interface{}) {
|
func (l *Logger) Error(v ...interface{}) {
|
||||||
l.logError.Println(v...)
|
l.logError.Println(v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errorf logs and error message with format
|
// Errorf logs and error message with format
|
||||||
func (l *LoggerEntity) Errorf(format string, v ...interface{}) {
|
func (l *Logger) Errorf(format string, v ...interface{}) {
|
||||||
l.logError.Printf(format, v...)
|
l.logError.Printf(format, v...)
|
||||||
}
|
}
|
20
runner.go
20
runner.go
|
@ -7,7 +7,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,8 +22,6 @@ type Runner interface {
|
||||||
Kill() error
|
Kill() error
|
||||||
Errors() chan error
|
Errors() chan error
|
||||||
Exited() bool
|
Exited() bool
|
||||||
IsRunning() bool
|
|
||||||
ExitStatus(err error) int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type runner struct {
|
type runner struct {
|
||||||
|
@ -109,28 +106,11 @@ func (r *runner) Exited() bool {
|
||||||
return r.command != nil && r.command.ProcessState != nil && r.command.ProcessState.Exited()
|
return r.command != nil && r.command.ProcessState != nil && r.command.ProcessState.Exited()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsRunning returns if the process is running
|
|
||||||
func (r *runner) IsRunning() bool {
|
|
||||||
return r.command != nil && r.command.Process != nil && r.command.Process.Pid > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Errors get errors occurred during the build
|
// Errors get errors occurred during the build
|
||||||
func (r *runner) Errors() chan error {
|
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,9 +2,7 @@ package gaper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -50,32 +48,3 @@ 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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
package ignoretestname
|
|
|
@ -1,90 +0,0 @@
|
||||||
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()
|
|
||||||
cmdArg := args.Get(0)
|
|
||||||
if cmdArg == nil {
|
|
||||||
return nil, args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmdArg.(*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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsRunning ...
|
|
||||||
func (m *MockRunner) IsRunning() 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)
|
|
||||||
}
|
|
|
@ -1,3 +1,3 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
sleep 3
|
sleep 2
|
||||||
echo "Gaper Test Message"
|
echo "Gaper Test Message"
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
timeout 3 2>NUL
|
timeout 2 2>NUL
|
||||||
@echo Gaper Test Message
|
@echo Gaper Test Message
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
test
|
|
|
@ -7,8 +7,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version string
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) // nolint gas
|
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) // nolint gas
|
||||||
|
@ -18,6 +16,6 @@ func main() {
|
||||||
log.Fatal("Forced failure")
|
log.Fatal("Forced failure")
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Println("Starting server: Version", Version)
|
log.Println("Starting server")
|
||||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
}
|
}
|
||||||
|
|
145
watcher.go
145
watcher.go
|
@ -5,7 +5,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -13,67 +12,49 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Watcher is a interface for the watch process
|
// Watcher is a interface for the watch process
|
||||||
type Watcher interface {
|
type Watcher struct {
|
||||||
Watch()
|
|
||||||
Errors() chan error
|
|
||||||
Events() chan string
|
|
||||||
}
|
|
||||||
|
|
||||||
// watcher is a interface for the watch process
|
|
||||||
type watcher struct {
|
|
||||||
defaultIgnore bool
|
|
||||||
pollInterval int
|
|
||||||
watchItems map[string]bool
|
|
||||||
ignoreItems map[string]bool
|
|
||||||
allowedExtensions map[string]bool
|
|
||||||
events chan string
|
|
||||||
errors chan error
|
|
||||||
}
|
|
||||||
|
|
||||||
// WatcherConfig defines the settings available for the watcher
|
|
||||||
type WatcherConfig struct {
|
|
||||||
DefaultIgnore bool
|
|
||||||
PollInterval int
|
PollInterval int
|
||||||
WatchItems []string
|
WatchItems map[string]bool
|
||||||
IgnoreItems []string
|
IgnoreItems map[string]bool
|
||||||
Extensions []string
|
AllowedExtensions map[string]bool
|
||||||
|
Events chan string
|
||||||
|
Errors chan error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWatcher creates a new watcher
|
// NewWatcher creates a new watcher
|
||||||
func NewWatcher(cfg WatcherConfig) (Watcher, error) {
|
func NewWatcher(pollInterval int, watchItems []string, ignoreItems []string, extensions []string) (*Watcher, error) {
|
||||||
if cfg.PollInterval == 0 {
|
if pollInterval == 0 {
|
||||||
cfg.PollInterval = DefaultPoolInterval
|
pollInterval = DefaultPoolInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cfg.Extensions) == 0 {
|
if len(extensions) == 0 {
|
||||||
cfg.Extensions = DefaultExtensions
|
extensions = DefaultExtensions
|
||||||
}
|
}
|
||||||
|
|
||||||
allowedExts := make(map[string]bool)
|
allowedExts := make(map[string]bool)
|
||||||
for _, ext := range cfg.Extensions {
|
for _, ext := range extensions {
|
||||||
allowedExts["."+ext] = true
|
allowedExts["."+ext] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
watchPaths, err := resolvePaths(cfg.WatchItems, allowedExts)
|
watchPaths, err := resolvePaths(watchItems, allowedExts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ignorePaths, err := resolvePaths(cfg.IgnoreItems, allowedExts)
|
ignorePaths, err := resolvePaths(ignoreItems, allowedExts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
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),
|
||||||
defaultIgnore: cfg.DefaultIgnore,
|
PollInterval: pollInterval,
|
||||||
pollInterval: cfg.PollInterval,
|
WatchItems: watchPaths,
|
||||||
watchItems: watchPaths,
|
IgnoreItems: ignorePaths,
|
||||||
ignoreItems: ignorePaths,
|
AllowedExtensions: allowedExts,
|
||||||
allowedExtensions: allowedExts,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,57 +62,42 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events get events occurred during the watching
|
func (w *Watcher) scanChange(watchPath string) (string, error) {
|
||||||
// these events are emitted 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
|
||||||
|
|
||||||
err := filepath.Walk(watchPath, func(path string, info os.FileInfo, err error) error {
|
err := filepath.Walk(watchPath, func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
// always ignore hidden files and directories
|
||||||
// Ignore attempt to acess go temporary unmask
|
if dir := filepath.Base(path); dir[0] == '.' && dir != "." {
|
||||||
if strings.Contains(err.Error(), "-go-tmp-umask") {
|
return skipFile(info)
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("couldn't walk to path \"%s\": %v", path, err)
|
if _, ignored := w.IgnoreItems[path]; ignored {
|
||||||
}
|
|
||||||
|
|
||||||
if w.ignoreFile(path, info) {
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -146,38 +112,6 @@ func (w *watcher) scanChange(watchPath string) (string, error) {
|
||||||
return fileChanged, nil
|
return fileChanged, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *watcher) ignoreFile(path string, info os.FileInfo) bool {
|
|
||||||
// if a file has been deleted after gaper was watching it
|
|
||||||
// info will be nil in the other iterations
|
|
||||||
if info == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if preset ignore is enabled
|
|
||||||
if w.defaultIgnore {
|
|
||||||
// check for hidden files and directories
|
|
||||||
if name := info.Name(); name[0] == '.' && name != "." {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if it is a Go testing file
|
|
||||||
if strings.HasSuffix(path, "_test.go") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if it is the vendor folder
|
|
||||||
if info.IsDir() && info.Name() == "vendor" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ignored := w.ignoreItems[path]; ignored {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolvePaths(paths []string, extensions map[string]bool) (map[string]bool, error) {
|
func resolvePaths(paths []string, extensions map[string]bool) (map[string]bool, error) {
|
||||||
result := map[string]bool{}
|
result := map[string]bool{}
|
||||||
|
|
||||||
|
@ -194,14 +128,13 @@ func resolvePaths(paths []string, extensions map[string]bool) (map[string]bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, match := range matches {
|
for _, match := range matches {
|
||||||
// ignore existing files that don't match the allowed extensions
|
// don't care for extension filter right now for non glob paths
|
||||||
if f, err := os.Stat(match); !os.IsNotExist(err) && !f.IsDir() {
|
// since they could be a directory
|
||||||
if ext := filepath.Ext(match); ext != "" {
|
if isGlob {
|
||||||
if _, ok := extensions[ext]; !ok {
|
if _, ok := extensions[filepath.Ext(path)]; !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := result[match]; !ok {
|
if _, ok := result[match]; !ok {
|
||||||
result[match] = true
|
result[match] = true
|
||||||
|
@ -216,19 +149,13 @@ func resolvePaths(paths []string, extensions map[string]bool) (map[string]bool,
|
||||||
|
|
||||||
// remove overlapped paths so it makes the scan for changes later faster and simpler
|
// remove overlapped paths so it makes the scan for changes later faster and simpler
|
||||||
func removeOverlappedPaths(mapPaths map[string]bool) {
|
func removeOverlappedPaths(mapPaths map[string]bool) {
|
||||||
startDot := regexp.MustCompile(`^\./`)
|
|
||||||
|
|
||||||
for p1 := range mapPaths {
|
for p1 := range mapPaths {
|
||||||
p1 = startDot.ReplaceAllString(p1, "")
|
|
||||||
|
|
||||||
// skip to next item if this path has already been checked
|
// skip to next item if this path has already been checked
|
||||||
if v, ok := mapPaths[p1]; ok && !v {
|
if v, ok := mapPaths[p1]; ok && !v {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for p2 := range mapPaths {
|
for p2 := range mapPaths {
|
||||||
p2 = startDot.ReplaceAllString(p2, "")
|
|
||||||
|
|
||||||
if p1 == p2 {
|
if p1 == p2 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
204
watcher_test.go
204
watcher_test.go
|
@ -16,26 +16,18 @@ func TestWatcherDefaultValues(t *testing.T) {
|
||||||
var ignoreItems []string
|
var ignoreItems []string
|
||||||
var extensions []string
|
var extensions []string
|
||||||
|
|
||||||
wCfg := WatcherConfig{
|
w, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions)
|
||||||
DefaultIgnore: true,
|
|
||||||
PollInterval: pollInterval,
|
|
||||||
WatchItems: watchItems,
|
|
||||||
IgnoreItems: ignoreItems,
|
|
||||||
Extensions: extensions,
|
|
||||||
}
|
|
||||||
wt, err := NewWatcher(wCfg)
|
|
||||||
|
|
||||||
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) {
|
||||||
|
@ -44,36 +36,20 @@ func TestWatcherGlobPath(t *testing.T) {
|
||||||
ignoreItems := []string{"./testdata/**/*_test.go"}
|
ignoreItems := []string{"./testdata/**/*_test.go"}
|
||||||
var extensions []string
|
var extensions []string
|
||||||
|
|
||||||
wCfg := WatcherConfig{
|
w, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions)
|
||||||
DefaultIgnore: true,
|
|
||||||
PollInterval: pollInterval,
|
|
||||||
WatchItems: watchItems,
|
|
||||||
IgnoreItems: ignoreItems,
|
|
||||||
Extensions: extensions,
|
|
||||||
}
|
|
||||||
wt, err := NewWatcher(wCfg)
|
|
||||||
assert.Nil(t, err, "wacher error")
|
assert.Nil(t, err, "wacher error")
|
||||||
w := wt.(*watcher)
|
assert.Equal(t, map[string]bool{"testdata/server/main_test.go": true}, w.IgnoreItems)
|
||||||
assert.Equal(t, map[string]bool{"testdata/server/main_test.go": true}, w.ignoreItems)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWatcherRemoveOverlapdPaths(t *testing.T) {
|
func TestWatcherRemoveOverlapdPaths(t *testing.T) {
|
||||||
pollInterval := 0
|
pollInterval := 0
|
||||||
watchItems := []string{filepath.Join("testdata", "server")}
|
watchItems := []string{filepath.Join("testdata", "server")}
|
||||||
ignoreItems := []string{"./testdata/server/**/*", "./testdata/server"}
|
ignoreItems := []string{"./testdata/**/*", "./testdata/server"}
|
||||||
var extensions []string
|
var extensions []string
|
||||||
|
|
||||||
wCfg := WatcherConfig{
|
w, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions)
|
||||||
DefaultIgnore: true,
|
|
||||||
PollInterval: pollInterval,
|
|
||||||
WatchItems: watchItems,
|
|
||||||
IgnoreItems: ignoreItems,
|
|
||||||
Extensions: extensions,
|
|
||||||
}
|
|
||||||
wt, err := NewWatcher(wCfg)
|
|
||||||
assert.Nil(t, err, "wacher error")
|
assert.Nil(t, err, "wacher error")
|
||||||
w := wt.(*watcher)
|
assert.Equal(t, map[string]bool{"./testdata/server": true}, w.IgnoreItems)
|
||||||
assert.Equal(t, map[string]bool{"./testdata/server": true}, w.ignoreItems)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWatcherWatchChange(t *testing.T) {
|
func TestWatcherWatchChange(t *testing.T) {
|
||||||
|
@ -90,172 +66,26 @@ func TestWatcherWatchChange(t *testing.T) {
|
||||||
ignoreItems := []string{testfile}
|
ignoreItems := []string{testfile}
|
||||||
extensions := []string{"go"}
|
extensions := []string{"go"}
|
||||||
|
|
||||||
wCfg := WatcherConfig{
|
w, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions)
|
||||||
DefaultIgnore: true,
|
|
||||||
PollInterval: pollInterval,
|
|
||||||
WatchItems: watchItems,
|
|
||||||
IgnoreItems: ignoreItems,
|
|
||||||
Extensions: extensions,
|
|
||||||
}
|
|
||||||
w, err := NewWatcher(wCfg)
|
|
||||||
assert.Nil(t, err, "wacher error")
|
assert.Nil(t, err, "wacher error")
|
||||||
|
|
||||||
go w.Watch()
|
go w.Watch()
|
||||||
time.Sleep(time.Millisecond * 500)
|
time.Sleep(time.Millisecond * 500)
|
||||||
|
|
||||||
// update hidden files and dirs to check builtin hidden ignore is working
|
// update hidden files and dirs to check builtin hidden ignore is working
|
||||||
err = os.Chtimes(hiddenfile1, time.Now(), time.Now())
|
os.Chtimes(hiddenfile1, time.Now(), time.Now())
|
||||||
assert.Nil(t, err, "chtimes error")
|
os.Chtimes(hiddenfile2, time.Now(), time.Now())
|
||||||
|
|
||||||
err = os.Chtimes(hiddenfile2, time.Now(), time.Now())
|
|
||||||
assert.Nil(t, err, "chtimes error")
|
|
||||||
|
|
||||||
// update testfile first to check ignore is working
|
// update testfile first to check ignore is working
|
||||||
err = os.Chtimes(testfile, time.Now(), time.Now())
|
os.Chtimes(testfile, time.Now(), time.Now())
|
||||||
assert.Nil(t, err, "chtimes error")
|
|
||||||
|
|
||||||
time.Sleep(time.Millisecond * 500)
|
time.Sleep(time.Millisecond * 500)
|
||||||
err = os.Chtimes(mainfile, time.Now(), time.Now())
|
os.Chtimes(mainfile, time.Now(), time.Now())
|
||||||
assert.Nil(t, err, "chtimes error")
|
|
||||||
|
|
||||||
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWatcherIgnoreFile(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name, file, ignoreFile string
|
|
||||||
defaultIgnore, expectIgnore bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "with default ignore enabled it ignores vendor folder",
|
|
||||||
file: "vendor",
|
|
||||||
defaultIgnore: true,
|
|
||||||
expectIgnore: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "without default ignore enabled it does not ignore vendor folder",
|
|
||||||
file: "vendor",
|
|
||||||
defaultIgnore: false,
|
|
||||||
expectIgnore: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "with default ignore enabled it ignores test file",
|
|
||||||
file: filepath.Join("testdata", "server", "main_test.go"),
|
|
||||||
defaultIgnore: true,
|
|
||||||
expectIgnore: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "with default ignore enabled it does no ignore non test files which have test in the name",
|
|
||||||
file: filepath.Join("testdata", "ignore-test-name.txt"),
|
|
||||||
defaultIgnore: true,
|
|
||||||
expectIgnore: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "without default ignore enabled it does not ignore test file",
|
|
||||||
file: filepath.Join("testdata", "server", "main_test.go"),
|
|
||||||
defaultIgnore: false,
|
|
||||||
expectIgnore: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "with default ignore enabled it ignores ignored items",
|
|
||||||
file: filepath.Join("testdata", "server", "main.go"),
|
|
||||||
ignoreFile: filepath.Join("testdata", "server", "main.go"),
|
|
||||||
defaultIgnore: true,
|
|
||||||
expectIgnore: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "without default ignore enabled it ignores ignored items",
|
|
||||||
file: filepath.Join("testdata", "server", "main.go"),
|
|
||||||
ignoreFile: filepath.Join("testdata", "server", "main.go"),
|
|
||||||
defaultIgnore: false,
|
|
||||||
expectIgnore: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// create vendor folder for testing
|
|
||||||
if err := os.MkdirAll("vendor", os.ModePerm); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
srvdir := "."
|
|
||||||
|
|
||||||
watchItems := []string{srvdir}
|
|
||||||
ignoreItems := []string{}
|
|
||||||
if len(tc.ignoreFile) > 0 {
|
|
||||||
ignoreItems = append(ignoreItems, tc.ignoreFile)
|
|
||||||
}
|
|
||||||
extensions := []string{"go"}
|
|
||||||
|
|
||||||
wCfg := WatcherConfig{
|
|
||||||
DefaultIgnore: tc.defaultIgnore,
|
|
||||||
WatchItems: watchItems,
|
|
||||||
IgnoreItems: ignoreItems,
|
|
||||||
Extensions: extensions,
|
|
||||||
}
|
|
||||||
w, err := NewWatcher(wCfg)
|
|
||||||
assert.Nil(t, err, "wacher error")
|
|
||||||
|
|
||||||
wt := w.(*watcher)
|
|
||||||
|
|
||||||
filePath := tc.file
|
|
||||||
file, err := os.Open(filePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInfo, err := file.Stat()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, tc.expectIgnore, wt.ignoreFile(filePath, fileInfo))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWatcherResolvePaths(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
paths []string
|
|
||||||
extensions, expectPaths map[string]bool
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "remove duplicated paths",
|
|
||||||
paths: []string{"testdata/test-duplicated-paths", "testdata/test-duplicated-paths"},
|
|
||||||
extensions: map[string]bool{".txt": true},
|
|
||||||
expectPaths: map[string]bool{"testdata/test-duplicated-paths": true},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "remove duplicated paths from glob",
|
|
||||||
paths: []string{"testdata/test-duplicated-paths", "testdata/test-duplicated-paths/**/*"},
|
|
||||||
extensions: map[string]bool{".txt": true},
|
|
||||||
expectPaths: map[string]bool{"testdata/test-duplicated-paths": true},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "remove duplicated paths from glob with inverse order",
|
|
||||||
paths: []string{"testdata/test-duplicated-paths/**/*", "testdata/test-duplicated-paths"},
|
|
||||||
extensions: map[string]bool{".txt": true},
|
|
||||||
expectPaths: map[string]bool{"testdata/test-duplicated-paths": true},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
paths, err := resolvePaths(tc.paths, tc.extensions)
|
|
||||||
if tc.err == nil {
|
|
||||||
assert.Nil(t, err, "resolve path error")
|
|
||||||
assert.Equal(t, tc.expectPaths, paths)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, tc.err, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue