diff --git a/cmd/adwatchd/main.go b/cmd/adwatchd/main.go index 825ab91bf..352a7fa04 100644 --- a/cmd/adwatchd/main.go +++ b/cmd/adwatchd/main.go @@ -14,7 +14,13 @@ import ( "github.com/ubuntu/go-i18n" ) -func run(a *commands.App) int { +type app interface { + Run() error + UsageError() bool + Quit(syscall.Signal) error +} + +func run(a app) int { i18n.InitI18nDomain(consts.TEXTDOMAIN, po.Files) defer installSignalHandler(a)() log.SetFormatter(&log.TextFormatter{ @@ -38,7 +44,7 @@ func run(a *commands.App) int { return 0 } -func installSignalHandler(a *commands.App) func() { +func installSignalHandler(a app) func() { c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) @@ -70,6 +76,5 @@ func installSignalHandler(a *commands.App) func() { } func main() { - app := commands.New() - os.Exit(run(app)) + os.Exit(run(commands.New())) } diff --git a/cmd/adwatchd/main_test.go b/cmd/adwatchd/main_test.go new file mode 100644 index 000000000..7ab11ba82 --- /dev/null +++ b/cmd/adwatchd/main_test.go @@ -0,0 +1,114 @@ +package main + +import ( + "errors" + "os" + "os/exec" + "syscall" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +type myApp struct { + done chan struct{} + + runError bool + usageErrorReturn bool +} + +func (a *myApp) Run() error { + <-a.done + if a.runError { + return errors.New("Error requested") + } + return nil +} + +func (a myApp) UsageError() bool { + return a.usageErrorReturn +} + +func (a *myApp) Quit(_ syscall.Signal) error { + close(a.done) + return nil +} + +func TestRun(t *testing.T) { + tests := map[string]struct { + runError bool + usageErrorReturn bool + sendSig syscall.Signal + + wantReturnCode int + }{ + "Run and exit successfully": {}, + "Run and return error": {runError: true, wantReturnCode: 1}, + "Run and return usage error": {usageErrorReturn: true, runError: true, wantReturnCode: 2}, + "Run and usage error only does not fail": {usageErrorReturn: true}, + + // Signals handling + "Send SIGINT exits": {sendSig: syscall.SIGINT}, + "Send SIGTERM exits": {sendSig: syscall.SIGTERM}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // Signal handler tests cannot run in parallel + // + + a := myApp{ + done: make(chan struct{}), + runError: tc.runError, + usageErrorReturn: tc.usageErrorReturn, + } + + var rc int + wait := make(chan struct{}) + go func() { + rc = run(&a) + close(wait) + }() + + time.Sleep(100 * time.Millisecond) + + var exited bool + switch tc.sendSig { + case syscall.SIGINT: + fallthrough + case syscall.SIGTERM: + err := syscall.Kill(syscall.Getpid(), tc.sendSig) + require.NoError(t, err, "Teardown: kill should return no error") + select { + case <-time.After(50 * time.Millisecond): + exited = false + case <-wait: + exited = true + } + require.Equal(t, true, exited, "Expect to exit on SIGINT and SIGTERM") + } + + if !exited { + _ = a.Quit(syscall.SIGINT) + <-wait + } + + require.Equal(t, tc.wantReturnCode, rc, "Return expected code") + }) + } +} + +func TestMainApp(t *testing.T) { + if os.Getenv("ADSYS_CALL_MAIN") != "" { + main() + return + } + + // #nosec G204: this is only for tests, under controlled args + cmd := exec.Command(os.Args[0], "version", "-test.run=TestMainApp") + cmd.Env = append(os.Environ(), "ADSYS_CALL_MAIN=1") + out, err := cmd.CombinedOutput() + + require.Contains(t, string(out), "adwatchd\tdev", "Main function should print the version") + require.NoError(t, err, "Main should not return an error") +}