diff --git a/go.mod b/go.mod index a4b2733..3236905 100644 --- a/go.mod +++ b/go.mod @@ -5,15 +5,15 @@ go 1.20 require ( github.com/charmbracelet/bubbles v0.16.1 github.com/charmbracelet/bubbletea v0.24.2 + github.com/charmbracelet/lipgloss v0.7.1 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/posener/complete/v2 v2.0.1-alpha.13 - mvdan.cc/sh/v3 v3.6.0 + mvdan.cc/sh/v3 v3.7.0 ) require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/lipgloss v0.7.1 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -28,8 +28,8 @@ require ( github.com/posener/script v1.1.5 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/sahilm/fuzzy v0.1.0 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/term v0.6.0 // indirect + golang.org/x/sync v0.2.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/term v0.8.0 // indirect golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index d7063fb..7309751 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,7 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 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/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= @@ -54,6 +55,7 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY= github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -61,15 +63,21 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -77,3 +85,5 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= mvdan.cc/sh/v3 v3.6.0 h1:gtva4EXJ0dFNvl5bHjcUEvws+KRcDslT8VKheTYkbGU= mvdan.cc/sh/v3 v3.6.0/go.mod h1:U4mhtBLZ32iWhif5/lD+ygy1zrgaQhUu+XFy7C8+TTA= +mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= +mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= diff --git a/run/interpreter.go b/run/interpreter.go index 82c9312..b28fff7 100644 --- a/run/interpreter.go +++ b/run/interpreter.go @@ -10,6 +10,7 @@ import ( "regexp" "strings" + "golang.org/x/term" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/interp" "mvdan.cc/sh/v3/syntax" @@ -99,6 +100,9 @@ func (i interpreter) executeShell( if err != nil { return fmt.Errorf("failed to parse task: %w", err) } + if os.Getenv("NO_COLOR") != "1" && term.IsTerminal(int(os.Stdout.Fd())) { + env = append(env, "CLICOLOR_FORCE=1", "FORCE_COLOR=1") + } runner, err := interp.New( interp.Env(expand.ListEnviron(env...)), interp.StdIO(stdFiles(logPrefix)), diff --git a/run/run.go b/run/run.go index 4daf644..f8d4e4e 100644 --- a/run/run.go +++ b/run/run.go @@ -25,6 +25,7 @@ type Runner struct { tasks models.Tasks dir string alreadyRan map[string]bool + alreadRanMu sync.Mutex } // NewRunner takes Tasks and returns a Runner. @@ -111,11 +112,14 @@ func (r *Runner) runWithPadding(ctx context.Context, name string, inputs []strin if !ok { return fmt.Errorf("task %s not found", name) } + r.alreadRanMu.Lock() if task.RequiredBehaviour == models.RequiredBehaviourOnce && r.alreadyRan[task.Name] { + r.alreadRanMu.Unlock() fmt.Printf("task %q ran already: skipping\n", task.Name) return nil } r.alreadyRan[task.Name] = true + r.alreadRanMu.Unlock() env := os.Environ() env = append(env, task.Env...) inp, err := getInputs(task, inputs, env) diff --git a/run/run_test.go b/run/run_test.go index 5fe6244..6541df7 100644 --- a/run/run_test.go +++ b/run/run_test.go @@ -4,33 +4,39 @@ import ( "context" "errors" "os" + "sync" "testing" "github.com/joerdav/xc/models" ) type mockScriptRunner struct { - calls int - returns error + calls int + returns error + runnerMutex sync.Mutex } func (r *mockScriptRunner) Execute( ctx context.Context, text string, env, args []string, dir, logPrefix string, ) error { + r.runnerMutex.Lock() + defer r.runnerMutex.Unlock() r.calls++ return r.returns } -func TestRun(t *testing.T) { - tests := []struct { - name string - tasks models.Tasks - taskName string - err error - expectedRunError bool - expectedParseError bool - expectedTasksRun int - }{ +type testCase struct { + name string + tasks models.Tasks + taskName string + err error + expectedRunError bool + expectedParseError bool + expectedTasksRun int +} + +func testCases() []testCase { + return []testCase{ { name: "given an invalid task should not run command", tasks: []models.Task{ @@ -165,7 +171,36 @@ func TestRun(t *testing.T) { expectedTasksRun: 3, }, } - for _, tt := range tests { +} + +func TestRunAsync(t *testing.T) { + for _, tt := range testCases() { + tt := tt + t.Run(tt.name, func(t *testing.T) { + for i := range tt.tasks { + tt.tasks[i].DepsBehaviour = models.DependencyBehaviourAsync + } + runner, err := NewRunner(tt.tasks, "") + if (err != nil) != tt.expectedParseError { + t.Fatalf("expected error %v, got %v", tt.expectedParseError, err) + } + if err != nil { + return + } + scriptRunner := &mockScriptRunner{returns: tt.err} + runner.scriptRunner = scriptRunner + err = runner.Run(context.Background(), tt.taskName, nil) + if (err != nil) != tt.expectedRunError { + t.Fatalf("expected error %v, got %v", tt.expectedRunError, err) + } + if scriptRunner.calls != tt.expectedTasksRun { + t.Fatalf("expected %d task runs got %d", tt.expectedTasksRun, scriptRunner.calls) + } + }) + } +} +func TestRun(t *testing.T) { + for _, tt := range testCases() { tt := tt t.Run(tt.name, func(t *testing.T) { runner, err := NewRunner(tt.tasks, "")