diff --git a/.vscode/settings.json b/.vscode/settings.json index 4f2e48f..2da1cf2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,15 +1,17 @@ { "cSpell.words": [ "charsets", + "exem", "menv", "merrs", "mexec", "MINIT", "mlog", + "mrunners", "mtmpl", "munit", "shellquote", "simplifiedchinese", "stretchr" ] -} +} \ No newline at end of file diff --git a/pkg/mrunners/runner_cron.go b/pkg/mrunners/runner_cron.go index e7f920a..197924c 100644 --- a/pkg/mrunners/runner_cron.go +++ b/pkg/mrunners/runner_cron.go @@ -48,6 +48,7 @@ func (r *runnerCron) Do(ctx context.Context) (err error) { cr := cron.New(cron.WithLogger(cron.PrintfLogger(r.Logger))) var chErr chan error + if r.Unit.Critical { chErr = make(chan error, 1) } @@ -56,7 +57,7 @@ func (r *runnerCron) Do(ctx context.Context) (err error) { r.Print("triggered") if err := r.Exec.Execute(r.Unit.ExecuteOptions(r.Logger)); err != nil { r.Error("failed executing: " + err.Error()) - if r.Unit.Critical { + if chErr != nil { select { case chErr <- err: default: @@ -72,7 +73,7 @@ func (r *runnerCron) Do(ctx context.Context) (err error) { cr.Start() - if r.Unit.Critical { + if chErr != nil { select { case <-ctx.Done(): case err = <-chErr: diff --git a/pkg/mrunners/runner_cron_test.go b/pkg/mrunners/runner_cron_test.go new file mode 100644 index 0000000..38aaa8b --- /dev/null +++ b/pkg/mrunners/runner_cron_test.go @@ -0,0 +1,103 @@ +package mrunners + +import ( + "bytes" + "context" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/yankeguo/minit/pkg/mexec" + "github.com/yankeguo/minit/pkg/mlog" + "github.com/yankeguo/minit/pkg/munit" + "github.com/yankeguo/rg" +) + +func TestRunnerCron(t *testing.T) { + exem := mexec.NewManager() + + buf := &bytes.Buffer{} + + r := &runnerCron{ + RunnerOptions: RunnerOptions{ + Unit: munit.Unit{ + Kind: munit.KindCron, + Name: "test", + Cron: "@every 1s", + Immediate: true, + Command: []string{ + "echo", "hhhlll", + }, + }, + Exec: exem, + Logger: rg.Must(mlog.NewProcLogger(mlog.ProcLoggerOptions{ + ConsoleOut: buf, + ConsoleErr: buf, + })), + }, + } + + ctx, ctxCancel := context.WithCancel(context.Background()) + defer ctxCancel() + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + err := r.Do(ctx) + require.NoError(t, err) + }() + + time.Sleep(time.Millisecond * 2500) + + ctxCancel() + + wg.Wait() + + require.Equal(t, 3, strings.Count(buf.String(), "hhhlll\n")) +} + +func TestRunnerCronCritical(t *testing.T) { + exem := mexec.NewManager() + + buf := &bytes.Buffer{} + + r := &runnerCron{ + RunnerOptions: RunnerOptions{ + Unit: munit.Unit{ + Kind: munit.KindCron, + Name: "test", + Cron: "@every 1s", + Shell: "/bin/bash", + Critical: true, + Command: []string{ + "exit 2", + }, + }, + Exec: exem, + Logger: rg.Must(mlog.NewProcLogger(mlog.ProcLoggerOptions{ + ConsoleOut: buf, + ConsoleErr: buf, + })), + }, + } + + ctx, ctxCancel := context.WithCancel(context.Background()) + defer ctxCancel() + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + err := r.Do(ctx) + require.Error(t, err) + }() + + time.Sleep(time.Millisecond * 2500) + + ctxCancel() + + wg.Wait() +} diff --git a/pkg/mrunners/runner_daemon_test.go b/pkg/mrunners/runner_daemon_test.go new file mode 100644 index 0000000..1de581d --- /dev/null +++ b/pkg/mrunners/runner_daemon_test.go @@ -0,0 +1,103 @@ +package mrunners + +import ( + "bytes" + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/yankeguo/minit/pkg/mexec" + "github.com/yankeguo/minit/pkg/mlog" + "github.com/yankeguo/minit/pkg/munit" + "github.com/yankeguo/rg" +) + +func TestRunnerDaemon(t *testing.T) { + exem := mexec.NewManager() + + buf := &bytes.Buffer{} + + r := &runnerDaemon{ + RunnerOptions: RunnerOptions{ + Unit: munit.Unit{ + Kind: munit.KindDaemon, + Name: "test", + Shell: "/bin/bash", + Command: []string{ + "sleep 1 && echo hello && exit 2", + }, + Critical: true, + SuccessCodes: []int{0, 2}, + }, + Exec: exem, + Logger: rg.Must(mlog.NewProcLogger(mlog.ProcLoggerOptions{ + ConsoleOut: buf, + ConsoleErr: buf, + })), + }, + } + + ctx, ctxCancel := context.WithCancel(context.Background()) + defer ctxCancel() + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + err := r.Do(ctx) + require.NoError(t, err) + }() + + time.Sleep(time.Millisecond * 2500) + + ctxCancel() + + wg.Wait() + + require.Equal(t, 1, bytes.Count(buf.Bytes(), []byte("hello\n"))) +} + +func TestRunnerDaemonCritical(t *testing.T) { + exem := mexec.NewManager() + + buf := &bytes.Buffer{} + + r := &runnerDaemon{ + RunnerOptions: RunnerOptions{ + Unit: munit.Unit{ + Kind: munit.KindDaemon, + Name: "test", + Shell: "/bin/bash", + Command: []string{ + "sleep 1 && echo hello && exit 2", + }, + Critical: true, + SuccessCodes: []int{1}, + }, + Exec: exem, + Logger: rg.Must(mlog.NewProcLogger(mlog.ProcLoggerOptions{ + ConsoleOut: buf, + ConsoleErr: buf, + })), + }, + } + + ctx, ctxCancel := context.WithCancel(context.Background()) + defer ctxCancel() + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + err := r.Do(ctx) + require.Error(t, err) + }() + + time.Sleep(time.Millisecond * 2500) + + ctxCancel() + + wg.Wait() +} diff --git a/pkg/mrunners/runner_once_test.go b/pkg/mrunners/runner_once_test.go new file mode 100644 index 0000000..1ff6609 --- /dev/null +++ b/pkg/mrunners/runner_once_test.go @@ -0,0 +1,107 @@ +package mrunners + +import ( + "bytes" + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/yankeguo/minit/pkg/mexec" + "github.com/yankeguo/minit/pkg/mlog" + "github.com/yankeguo/minit/pkg/munit" + "github.com/yankeguo/rg" +) + +func TestRunnerOnce(t *testing.T) { + exem := mexec.NewManager() + + buf := &bytes.Buffer{} + + r := &runnerOnce{ + RunnerOptions: RunnerOptions{ + Unit: munit.Unit{ + Kind: munit.KindOnce, + Name: "test", + Shell: "/bin/bash", + Command: []string{ + "sleep 1 && echo hello && exit 2", + }, + Critical: true, + SuccessCodes: []int{0, 2}, + }, + Exec: exem, + Logger: rg.Must(mlog.NewProcLogger(mlog.ProcLoggerOptions{ + ConsoleOut: buf, + ConsoleErr: buf, + })), + }, + } + + err := r.Do(context.Background()) + require.NoError(t, err) +} + +func TestRunnerOnceCritical(t *testing.T) { + exem := mexec.NewManager() + + buf := &bytes.Buffer{} + + r := &runnerOnce{ + RunnerOptions: RunnerOptions{ + Unit: munit.Unit{ + Kind: munit.KindOnce, + Name: "test", + Shell: "/bin/bash", + Command: []string{ + "sleep 1 && echo hello && exit 2", + }, + Critical: true, + SuccessCodes: []int{1}, + }, + Exec: exem, + Logger: rg.Must(mlog.NewProcLogger(mlog.ProcLoggerOptions{ + ConsoleOut: buf, + ConsoleErr: buf, + })), + }, + } + + err := r.Do(context.Background()) + require.Error(t, err) +} + +func TestRunnerOnceCriticalNonBlocking(t *testing.T) { + exem := mexec.NewManager() + + buf := &bytes.Buffer{} + + blocking := false + + r := &runnerOnce{ + RunnerOptions: RunnerOptions{ + Unit: munit.Unit{ + Kind: munit.KindOnce, + Name: "test", + Shell: "/bin/bash", + Blocking: &blocking, + Command: []string{ + "sleep 1 && echo hello && exit 2", + }, + Critical: true, + SuccessCodes: []int{1}, + }, + Exec: exem, + Logger: rg.Must(mlog.NewProcLogger(mlog.ProcLoggerOptions{ + ConsoleOut: buf, + ConsoleErr: buf, + })), + }, + } + + start := time.Now() + + err := r.Do(context.Background()) + require.NoError(t, err) + require.True(t, time.Since(start) < time.Millisecond*100) +} diff --git a/pkg/mrunners/runner_render.go b/pkg/mrunners/runner_render.go index aa7e7a4..d4d32e8 100644 --- a/pkg/mrunners/runner_render.go +++ b/pkg/mrunners/runner_render.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "unicode" "github.com/yankeguo/minit/pkg/menv" "github.com/yankeguo/minit/pkg/mtmpl" @@ -62,6 +63,8 @@ func (r *runnerRender) Do(ctx context.Context) (err error) { return } + allNames := map[string]struct{}{} + for _, filePattern := range r.Unit.Files { var names []string @@ -78,35 +81,37 @@ func (r *runnerRender) Do(ctx context.Context) (err error) { } for _, name := range names { - if err = r.doFile(ctx, name, env); err != nil { - r.Error("failed rendering: " + name + ": " + err.Error()) + allNames[name] = struct{}{} + } + } - if r.Unit.Critical { - return - } else { - err = nil - } + for name := range allNames { + if err = r.doFile(ctx, name, env); err != nil { + r.Error("failed rendering: " + name + ": " + err.Error()) - continue + if r.Unit.Critical { + return + } else { + err = nil } - r.Print("done rendering: " + name) + continue } + + r.Print("done rendering: " + name) } return } func sanitizeLines(s []byte) []byte { - lines := bytes.Split(s, []byte("\n")) - out := &bytes.Buffer{} - for _, line := range lines { - line = bytes.TrimSpace(line) + var out [][]byte + for _, line := range bytes.Split(s, []byte("\n")) { + line = bytes.TrimRightFunc(line, unicode.IsSpace) if len(line) == 0 { continue } - out.Write(line) - out.WriteRune('\n') + out = append(out, line) } - return out.Bytes() + return bytes.Join(out, []byte("\n")) } diff --git a/pkg/mrunners/runner_render_test.go b/pkg/mrunners/runner_render_test.go new file mode 100644 index 0000000..6f39f1a --- /dev/null +++ b/pkg/mrunners/runner_render_test.go @@ -0,0 +1,57 @@ +package mrunners + +import ( + "bytes" + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "github.com/yankeguo/minit/pkg/mexec" + "github.com/yankeguo/minit/pkg/mlog" + "github.com/yankeguo/minit/pkg/munit" + "github.com/yankeguo/rg" +) + +func TestRunnerRender(t *testing.T) { + dir := t.TempDir() + os.WriteFile(filepath.Join(dir, "file1.txt"), []byte("{{stringsToUpper .Env.Foo}}"), 0755) + os.WriteFile(filepath.Join(dir, "file2.txt"), []byte("{{stringsToUpper .Env.Bar}}"), 0755) + os.WriteFile(filepath.Join(dir, "file3.txt"), []byte("{{stringsToUpper .Env.Foo}} \n {{stringsToUpper .Env.Bar}}\n\n \n{{stringsToUpper .Env.Foo}}\n"), 0755) + + exem := mexec.NewManager() + + buf := &bytes.Buffer{} + + r := &runnerRender{ + RunnerOptions: RunnerOptions{ + Unit: munit.Unit{ + Kind: munit.KindRender, + Name: "test", + Files: []string{filepath.Join(dir, "*.txt")}, + Critical: true, + Env: map[string]string{ + "Foo": "foo", + "Bar": "bar", + }, + }, + Exec: exem, + Logger: rg.Must(mlog.NewProcLogger(mlog.ProcLoggerOptions{ + ConsoleOut: buf, + ConsoleErr: buf, + })), + }, + } + err := r.Do(context.Background()) + require.NoError(t, err) + buf1, err := os.ReadFile(filepath.Join(dir, "file1.txt")) + require.NoError(t, err) + require.Equal(t, "FOO", string(buf1)) + buf2, err := os.ReadFile(filepath.Join(dir, "file2.txt")) + require.NoError(t, err) + require.Equal(t, "BAR", string(buf2)) + buf3, err := os.ReadFile(filepath.Join(dir, "file3.txt")) + require.NoError(t, err) + require.Equal(t, "FOO\n BAR\nFOO", string(buf3)) +}