diff --git a/cmd/task/task.go b/cmd/task/task.go index d5f2b7baf0..6cdcdb0d3d 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -16,6 +16,7 @@ import ( "github.com/go-task/task/v3/internal/flags" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/version" + "github.com/go-task/task/v3/taskfile" "github.com/go-task/task/v3/taskfile/ast" ) @@ -110,16 +111,63 @@ func run() error { return nil } - e := task.NewExecutor( + if err := experiments.Validate(); err != nil { + log.Warnf("%s\n", err.Error()) + } + + // Create a new root node for the given entrypoint + node, err := taskfile.NewRootNode( + flags.Entrypoint, + flags.Dir, + flags.Insecure, + ) + if err != nil { + return err + } + + tempDir, err := task.NewTempDir(node.Dir()) + if err != nil { + return err + } + + reader := taskfile.NewReader( flags.WithFlags(), - task.WithVersionCheck(true), + taskfile.WithTempDir(tempDir.Remote), + taskfile.WithDebugFunc(func(s string) { + log.VerboseOutf(logger.Magenta, s) + }), + taskfile.WithPromptFunc(func(s string) error { + return log.Prompt(logger.Yellow, s, "n", "y", "yes") + }), ) - if err := e.Setup(); err != nil { + + ctx, cf := context.WithTimeout(context.Background(), flags.Timeout) + defer cf() + graph, err := reader.Read(ctx, node) + if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + return &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: flags.Timeout} + } + return err + } + + executor, err := task.NewExecutor(graph, + flags.WithFlags(), + task.WithDir(node.Dir()), + task.WithTempDir(tempDir), + ) + if err != nil { return err } + // If the download flag is specified, we should stop execution as soon as + // taskfile is downloaded + if flags.Download { + return nil + } + if flags.ClearCache { - cachePath := filepath.Join(e.TempDir.Remote, "remote") + cachePath := filepath.Join(executor.TempDir.Remote, "remote") return os.RemoveAll(cachePath) } @@ -131,9 +179,9 @@ func run() error { ) if listOptions.ShouldListTasks() { if flags.Silent { - return e.ListTaskNames(flags.ListAll) + return executor.ListTaskNames(flags.ListAll) } - foundTasks, err := e.ListTasks(listOptions) + foundTasks, err := executor.ListTasks(listOptions) if err != nil { return err } @@ -165,17 +213,17 @@ func run() error { globals.Set("CLI_SILENT", ast.Var{Value: flags.Silent}) globals.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose}) globals.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline}) - e.Taskfile.Vars.Merge(globals, nil) + executor.Taskfile.Vars.Merge(globals, nil) if !flags.Watch { - e.InterceptInterruptSignals() + executor.InterceptInterruptSignals() } - ctx := context.Background() + ctx = context.Background() if flags.Status { - return e.Status(ctx, calls...) + return executor.Status(ctx, calls...) } - return e.Run(ctx, calls...) + return executor.Run(ctx, calls...) } diff --git a/executor.go b/executor.go index 8f9233ef88..ad8cd6545f 100644 --- a/executor.go +++ b/executor.go @@ -4,6 +4,7 @@ import ( "context" "io" "os" + "path/filepath" "sync" "time" @@ -26,27 +27,21 @@ type ( // within them. Executor struct { // Flags - Dir string - Entrypoint string - TempDir TempDir - Force bool - ForceAll bool - Insecure bool - Download bool - Offline bool - Timeout time.Duration - CacheExpiryDuration time.Duration - Watch bool - Verbose bool - Silent bool - AssumeYes bool - AssumeTerm bool // Used for testing - Dry bool - Summary bool - Parallel bool - Color bool - Concurrency int - Interval time.Duration + Dir string + TempDir *TempDir + Force bool + ForceAll bool + Watch bool + Verbose bool + Silent bool + AssumeYes bool + AssumeTerm bool // Used for testing + Dry bool + Summary bool + Parallel bool + Color bool + Concurrency int + Interval time.Duration // I/O Stdin io.Reader @@ -72,17 +67,17 @@ type ( executionHashesMutex sync.Mutex watchedDirs *xsync.MapOf[string, bool] } - TempDir struct { - Remote string - Fingerprint string - } ) // NewExecutor creates a new [Executor] and applies the given functional options // to it. -func NewExecutor(opts ...ExecutorOption) *Executor { +func NewExecutor(graph *ast.TaskfileGraph, opts ...ExecutorOption) (*Executor, error) { + tf, err := graph.Merge() + if err != nil { + return nil, err + } e := &Executor{ - Timeout: time.Second * 10, + Taskfile: tf, Stdin: os.Stdin, Stdout: os.Stdout, Stderr: os.Stderr, @@ -100,7 +95,10 @@ func NewExecutor(opts ...ExecutorOption) *Executor { executionHashesMutex: sync.Mutex{}, } e.Options(opts...) - return e + if err := e.setup(); err != nil { + return nil, err + } + return e, nil } // Options loops through the given [ExecutorOption] functions and applies them @@ -122,33 +120,23 @@ type dirOption struct { } func (o *dirOption) ApplyToExecutor(e *Executor) { - e.Dir = o.dir -} - -// WithEntrypoint sets the entrypoint (main Taskfile) of the [Executor]. By -// default, Task will search for one of the default Taskfiles in the given -// directory. -func WithEntrypoint(entrypoint string) ExecutorOption { - return &entrypointOption{entrypoint} -} - -type entrypointOption struct { - entrypoint string -} - -func (o *entrypointOption) ApplyToExecutor(e *Executor) { - e.Entrypoint = o.entrypoint + absDir, err := filepath.Abs(o.dir) + if err != nil { + e.Dir = o.dir + return + } + e.Dir = absDir } // WithTempDir sets the temporary directory that will be used by [Executor] for // storing temporary files like checksums and cached remote files. By default, // the temporary directory is set to the user's temporary directory. -func WithTempDir(tempDir TempDir) ExecutorOption { +func WithTempDir(tempDir *TempDir) ExecutorOption { return &tempDirOption{tempDir} } type tempDirOption struct { - tempDir TempDir + tempDir *TempDir } func (o *tempDirOption) ApplyToExecutor(e *Executor) { @@ -183,76 +171,6 @@ func (o *forceAllOption) ApplyToExecutor(e *Executor) { e.ForceAll = o.forceAll } -// WithInsecure allows the [Executor] to make insecure connections when reading -// remote taskfiles. By default, insecure connections are rejected. -func WithInsecure(insecure bool) ExecutorOption { - return &insecureOption{insecure} -} - -type insecureOption struct { - insecure bool -} - -func (o *insecureOption) ApplyToExecutor(e *Executor) { - e.Insecure = o.insecure -} - -// WithDownload forces the [Executor] to download a fresh copy of the taskfile -// from the remote source. -func WithDownload(download bool) ExecutorOption { - return &downloadOption{download} -} - -type downloadOption struct { - download bool -} - -func (o *downloadOption) ApplyToExecutor(e *Executor) { - e.Download = o.download -} - -// WithOffline stops the [Executor] from being able to make network connections. -// It will still be able to read local files and cached copies of remote files. -func WithOffline(offline bool) ExecutorOption { - return &offlineOption{offline} -} - -type offlineOption struct { - offline bool -} - -func (o *offlineOption) ApplyToExecutor(e *Executor) { - e.Offline = o.offline -} - -// WithTimeout sets the [Executor]'s timeout for fetching remote taskfiles. By -// default, the timeout is set to 10 seconds. -func WithTimeout(timeout time.Duration) ExecutorOption { - return &timeoutOption{timeout} -} - -type timeoutOption struct { - timeout time.Duration -} - -func (o *timeoutOption) ApplyToExecutor(e *Executor) { - e.Timeout = o.timeout -} - -// WithCacheExpiryDuration sets the duration after which the cache is considered -// expired. By default, the cache is considered expired after 24 hours. -func WithCacheExpiryDuration(duration time.Duration) ExecutorOption { - return &cacheExpiryDurationOption{duration: duration} -} - -type cacheExpiryDurationOption struct { - duration time.Duration -} - -func (o *cacheExpiryDurationOption) ApplyToExecutor(r *Executor) { - r.CacheExpiryDuration = o.duration -} - // WithWatch tells the [Executor] to keep running in the background and watch // for changes to the fingerprint of the tasks that are run. When changes are // detected, a new task run is triggered. diff --git a/setup.go b/executor_setup.go similarity index 61% rename from setup.go rename to executor_setup.go index b4926f2d60..9128229933 100644 --- a/setup.go +++ b/executor_setup.go @@ -4,18 +4,13 @@ import ( "context" "fmt" "os" - "path/filepath" "slices" - "strings" "sync" "github.com/Masterminds/semver/v3" "github.com/sajari/fuzzy" "github.com/go-task/task/v3/errors" - "github.com/go-task/task/v3/internal/env" - "github.com/go-task/task/v3/internal/execext" - "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/version" @@ -23,18 +18,8 @@ import ( "github.com/go-task/task/v3/taskfile/ast" ) -func (e *Executor) Setup() error { +func (e *Executor) setup() error { e.setupLogger() - node, err := e.getRootNode() - if err != nil { - return err - } - if err := e.setupTempDir(); err != nil { - return err - } - if err := e.readTaskfile(node); err != nil { - return err - } e.setupFuzzyModel() e.setupStdFiles() if err := e.setupOutput(); err != nil { @@ -54,46 +39,6 @@ func (e *Executor) Setup() error { return nil } -func (e *Executor) getRootNode() (taskfile.Node, error) { - node, err := taskfile.NewRootNode(e.Entrypoint, e.Dir, e.Insecure, e.Timeout) - if err != nil { - return nil, err - } - e.Dir = node.Dir() - return node, err -} - -func (e *Executor) readTaskfile(node taskfile.Node) error { - ctx, cf := context.WithTimeout(context.Background(), e.Timeout) - defer cf() - debugFunc := func(s string) { - e.Logger.VerboseOutf(logger.Magenta, s) - } - promptFunc := func(s string) error { - return e.Logger.Prompt(logger.Yellow, s, "n", "y", "yes") - } - reader := taskfile.NewReader( - taskfile.WithInsecure(e.Insecure), - taskfile.WithDownload(e.Download), - taskfile.WithOffline(e.Offline), - taskfile.WithTempDir(e.TempDir.Remote), - taskfile.WithCacheExpiryDuration(e.CacheExpiryDuration), - taskfile.WithDebugFunc(debugFunc), - taskfile.WithPromptFunc(promptFunc), - ) - graph, err := reader.Read(ctx, node) - if err != nil { - if errors.Is(err, context.DeadlineExceeded) { - return &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: e.Timeout} - } - return err - } - if e.Taskfile, err = graph.Merge(); err != nil { - return err - } - return nil -} - func (e *Executor) setupFuzzyModel() { if e.Taskfile == nil { return @@ -115,52 +60,6 @@ func (e *Executor) setupFuzzyModel() { e.fuzzyModel = model } -func (e *Executor) setupTempDir() error { - if e.TempDir != (TempDir{}) { - return nil - } - - tempDir := env.GetTaskEnv("TEMP_DIR") - if tempDir == "" { - e.TempDir = TempDir{ - Remote: filepathext.SmartJoin(e.Dir, ".task"), - Fingerprint: filepathext.SmartJoin(e.Dir, ".task"), - } - } else if filepath.IsAbs(tempDir) || strings.HasPrefix(tempDir, "~") { - tempDir, err := execext.ExpandLiteral(tempDir) - if err != nil { - return err - } - projectDir, _ := filepath.Abs(e.Dir) - projectName := filepath.Base(projectDir) - e.TempDir = TempDir{ - Remote: tempDir, - Fingerprint: filepathext.SmartJoin(tempDir, projectName), - } - - } else { - e.TempDir = TempDir{ - Remote: filepathext.SmartJoin(e.Dir, tempDir), - Fingerprint: filepathext.SmartJoin(e.Dir, tempDir), - } - } - - remoteDir := env.GetTaskEnv("REMOTE_DIR") - if remoteDir != "" { - if filepath.IsAbs(remoteDir) || strings.HasPrefix(remoteDir, "~") { - remoteTempDir, err := execext.ExpandLiteral(remoteDir) - if err != nil { - return err - } - e.TempDir.Remote = remoteTempDir - } else { - e.TempDir.Remote = filepathext.SmartJoin(e.Dir, ".task") - } - } - - return nil -} - func (e *Executor) setupStdFiles() { if e.Stdin == nil { e.Stdin = os.Stdin @@ -206,7 +105,7 @@ func (e *Executor) setupCompiler() error { e.Compiler = &Compiler{ Dir: e.Dir, - Entrypoint: e.Entrypoint, + Entrypoint: e.Taskfile.Location, UserWorkingDir: e.UserWorkingDir, TaskfileEnv: e.Taskfile.Env, TaskfileVars: e.Taskfile.Vars, diff --git a/executor_test.go b/executor_test.go index 0e4d667cd0..7522984112 100644 --- a/executor_test.go +++ b/executor_test.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "testing" "github.com/sebdah/goldie/v2" @@ -15,6 +16,7 @@ import ( "github.com/go-task/task/v3" "github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/internal/filepathext" + "github.com/go-task/task/v3/taskfile" "github.com/go-task/task/v3/taskfile/ast" ) @@ -34,7 +36,12 @@ type ( task string vars map[string]any input string + nodeDir string + nodeEntrypoint string + nodeInsecure bool + readerOpts []taskfile.ReaderOption executorOpts []task.ExecutorOption + wantReaderError bool wantSetupError bool wantRunError bool wantStatusError bool @@ -47,8 +54,9 @@ type ( func NewExecutorTest(t *testing.T, opts ...ExecutorTestOption) { t.Helper() tt := &ExecutorTest{ - task: "default", - vars: map[string]any{}, + task: "default", + vars: map[string]any{}, + nodeDir: ".", TaskTest: TaskTest{ experiments: map[*experiments.Experiment]int{}, fixtureTemplateData: map[string]any{}, @@ -145,11 +153,52 @@ func (tt *ExecutorTest) run(t *testing.T) { f := func(t *testing.T) { t.Helper() var buf bytes.Buffer + ctx := context.Background() + + // Create a new root node for the given entrypoint + node, err := taskfile.NewRootNode( + tt.nodeEntrypoint, + tt.nodeDir, + tt.nodeInsecure, + ) + require.NoError(t, err) + + // Create a golden fixture file for the output + g := goldie.New(t, + goldie.WithFixtureDir(filepath.Join(node.Dir(), "testdata")), + ) + + // Set up a temporary directory for the taskfile reader and task executor + tempDir, err := task.NewTempDir(node.Dir()) + require.NoError(t, err) + tt.readerOpts = append(tt.readerOpts, taskfile.WithTempDir(tempDir.Remote)) + + // Set up the taskfile reader + reader := taskfile.NewReader(tt.readerOpts...) + graph, err := reader.Read(ctx, node) + if tt.wantReaderError { + require.Error(t, err) + tt.writeFixtureErrReader(t, g, err) + tt.writeFixtureBuffer(t, g, buf) + return + } else { + require.NoError(t, err) + } - opts := append( + executorOpts := slices.Concat( + // Apply the node directory and temp directory to the executor options + // by default, but allow them to by overridden by the test options + []task.ExecutorOption{ + task.WithDir(node.Dir()), + task.WithTempDir(tempDir), + }, + // Apply the executor options from the test tt.executorOpts, - task.WithStdout(&buf), - task.WithStderr(&buf), + // Force the input/output streams to be set to the test buffer + []task.ExecutorOption{ + task.WithStdout(&buf), + task.WithStderr(&buf), + }, ) // If the test has input, create a reader for it and add it to the @@ -157,19 +206,12 @@ func (tt *ExecutorTest) run(t *testing.T) { if tt.input != "" { var reader bytes.Buffer reader.WriteString(tt.input) - opts = append(opts, task.WithStdin(&reader)) + executorOpts = append(executorOpts, task.WithStdin(&reader)) } // Set up the task executor - e := task.NewExecutor(opts...) - - // Create a golden fixture file for the output - g := goldie.New(t, - goldie.WithFixtureDir(filepath.Join(e.Dir, "testdata")), - ) - - // Call setup and check for errors - if err := e.Setup(); tt.wantSetupError { + executor, err := task.NewExecutor(graph, executorOpts...) + if tt.wantSetupError { require.Error(t, err) tt.writeFixtureErrSetup(t, g, err) tt.writeFixtureBuffer(t, g, buf) @@ -189,8 +231,7 @@ func (tt *ExecutorTest) run(t *testing.T) { } // Run the task and check for errors - ctx := context.Background() - if err := e.Run(ctx, call); tt.wantRunError { + if err := executor.Run(ctx, call); tt.wantRunError { require.Error(t, err) tt.writeFixtureErrRun(t, g, err) tt.writeFixtureBuffer(t, g, buf) @@ -201,7 +242,7 @@ func (tt *ExecutorTest) run(t *testing.T) { // If the status flag is set, run the status check if tt.wantStatusError { - if err := e.Status(ctx, call); err != nil { + if err := executor.Status(ctx, call); err != nil { tt.writeFixtureStatus(t, g, err.Error()) } } @@ -220,19 +261,16 @@ func (tt *ExecutorTest) run(t *testing.T) { func TestEmptyTask(t *testing.T) { t.Parallel() NewExecutorTest(t, - WithExecutorOptions( - task.WithDir("testdata/empty_task"), - ), + WithNodeDir("testdata/empty_task"), + WithExecutorOptions(), ) } func TestEmptyTaskfile(t *testing.T) { t.Parallel() NewExecutorTest(t, - WithExecutorOptions( - task.WithDir("testdata/empty_taskfile"), - ), - WithSetupError(), + WithNodeDir("testdata/empty_taskfile"), + WithReaderError(), WithFixtureTemplating(), ) } @@ -241,15 +279,15 @@ func TestEnv(t *testing.T) { t.Setenv("QUX", "from_os") NewExecutorTest(t, WithName("env precedence disabled"), + WithNodeDir("testdata/env"), WithExecutorOptions( - task.WithDir("testdata/env"), task.WithSilent(true), ), ) NewExecutorTest(t, WithName("env precedence enabled"), + WithNodeDir("testdata/env"), WithExecutorOptions( - task.WithDir("testdata/env"), task.WithSilent(true), ), WithExperiment(&experiments.EnvPrecedence, 1), @@ -259,8 +297,8 @@ func TestEnv(t *testing.T) { func TestVars(t *testing.T) { t.Parallel() NewExecutorTest(t, + WithNodeDir("testdata/vars"), WithExecutorOptions( - task.WithDir("testdata/vars"), task.WithSilent(true), ), ) @@ -270,25 +308,19 @@ func TestRequires(t *testing.T) { t.Parallel() NewExecutorTest(t, WithName("required var missing"), - WithExecutorOptions( - task.WithDir("testdata/requires"), - ), + WithNodeDir("testdata/requires"), WithTask("missing-var"), WithRunError(), ) NewExecutorTest(t, WithName("required var ok"), - WithExecutorOptions( - task.WithDir("testdata/requires"), - ), + WithNodeDir("testdata/requires"), WithTask("missing-var"), WithVar("FOO", "bar"), ) NewExecutorTest(t, WithName("fails validation"), - WithExecutorOptions( - task.WithDir("testdata/requires"), - ), + WithNodeDir("testdata/requires"), WithTask("validation-var"), WithVar("ENV", "dev"), WithVar("FOO", "bar"), @@ -296,48 +328,37 @@ func TestRequires(t *testing.T) { ) NewExecutorTest(t, WithName("passes validation"), - WithExecutorOptions( - task.WithDir("testdata/requires"), - ), + WithNodeDir("testdata/requires"), WithTask("validation-var"), WithVar("FOO", "one"), WithVar("ENV", "dev"), ) NewExecutorTest(t, WithName("required var missing + fails validation"), - WithExecutorOptions( - task.WithDir("testdata/requires"), - ), + WithNodeDir("testdata/requires"), WithTask("validation-var"), WithRunError(), ) NewExecutorTest(t, WithName("required var missing + fails validation"), - WithExecutorOptions( - task.WithDir("testdata/requires"), - ), + WithNodeDir("testdata/requires"), WithTask("validation-var-dynamic"), WithVar("FOO", "one"), WithVar("ENV", "dev"), ) NewExecutorTest(t, WithName("require before compile"), - WithExecutorOptions( - task.WithDir("testdata/requires"), - ), + WithNodeDir("testdata/requires"), WithTask("require-before-compile"), WithRunError(), ) NewExecutorTest(t, WithName("var defined in task"), - WithExecutorOptions( - task.WithDir("testdata/requires"), - ), + WithNodeDir("testdata/requires"), WithTask("var-defined-in-task"), ) } -// TODO: mock fs func TestSpecialVars(t *testing.T) { t.Parallel() @@ -358,12 +379,13 @@ func TestSpecialVars(t *testing.T) { "included:print-taskfile-dir", } - for _, dir := range []string{dir, subdir} { + for _, executorDir := range []string{dir, subdir} { for _, test := range tests { + name := fmt.Sprintf("%s-%s", executorDir, test) NewExecutorTest(t, - WithName(fmt.Sprintf("%s-%s", dir, test)), + WithName(name), + WithNodeDir(executorDir), WithExecutorOptions( - task.WithDir(dir), task.WithSilent(true), task.WithVersionCheck(true), ), @@ -377,8 +399,8 @@ func TestSpecialVars(t *testing.T) { func TestConcurrency(t *testing.T) { t.Parallel() NewExecutorTest(t, + WithNodeDir("testdata/concurrency"), WithExecutorOptions( - task.WithDir("testdata/concurrency"), task.WithConcurrency(1), ), WithPostProcessFn(PPSortedLines), @@ -388,8 +410,8 @@ func TestConcurrency(t *testing.T) { func TestParams(t *testing.T) { t.Parallel() NewExecutorTest(t, + WithNodeDir("testdata/params"), WithExecutorOptions( - task.WithDir("testdata/params"), task.WithSilent(true), ), WithPostProcessFn(PPSortedLines), @@ -399,15 +421,14 @@ func TestParams(t *testing.T) { func TestDeps(t *testing.T) { t.Parallel() NewExecutorTest(t, + WithNodeDir("testdata/deps"), WithExecutorOptions( - task.WithDir("testdata/deps"), task.WithSilent(true), ), WithPostProcessFn(PPSortedLines), ) } -// TODO: mock fs func TestStatus(t *testing.T) { t.Parallel() @@ -430,8 +451,8 @@ func TestStatus(t *testing.T) { // gen-foo creates foo.txt, and will always fail it's status check. NewExecutorTest(t, WithName("run gen-foo 1 silent"), + WithNodeDir(dir), WithExecutorOptions( - task.WithDir(dir), task.WithSilent(true), ), WithTask("gen-foo"), @@ -442,8 +463,8 @@ func TestStatus(t *testing.T) { // only exists after the first run. NewExecutorTest(t, WithName("run gen-bar 1 silent"), + WithNodeDir(dir), WithExecutorOptions( - task.WithDir(dir), task.WithSilent(true), ), WithTask("gen-bar"), @@ -452,8 +473,8 @@ func TestStatus(t *testing.T) { // if e.Verbose is set to true. NewExecutorTest(t, WithName("run gen-baz silent"), + WithNodeDir(dir), WithExecutorOptions( - task.WithDir(dir), task.WithSilent(true), ), WithTask("gen-silent-baz"), @@ -468,8 +489,8 @@ func TestStatus(t *testing.T) { // Run gen-bar a second time to produce a checksum file that matches bar.txt NewExecutorTest(t, WithName("run gen-bar 2 silent"), + WithNodeDir(dir), WithExecutorOptions( - task.WithDir(dir), task.WithSilent(true), ), WithTask("gen-bar"), @@ -477,8 +498,8 @@ func TestStatus(t *testing.T) { // Run gen-bar a third time, to make sure we've triggered the status check. NewExecutorTest(t, WithName("run gen-bar 3 silent"), + WithNodeDir(dir), WithExecutorOptions( - task.WithDir(dir), task.WithSilent(true), ), WithTask("gen-bar"), @@ -490,8 +511,8 @@ func TestStatus(t *testing.T) { require.NoError(t, err) NewExecutorTest(t, WithName("run gen-bar 4 silent"), + WithNodeDir(dir), WithExecutorOptions( - task.WithDir(dir), task.WithSilent(true), ), WithTask("gen-bar"), @@ -499,56 +520,44 @@ func TestStatus(t *testing.T) { // all: not up-to-date NewExecutorTest(t, WithName("run gen-foo 2"), - WithExecutorOptions( - task.WithDir(dir), - ), + WithNodeDir(dir), WithTask("gen-foo"), ) // status: not up-to-date NewExecutorTest(t, WithName("run gen-foo 3"), - WithExecutorOptions( - task.WithDir(dir), - ), + WithNodeDir(dir), WithTask("gen-foo"), ) // sources: not up-to-date NewExecutorTest(t, WithName("run gen-bar 5"), - WithExecutorOptions( - task.WithDir(dir), - ), + WithNodeDir(dir), WithTask("gen-bar"), ) // all: up-to-date NewExecutorTest(t, WithName("run gen-bar 6"), - WithExecutorOptions( - task.WithDir(dir), - ), + WithNodeDir(dir), WithTask("gen-bar"), ) // sources: not up-to-date, no output produced. NewExecutorTest(t, WithName("run gen-baz 2"), - WithExecutorOptions( - task.WithDir(dir), - ), + WithNodeDir(dir), WithTask("gen-silent-baz"), ) // up-to-date, no output produced NewExecutorTest(t, WithName("run gen-baz 3"), - WithExecutorOptions( - task.WithDir(dir), - ), + WithNodeDir(dir), WithTask("gen-silent-baz"), ) // up-to-date, output produced due to Verbose mode. NewExecutorTest(t, WithName("run gen-baz 4 verbose"), + WithNodeDir(dir), WithExecutorOptions( - task.WithDir(dir), task.WithVerbose(true), ), WithTask("gen-silent-baz"), @@ -561,32 +570,24 @@ func TestPrecondition(t *testing.T) { const dir = "testdata/precondition" NewExecutorTest(t, WithName("a precondition has been met"), - WithExecutorOptions( - task.WithDir(dir), - ), + WithNodeDir(dir), WithTask("foo"), ) NewExecutorTest(t, WithName("a precondition was not met"), - WithExecutorOptions( - task.WithDir(dir), - ), + WithNodeDir(dir), WithTask("impossible"), WithRunError(), ) NewExecutorTest(t, WithName("precondition in dependency fails the task"), - WithExecutorOptions( - task.WithDir(dir), - ), + WithNodeDir(dir), WithTask("depends_on_impossible"), WithRunError(), ) NewExecutorTest(t, WithName("precondition in cmd fails the task"), - WithExecutorOptions( - task.WithDir(dir), - ), + WithNodeDir(dir), WithTask("executes_failing_task_as_cmd"), WithRunError(), ) @@ -597,25 +598,21 @@ func TestAlias(t *testing.T) { NewExecutorTest(t, WithName("alias"), - WithExecutorOptions( - task.WithDir("testdata/alias"), - ), + WithNodeDir("testdata/alias"), WithTask("f"), ) NewExecutorTest(t, WithName("duplicate alias"), - WithExecutorOptions( - task.WithDir("testdata/alias"), - ), + WithNodeDir("testdata/alias"), WithTask("x"), WithRunError(), ) NewExecutorTest(t, WithName("alias summary"), + WithNodeDir("testdata/alias"), WithExecutorOptions( - task.WithDir("testdata/alias"), task.WithSummary(true), ), WithTask("f"), @@ -627,16 +624,14 @@ func TestLabel(t *testing.T) { NewExecutorTest(t, WithName("up to date"), - WithExecutorOptions( - task.WithDir("testdata/label_uptodate"), - ), + WithNodeDir("testdata/label_uptodate"), WithTask("foo"), ) NewExecutorTest(t, WithName("summary"), + WithNodeDir("testdata/label_summary"), WithExecutorOptions( - task.WithDir("testdata/label_summary"), task.WithSummary(true), ), WithTask("foo"), @@ -644,26 +639,20 @@ func TestLabel(t *testing.T) { NewExecutorTest(t, WithName("status"), - WithExecutorOptions( - task.WithDir("testdata/label_status"), - ), + WithNodeDir("testdata/label_status"), WithTask("foo"), WithStatusError(), ) NewExecutorTest(t, WithName("var"), - WithExecutorOptions( - task.WithDir("testdata/label_var"), - ), + WithNodeDir("testdata/label_var"), WithTask("foo"), ) NewExecutorTest(t, WithName("label in summary"), - WithExecutorOptions( - task.WithDir("testdata/label_summary"), - ), + WithNodeDir("testdata/label_summary"), WithTask("foo"), ) } @@ -690,8 +679,8 @@ func TestPromptInSummary(t *testing.T) { opts := []ExecutorTestOption{ WithName(test.name), + WithNodeDir("testdata/prompt"), WithExecutorOptions( - task.WithDir("testdata/prompt"), task.WithAssumeTerm(true), ), WithTask("foo"), @@ -709,8 +698,8 @@ func TestPromptWithIndirectTask(t *testing.T) { t.Parallel() NewExecutorTest(t, + WithNodeDir("testdata/prompt"), WithExecutorOptions( - task.WithDir("testdata/prompt"), task.WithAssumeTerm(true), ), WithTask("bar"), @@ -723,8 +712,8 @@ func TestPromptAssumeYes(t *testing.T) { NewExecutorTest(t, WithName("--yes flag should skip prompt"), + WithNodeDir("testdata/prompt"), WithExecutorOptions( - task.WithDir("testdata/prompt"), task.WithAssumeTerm(true), task.WithAssumeYes(true), ), @@ -734,8 +723,8 @@ func TestPromptAssumeYes(t *testing.T) { NewExecutorTest(t, WithName("task should raise errors.TaskCancelledError"), + WithNodeDir("testdata/prompt"), WithExecutorOptions( - task.WithDir("testdata/prompt"), task.WithAssumeTerm(true), ), WithTask("foo"), @@ -772,8 +761,8 @@ func TestForCmds(t *testing.T) { for _, test := range tests { opts := []ExecutorTestOption{ WithName(test.name), + WithNodeDir("testdata/for/cmds"), WithExecutorOptions( - task.WithDir("testdata/for/cmds"), task.WithSilent(true), task.WithForce(true), ), @@ -815,8 +804,8 @@ func TestForDeps(t *testing.T) { for _, test := range tests { opts := []ExecutorTestOption{ WithName(test.name), + WithNodeDir("testdata/for/deps"), WithExecutorOptions( - task.WithDir("testdata/for/deps"), task.WithSilent(true), task.WithForce(true), // Force output of each dep to be grouped together to prevent interleaving @@ -861,8 +850,8 @@ func TestReference(t *testing.T) { for _, test := range tests { NewExecutorTest(t, WithName(test.name), + WithNodeDir("testdata/var_references"), WithExecutorOptions( - task.WithDir("testdata/var_references"), task.WithSilent(true), task.WithForce(true), ), @@ -929,8 +918,8 @@ func TestVarInheritance(t *testing.T) { for _, test := range tests { NewExecutorTest(t, WithName(test.name), + WithNodeDir(fmt.Sprintf("testdata/var_inheritance/v3/%s", test.name)), WithExecutorOptions( - task.WithDir(fmt.Sprintf("testdata/var_inheritance/v3/%s", test.name)), task.WithSilent(true), task.WithForce(true), ), @@ -944,26 +933,20 @@ func TestFuzzyModel(t *testing.T) { NewExecutorTest(t, WithName("fuzzy"), - WithExecutorOptions( - task.WithDir("testdata/fuzzy"), - ), + WithNodeDir("testdata/fuzzy"), WithTask("instal"), WithRunError(), ) NewExecutorTest(t, WithName("not-fuzzy"), - WithExecutorOptions( - task.WithDir("testdata/fuzzy"), - ), + WithNodeDir("testdata/fuzzy"), WithTask("install"), ) NewExecutorTest(t, WithName("intern"), - WithExecutorOptions( - task.WithDir("testdata/fuzzy"), - ), + WithNodeDir("testdata/fuzzy"), WithTask("intern"), WithRunError(), ) @@ -974,17 +957,65 @@ func TestIncludeChecksum(t *testing.T) { NewExecutorTest(t, WithName("correct"), - WithExecutorOptions( - task.WithDir("testdata/includes_checksum/correct"), - ), + WithNodeDir("testdata/includes_checksum/correct"), ) NewExecutorTest(t, WithName("incorrect"), - WithExecutorOptions( - task.WithDir("testdata/includes_checksum/incorrect"), - ), - WithSetupError(), + WithNodeDir("testdata/includes_checksum/incorrect"), + WithReaderError(), WithFixtureTemplating(), ) } + +func TestWildcard(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + call string + wantErr bool + }{ + { + name: "basic wildcard", + call: "wildcard-foo", + }, + { + name: "double wildcard", + call: "foo-wildcard-bar", + }, + { + name: "store wildcard", + call: "start-foo", + }, + { + name: "matches exactly", + call: "matches-exactly-*", + }, + { + name: "no matches", + call: "no-match", + wantErr: true, + }, + { + name: "multiple matches", + call: "wildcard-foo-bar", + }, + } + + for _, test := range tests { + opts := []ExecutorTestOption{ + WithName(test.name), + WithNodeDir("testdata/wildcards"), + WithExecutorOptions( + task.WithSilent(true), + task.WithForce(true), + ), + WithTask(test.call), + } + if test.wantErr { + opts = append(opts, WithRunError()) + } + NewExecutorTest(t, opts...) + } +} diff --git a/formatter_test.go b/formatter_test.go index 7221ff769e..acfdf9544f 100644 --- a/formatter_test.go +++ b/formatter_test.go @@ -2,7 +2,9 @@ package task_test import ( "bytes" + "context" "path/filepath" + "slices" "testing" "github.com/sebdah/goldie/v2" @@ -10,6 +12,7 @@ import ( "github.com/go-task/task/v3" "github.com/go-task/task/v3/experiments" + "github.com/go-task/task/v3/taskfile" "github.com/go-task/task/v3/taskfile/ast" ) @@ -26,12 +29,17 @@ type ( // running `task gen:fixtures`. FormatterTest struct { TaskTest - task string - vars map[string]any - executorOpts []task.ExecutorOption - listOptions task.ListOptions - wantSetupError bool - wantListError bool + task string + vars map[string]any + nodeDir string + nodeEntrypoint string + nodeInsecure bool + readerOpts []taskfile.ReaderOption + executorOpts []task.ExecutorOption + listOptions task.ListOptions + wantReaderError bool + wantSetupError bool + wantListError bool } ) @@ -41,8 +49,9 @@ type ( func NewFormatterTest(t *testing.T, opts ...FormatterTestOption) { t.Helper() tt := &FormatterTest{ - task: "default", - vars: map[string]any{}, + task: "default", + vars: map[string]any{}, + nodeDir: ".", TaskTest: TaskTest{ experiments: map[*experiments.Experiment]int{}, fixtureTemplateData: map[string]any{}, @@ -114,23 +123,57 @@ func (tt *FormatterTest) run(t *testing.T) { f := func(t *testing.T) { t.Helper() var buf bytes.Buffer + ctx := context.Background() - opts := append( - tt.executorOpts, - task.WithStdout(&buf), - task.WithStderr(&buf), + // Create a new root node for the given entrypoint + node, err := taskfile.NewRootNode( + tt.nodeEntrypoint, + tt.nodeDir, + tt.nodeInsecure, ) - - // Set up the task executor - e := task.NewExecutor(opts...) + require.NoError(t, err) // Create a golden fixture file for the output g := goldie.New(t, - goldie.WithFixtureDir(filepath.Join(e.Dir, "testdata")), + goldie.WithFixtureDir(filepath.Join(node.Dir(), "testdata")), ) - // Call setup and check for errors - if err := e.Setup(); tt.wantSetupError { + // Set up a temporary directory for the taskfile reader and task executor + tempDir, err := task.NewTempDir(node.Dir()) + require.NoError(t, err) + tt.readerOpts = append(tt.readerOpts, taskfile.WithTempDir(tempDir.Remote)) + + // Set up the taskfile reader + reader := taskfile.NewReader(tt.readerOpts...) + graph, err := reader.Read(ctx, node) + if tt.wantReaderError { + require.Error(t, err) + tt.writeFixtureErrReader(t, g, err) + tt.writeFixtureBuffer(t, g, buf) + return + } else { + require.NoError(t, err) + } + + executorOpts := slices.Concat( + // Apply the node directory and temp directory to the executor options + // by default, but allow them to by overridden by the test options + []task.ExecutorOption{ + task.WithDir(node.Dir()), + task.WithTempDir(tempDir), + }, + // Apply the executor options from the test + tt.executorOpts, + // Force the input/output streams to be set to the test buffer + []task.ExecutorOption{ + task.WithStdout(&buf), + task.WithStderr(&buf), + }, + ) + + // Set up the task executor + executor, err := task.NewExecutor(graph, executorOpts...) + if tt.wantSetupError { require.Error(t, err) tt.writeFixtureErrSetup(t, g, err) tt.writeFixtureBuffer(t, g, buf) @@ -146,7 +189,7 @@ func (tt *FormatterTest) run(t *testing.T) { } // Run the formatter and check for errors - if _, err := e.ListTasks(tt.listOptions); tt.wantListError { + if _, err := executor.ListTasks(tt.listOptions); tt.wantListError { require.Error(t, err) tt.writeFixtureErrList(t, g, err) tt.writeFixtureBuffer(t, g, buf) @@ -170,9 +213,7 @@ func TestNoLabelInList(t *testing.T) { t.Parallel() NewFormatterTest(t, - WithExecutorOptions( - task.WithDir("testdata/label_list"), - ), + WithNodeDir("testdata/label_list"), WithListOptions(task.ListOptions{ ListOnlyTasksWithDescriptions: true, }), @@ -184,9 +225,7 @@ func TestListAllShowsNoDesc(t *testing.T) { t.Parallel() NewFormatterTest(t, - WithExecutorOptions( - task.WithDir("testdata/list_mixed_desc"), - ), + WithNodeDir("testdata/list_mixed_desc"), WithListOptions(task.ListOptions{ ListAllTasks: true, }), @@ -198,9 +237,7 @@ func TestListCanListDescOnly(t *testing.T) { t.Parallel() NewFormatterTest(t, - WithExecutorOptions( - task.WithDir("testdata/list_mixed_desc"), - ), + WithNodeDir("testdata/list_mixed_desc"), WithListOptions(task.ListOptions{ ListOnlyTasksWithDescriptions: true, }), @@ -211,9 +248,7 @@ func TestListDescInterpolation(t *testing.T) { t.Parallel() NewFormatterTest(t, - WithExecutorOptions( - task.WithDir("testdata/list_desc_interpolation"), - ), + WithNodeDir("testdata/list_desc_interpolation"), WithListOptions(task.ListOptions{ ListOnlyTasksWithDescriptions: true, }), @@ -224,9 +259,7 @@ func TestJsonListFormat(t *testing.T) { t.Parallel() NewFormatterTest(t, - WithExecutorOptions( - task.WithDir("testdata/json_list_format"), - ), + WithNodeDir("testdata/json_list_format"), WithListOptions(task.ListOptions{ FormatTaskListAsJSON: true, }), diff --git a/internal/flags/flags.go b/internal/flags/flags.go index dab9fdf8f4..c3611558c5 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -15,6 +15,7 @@ import ( "github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/sort" + "github.com/go-task/task/v3/taskfile" "github.com/go-task/task/v3/taskfile/ast" ) @@ -201,7 +202,7 @@ func Validate() error { // WithFlags is a special internal functional option that is used to pass flags // from the CLI into any constructor that accepts functional options. -func WithFlags() task.ExecutorOption { +func WithFlags() *flagsOption { return &flagsOption{} } @@ -228,14 +229,8 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) { e.Options( task.WithDir(dir), - task.WithEntrypoint(Entrypoint), task.WithForce(Force), task.WithForceAll(ForceAll), - task.WithInsecure(Insecure), - task.WithDownload(Download), - task.WithOffline(Offline), - task.WithTimeout(Timeout), - task.WithCacheExpiryDuration(CacheExpiryDuration), task.WithWatch(Watch), task.WithVerbose(Verbose), task.WithSilent(Silent), @@ -251,3 +246,12 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) { task.WithVersionCheck(true), ) } + +func (o *flagsOption) ApplyToReader(r *taskfile.Reader) { + r.Options( + taskfile.WithInsecure(Insecure), + taskfile.WithDownload(Download), + taskfile.WithOffline(Offline), + taskfile.WithCacheExpiryDuration(CacheExpiryDuration), + ) +} diff --git a/task_test.go b/task_test.go index 927ff29fbd..ed3ea1f96b 100644 --- a/task_test.go +++ b/task_test.go @@ -5,32 +5,22 @@ import ( "context" "fmt" "io" - "io/fs" "maps" - rand "math/rand/v2" - "net/http" - "net/http/httptest" "os" - "path/filepath" - "regexp" - "runtime" "slices" "sort" "strings" "sync" "testing" - "time" - "github.com/Masterminds/semver/v3" "github.com/sebdah/goldie/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/go-task/task/v3" - "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/internal/filepathext" - "github.com/go-task/task/v3/taskfile/ast" + "github.com/go-task/task/v3/taskfile" ) func init() { @@ -111,6 +101,17 @@ func (tt *TaskTest) writeFixtureBuffer( tt.writeFixture(t, g, "", buff.Bytes()) } +// writeFixtureErrSetup is a wrapper for writing the output of an error during +// the setup phase of the task to a fixture file. +func (tt *TaskTest) writeFixtureErrReader( + t *testing.T, + g *goldie.Goldie, + err error, +) { + t.Helper() + tt.writeFixture(t, g, "err-reader", []byte(err.Error())) +} + // writeFixtureErrSetup is a wrapper for writing the output of an error during // the setup phase of the task to a fixture file. func (tt *TaskTest) writeFixtureErrSetup( @@ -142,6 +143,57 @@ func (opt *nameTestOption) applyToFormatterTest(t *FormatterTest) { t.name = opt.name } +// WithNodeDir sets the directory to be used for the test node. +func WithNodeDir(dir string) TestOption { + return &nodeDirTestOption{dir: dir} +} + +type nodeDirTestOption struct { + dir string +} + +func (opt *nodeDirTestOption) applyToExecutorTest(t *ExecutorTest) { + t.nodeDir = opt.dir +} + +func (opt *nodeDirTestOption) applyToFormatterTest(t *FormatterTest) { + t.nodeDir = opt.dir +} + +// WithNodeEntrypoint sets the entrypoint to be used for the test node. +func WithNodeEntrypoint(entrypoint string) TestOption { + return &nodeEntrypointTestOption{entrypoint: entrypoint} +} + +type nodeEntrypointTestOption struct { + entrypoint string +} + +func (opt *nodeEntrypointTestOption) applyToExecutorTest(t *ExecutorTest) { + t.nodeEntrypoint = opt.entrypoint +} + +func (opt *nodeEntrypointTestOption) applyToFormatterTest(t *FormatterTest) { + t.nodeEntrypoint = opt.entrypoint +} + +// WithNodeInsecure sets the insecure flag to be used for the test node. +func WithNodeInsecure(insecure bool) TestOption { + return &nodeInsecureTestOption{insecure: insecure} +} + +type nodeInsecureTestOption struct { + insecure bool +} + +func (opt *nodeInsecureTestOption) applyToExecutorTest(t *ExecutorTest) { + t.nodeInsecure = opt.insecure +} + +func (opt *nodeInsecureTestOption) applyToFormatterTest(t *FormatterTest) { + t.nodeInsecure = opt.insecure +} + // WithTask sets the name of the task to run. This should be used when the task // to run is not the default task. func WithTask(task string) TestOption { @@ -236,6 +288,22 @@ func (opt *postProcessFnTestOption) applyToFormatterTest(t *FormatterTest) { t.postProcessFns = append(t.postProcessFns, opt.fn) } +// WithReaderError sets the test to expect an error during the reader phase of the +// task execution. A fixture will be created with the output of any errors. +func WithReaderError() TestOption { + return &readerErrorTestOption{} +} + +type readerErrorTestOption struct{} + +func (opt *readerErrorTestOption) applyToExecutorTest(t *ExecutorTest) { + t.wantReaderError = true +} + +func (opt *readerErrorTestOption) applyToFormatterTest(t *FormatterTest) { + t.wantReaderError = true +} + // WithSetupError sets the test to expect an error during the setup phase of the // task execution. A fixture will be created with the output of any errors. func WithSetupError() TestOption { @@ -337,6 +405,38 @@ func (fct fileContentTest) name(file string) string { return fmt.Sprintf("target=%q,file=%q", fct.Target, file) } +func deprecatedExecutor( + t *testing.T, + entrypoint string, + dir string, + insecure bool, + readerOpts []taskfile.ReaderOption, + opts ...task.ExecutorOption, +) (*task.Executor, error) { + ctx := context.Background() + + // Create a new root node for the given entrypoint + node, err := taskfile.NewRootNode( + entrypoint, + dir, + insecure, + ) + require.NoError(t, err) + + // Set up a temporary directory for the taskfile reader and task executor + tempDir, err := task.NewTempDir(node.Dir()) + require.NoError(t, err) + + // Set up the taskfile reader + readerOpts = append(readerOpts, taskfile.WithTempDir(tempDir.Remote)) + reader := taskfile.NewReader(readerOpts...) + graph, err := reader.Read(ctx, node) + require.NoError(t, err) + + // Set up the task executor + return task.NewExecutor(graph, opts...) +} + func (fct fileContentTest) Run(t *testing.T) { t.Helper() @@ -344,18 +444,21 @@ func (fct fileContentTest) Run(t *testing.T) { _ = os.Remove(filepathext.SmartJoin(fct.Dir, f)) } - e := task.NewExecutor( + e, err := deprecatedExecutor(t, + fct.Entrypoint, + fct.Dir, + false, + []taskfile.ReaderOption{}, task.WithDir(fct.Dir), - task.WithTempDir(task.TempDir{ + task.WithTempDir(&task.TempDir{ Remote: filepathext.SmartJoin(fct.Dir, ".task"), Fingerprint: filepathext.SmartJoin(fct.Dir, ".task"), }), - task.WithEntrypoint(fct.Entrypoint), task.WithStdout(io.Discard), task.WithStderr(io.Discard), ) - require.NoError(t, e.Setup(), "e.Setup()") + require.NoError(t, err, "e.Setup()") require.NoError(t, e.Run(context.Background(), &task.Call{Task: fct.Target}), "e.Run(target)") for name, expectContent := range fct.Files { t.Run(fct.name(name), func(t *testing.T) { @@ -371,2222 +474,2158 @@ func (fct fileContentTest) Run(t *testing.T) { } } -func TestGenerates(t *testing.T) { - t.Parallel() - - const dir = "testdata/generates" - - const ( - srcTask = "sub/src.txt" - relTask = "rel.txt" - absTask = "abs.txt" - fileWithSpaces = "my text file.txt" - ) - - srcFile := filepathext.SmartJoin(dir, srcTask) - - for _, task := range []string{srcTask, relTask, absTask, fileWithSpaces} { - path := filepathext.SmartJoin(dir, task) - _ = os.Remove(path) - if _, err := os.Stat(path); err == nil { - t.Errorf("File should not exist: %v", err) - } - } - - buff := bytes.NewBuffer(nil) - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(buff), - task.WithStderr(buff), - ) - require.NoError(t, e.Setup()) - - for _, theTask := range []string{relTask, absTask, fileWithSpaces} { - destFile := filepathext.SmartJoin(dir, theTask) - upToDate := fmt.Sprintf("task: Task \"%s\" is up to date\n", srcTask) + - fmt.Sprintf("task: Task \"%s\" is up to date\n", theTask) - - // Run task for the first time. - require.NoError(t, e.Run(context.Background(), &task.Call{Task: theTask})) - - if _, err := os.Stat(srcFile); err != nil { - t.Errorf("File should exist: %v", err) - } - if _, err := os.Stat(destFile); err != nil { - t.Errorf("File should exist: %v", err) - } - // Ensure task was not incorrectly found to be up-to-date on first run. - if buff.String() == upToDate { - t.Errorf("Wrong output message: %s", buff.String()) - } - buff.Reset() - - // Re-run task to ensure it's now found to be up-to-date. - require.NoError(t, e.Run(context.Background(), &task.Call{Task: theTask})) - if buff.String() != upToDate { - t.Errorf("Wrong output message: %s", buff.String()) - } - buff.Reset() - } -} - -func TestStatusChecksum(t *testing.T) { // nolint:paralleltest // cannot run in parallel - const dir = "testdata/checksum" - - tests := []struct { - files []string - task string - }{ - {[]string{"generated.txt", ".task/checksum/build"}, "build"}, - {[]string{"generated.txt", ".task/checksum/build-with-status"}, "build-with-status"}, - } - - for _, test := range tests { // nolint:paralleltest // cannot run in parallel - t.Run(test.task, func(t *testing.T) { - for _, f := range test.files { - _ = os.Remove(filepathext.SmartJoin(dir, f)) - - _, err := os.Stat(filepathext.SmartJoin(dir, f)) - require.Error(t, err) - } - - var buff bytes.Buffer - tempDir := task.TempDir{ - Remote: filepathext.SmartJoin(dir, ".task"), - Fingerprint: filepathext.SmartJoin(dir, ".task"), - } - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithTempDir(tempDir), - ) - require.NoError(t, e.Setup()) - - require.NoError(t, e.Run(context.Background(), &task.Call{Task: test.task})) - for _, f := range test.files { - _, err := os.Stat(filepathext.SmartJoin(dir, f)) - require.NoError(t, err) - } - - // Capture the modification time, so we can ensure the checksum file - // is not regenerated when the hash hasn't changed. - s, err := os.Stat(filepathext.SmartJoin(tempDir.Fingerprint, "checksum/"+test.task)) - require.NoError(t, err) - time := s.ModTime() - - buff.Reset() - require.NoError(t, e.Run(context.Background(), &task.Call{Task: test.task})) - assert.Equal(t, `task: Task "`+test.task+`" is up to date`+"\n", buff.String()) - - s, err = os.Stat(filepathext.SmartJoin(tempDir.Fingerprint, "checksum/"+test.task)) - require.NoError(t, err) - assert.Equal(t, time, s.ModTime()) - }) - } -} - -func TestStatusVariables(t *testing.T) { - t.Parallel() - - const dir = "testdata/status_vars" - - _ = os.RemoveAll(filepathext.SmartJoin(dir, ".task")) - _ = os.Remove(filepathext.SmartJoin(dir, "generated.txt")) - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithTempDir(task.TempDir{ - Remote: filepathext.SmartJoin(dir, ".task"), - Fingerprint: filepathext.SmartJoin(dir, ".task"), - }), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithSilent(false), - task.WithVerbose(true), - ) - require.NoError(t, e.Setup()) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build-checksum"})) - - assert.Contains(t, buff.String(), "3e464c4b03f4b65d740e1e130d4d108a") - - buff.Reset() - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build-ts"})) - - inf, err := os.Stat(filepathext.SmartJoin(dir, "source.txt")) - require.NoError(t, err) - ts := fmt.Sprintf("%d", inf.ModTime().Unix()) - tf := inf.ModTime().String() - - assert.Contains(t, buff.String(), ts) - assert.Contains(t, buff.String(), tf) -} - -func TestCmdsVariables(t *testing.T) { - t.Parallel() - - const dir = "testdata/cmds_vars" - - _ = os.RemoveAll(filepathext.SmartJoin(dir, ".task")) - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithTempDir(task.TempDir{ - Remote: filepathext.SmartJoin(dir, ".task"), - Fingerprint: filepathext.SmartJoin(dir, ".task"), - }), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithSilent(false), - task.WithVerbose(true), - ) - require.NoError(t, e.Setup()) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build-checksum"})) - - assert.Contains(t, buff.String(), "3e464c4b03f4b65d740e1e130d4d108a") - - buff.Reset() - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build-ts"})) - inf, err := os.Stat(filepathext.SmartJoin(dir, "source.txt")) - require.NoError(t, err) - ts := fmt.Sprintf("%d", inf.ModTime().Unix()) - tf := inf.ModTime().String() - - assert.Contains(t, buff.String(), ts) - assert.Contains(t, buff.String(), tf) -} - -func TestCyclicDep(t *testing.T) { - t.Parallel() - - const dir = "testdata/cyclic" - - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(io.Discard), - task.WithStderr(io.Discard), - ) - require.NoError(t, e.Setup()) - assert.IsType(t, &errors.TaskCalledTooManyTimesError{}, e.Run(context.Background(), &task.Call{Task: "task-1"})) -} - -func TestTaskVersion(t *testing.T) { - t.Parallel() - - tests := []struct { - Dir string - Version *semver.Version - wantErr bool - }{ - {"testdata/version/v1", semver.MustParse("1"), true}, - {"testdata/version/v2", semver.MustParse("2"), true}, - {"testdata/version/v3", semver.MustParse("3"), false}, - } - - for _, test := range tests { - t.Run(test.Dir, func(t *testing.T) { - t.Parallel() - - e := task.NewExecutor( - task.WithDir(test.Dir), - task.WithStdout(io.Discard), - task.WithStderr(io.Discard), - task.WithVersionCheck(true), - ) - err := e.Setup() - if test.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - assert.Equal(t, test.Version, e.Taskfile.Version) - assert.Equal(t, 2, e.Taskfile.Tasks.Len()) - }) - } -} - -func TestTaskIgnoreErrors(t *testing.T) { - t.Parallel() - - const dir = "testdata/ignore_errors" - - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(io.Discard), - task.WithStderr(io.Discard), - ) - require.NoError(t, e.Setup()) - - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "task-should-pass"})) - require.Error(t, e.Run(context.Background(), &task.Call{Task: "task-should-fail"})) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "cmd-should-pass"})) - require.Error(t, e.Run(context.Background(), &task.Call{Task: "cmd-should-fail"})) -} - -func TestExpand(t *testing.T) { - t.Parallel() - - const dir = "testdata/expand" - - home, err := os.UserHomeDir() - if err != nil { - t.Errorf("Couldn't get $HOME: %v", err) - } - var buff bytes.Buffer - - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - require.NoError(t, e.Setup()) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "pwd"})) - assert.Equal(t, home, strings.TrimSpace(buff.String())) -} - -func TestDry(t *testing.T) { - t.Parallel() - - const dir = "testdata/dry" - - file := filepathext.SmartJoin(dir, "file.txt") - _ = os.Remove(file) - - var buff bytes.Buffer - - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithDry(true), - ) - require.NoError(t, e.Setup()) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build"})) - - assert.Equal(t, "task: [build] touch file.txt", strings.TrimSpace(buff.String())) - if _, err := os.Stat(file); err == nil { - t.Errorf("File should not exist %s", file) - } -} - -// TestDryChecksum tests if the checksum file is not being written to disk -// if the dry mode is enabled. -func TestDryChecksum(t *testing.T) { - t.Parallel() - - const dir = "testdata/dry_checksum" - - checksumFile := filepathext.SmartJoin(dir, ".task/checksum/default") - _ = os.Remove(checksumFile) - - e := task.NewExecutor( - task.WithDir(dir), - task.WithTempDir(task.TempDir{ - Remote: filepathext.SmartJoin(dir, ".task"), - Fingerprint: filepathext.SmartJoin(dir, ".task"), - }), - task.WithStdout(io.Discard), - task.WithStderr(io.Discard), - task.WithDry(true), - ) - require.NoError(t, e.Setup()) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"})) - - _, err := os.Stat(checksumFile) - require.Error(t, err, "checksum file should not exist") - - e.Dry = false - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"})) - _, err = os.Stat(checksumFile) - require.NoError(t, err, "checksum file should exist") -} - -func TestIncludes(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/includes", - Target: "default", - TrimSpace: true, - Files: map[string]string{ - "main.txt": "main", - "included_directory.txt": "included_directory", - "included_directory_without_dir.txt": "included_directory_without_dir", - "included_taskfile_without_dir.txt": "included_taskfile_without_dir", - "./module2/included_directory_with_dir.txt": "included_directory_with_dir", - "./module2/included_taskfile_with_dir.txt": "included_taskfile_with_dir", - "os_include.txt": "os", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestIncludesMultiLevel(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/includes_multi_level", - Target: "default", - TrimSpace: true, - Files: map[string]string{ - "called_one.txt": "one", - "called_two.txt": "two", - "called_three.txt": "three", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestIncludesRemote(t *testing.T) { - enableExperimentForTest(t, &experiments.RemoteTaskfiles, 1) - - dir := "testdata/includes_remote" - os.RemoveAll(filepath.Join(dir, ".task", "remote")) - - srv := httptest.NewServer(http.FileServer(http.Dir(dir))) - defer srv.Close() - - tcs := []struct { - firstRemote string - secondRemote string - }{ - { - firstRemote: srv.URL + "/first/Taskfile.yml", - secondRemote: srv.URL + "/first/second/Taskfile.yml", - }, - { - firstRemote: srv.URL + "/first/Taskfile.yml", - secondRemote: "./second/Taskfile.yml", - }, - { - firstRemote: srv.URL + "/first/", - secondRemote: srv.URL + "/first/second/", - }, - } - - taskCalls := []*task.Call{ - {Task: "first:write-file"}, - {Task: "first:second:write-file"}, - } - - for i, tc := range tcs { - t.Run(fmt.Sprint(i), func(t *testing.T) { - t.Setenv("FIRST_REMOTE_URL", tc.firstRemote) - t.Setenv("SECOND_REMOTE_URL", tc.secondRemote) - - var buff SyncBuffer - - executors := []struct { - name string - executor *task.Executor - }{ - { - name: "online, always download", - executor: task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithTimeout(time.Minute), - task.WithInsecure(true), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithVerbose(true), - - // Without caching - task.WithAssumeYes(true), - task.WithDownload(true), - ), - }, - { - name: "offline, use cache", - executor: task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithTimeout(time.Minute), - task.WithInsecure(true), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithVerbose(true), - - // With caching - task.WithAssumeYes(false), - task.WithDownload(false), - task.WithOffline(true), - ), - }, - } - - for _, e := range executors { - t.Run(e.name, func(t *testing.T) { - require.NoError(t, e.executor.Setup()) - - for k, taskCall := range taskCalls { - t.Run(taskCall.Task, func(t *testing.T) { - expectedContent := fmt.Sprint(rand.Int64()) - t.Setenv("CONTENT", expectedContent) - - outputFile := fmt.Sprintf("%d.%d.txt", i, k) - t.Setenv("OUTPUT_FILE", outputFile) - - path := filepath.Join(dir, outputFile) - require.NoError(t, os.RemoveAll(path)) - - require.NoError(t, e.executor.Run(context.Background(), taskCall)) - - actualContent, err := os.ReadFile(path) - require.NoError(t, err) - assert.Equal(t, expectedContent, strings.TrimSpace(string(actualContent))) - }) - } - }) - } - - t.Log("\noutput:\n", buff.buf.String()) - }) - } -} - -func TestIncludeCycle(t *testing.T) { - t.Parallel() - - const dir = "testdata/includes_cycle" - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithSilent(true), - ) - - err := e.Setup() - require.Error(t, err) - assert.Contains(t, err.Error(), "task: include cycle detected between") -} - -func TestIncludesIncorrect(t *testing.T) { - t.Parallel() - - const dir = "testdata/includes_incorrect" - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithSilent(true), - ) - - err := e.Setup() - require.Error(t, err) - assert.Contains(t, err.Error(), "Failed to parse testdata/includes_incorrect/incomplete.yml:", err.Error()) -} - -func TestIncludesEmptyMain(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/includes_empty", - Target: "included:default", - TrimSpace: true, - Files: map[string]string{ - "file.txt": "default", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestIncludesHttp(t *testing.T) { - enableExperimentForTest(t, &experiments.RemoteTaskfiles, 1) - - dir, err := filepath.Abs("testdata/includes_http") - require.NoError(t, err) - - srv := httptest.NewServer(http.FileServer(http.Dir(dir))) - defer srv.Close() - - t.Cleanup(func() { - // This test fills the .task/remote directory with cache entries because the include URL - // is different on every test due to the dynamic nature of the TCP port in srv.URL - if err := os.RemoveAll(filepath.Join(dir, ".task")); err != nil { - t.Logf("error cleaning up: %s", err) - } - }) - - taskfiles, err := fs.Glob(os.DirFS(dir), "root-taskfile-*.yml") - require.NoError(t, err) - - remotes := []struct { - name string - root string - }{ - { - name: "local", - root: ".", - }, - { - name: "http-remote", - root: srv.URL, - }, - } - - for _, taskfile := range taskfiles { - t.Run(taskfile, func(t *testing.T) { - for _, remote := range remotes { - t.Run(remote.name, func(t *testing.T) { - t.Setenv("INCLUDE_ROOT", remote.root) - entrypoint := filepath.Join(dir, taskfile) - - var buff SyncBuffer - e := task.NewExecutor( - task.WithEntrypoint(entrypoint), - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithInsecure(true), - task.WithDownload(true), - task.WithAssumeYes(true), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithVerbose(true), - task.WithTimeout(time.Minute), - ) - require.NoError(t, e.Setup()) - defer func() { t.Log("output:", buff.buf.String()) }() - - tcs := []struct { - name, dir string - }{ - { - name: "second-with-dir-1:third-with-dir-1:default", - dir: filepath.Join(dir, "dir-1"), - }, - { - name: "second-with-dir-1:third-with-dir-2:default", - dir: filepath.Join(dir, "dir-2"), - }, - } - - for _, tc := range tcs { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - task, err := e.CompiledTask(&task.Call{Task: tc.name}) - require.NoError(t, err) - assert.Equal(t, tc.dir, task.Dir) - }) - } - }) - } - }) - } -} - -func TestIncludesDependencies(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/includes_deps", - Target: "default", - TrimSpace: true, - Files: map[string]string{ - "default.txt": "default", - "called_dep.txt": "called_dep", - "called_task.txt": "called_task", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestIncludesCallingRoot(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/includes_call_root_task", - Target: "included:call-root", - TrimSpace: true, - Files: map[string]string{ - "root_task.txt": "root task", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestIncludesOptional(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/includes_optional", - Target: "default", - TrimSpace: true, - Files: map[string]string{ - "called_dep.txt": "called_dep", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestIncludesOptionalImplicitFalse(t *testing.T) { - t.Parallel() - - const dir = "testdata/includes_optional_implicit_false" - wd, _ := os.Getwd() - - message := "stat %s/%s/TaskfileOptional.yml: no such file or directory" - expected := fmt.Sprintf(message, wd, dir) - - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(io.Discard), - task.WithStderr(io.Discard), - ) - - err := e.Setup() - require.Error(t, err) - assert.Equal(t, expected, err.Error()) -} - -func TestIncludesOptionalExplicitFalse(t *testing.T) { - t.Parallel() - - const dir = "testdata/includes_optional_explicit_false" - wd, _ := os.Getwd() - - message := "stat %s/%s/TaskfileOptional.yml: no such file or directory" - expected := fmt.Sprintf(message, wd, dir) - - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(io.Discard), - task.WithStderr(io.Discard), - ) - - err := e.Setup() - require.Error(t, err) - assert.Equal(t, expected, err.Error()) -} - -func TestIncludesFromCustomTaskfile(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Entrypoint: "testdata/includes_yaml/Custom.ext", - Dir: "testdata/includes_yaml", - Target: "default", - TrimSpace: true, - Files: map[string]string{ - "main.txt": "main", - "included_with_yaml_extension.txt": "included_with_yaml_extension", - "included_with_custom_file.txt": "included_with_custom_file", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestIncludesRelativePath(t *testing.T) { - t.Parallel() - - const dir = "testdata/includes_rel_path" - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - - require.NoError(t, e.Setup()) - - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "common:pwd"})) - assert.Contains(t, buff.String(), "testdata/includes_rel_path/common") - - buff.Reset() - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "included:common:pwd"})) - assert.Contains(t, buff.String(), "testdata/includes_rel_path/common") -} - -func TestIncludesInternal(t *testing.T) { - t.Parallel() - - const dir = "testdata/internal_task" - tests := []struct { - name string - task string - expectedErr bool - expectedOutput string - }{ - {"included internal task via task", "task-1", false, "Hello, World!\n"}, - {"included internal task via dep", "task-2", false, "Hello, World!\n"}, - {"included internal direct", "included:task-3", true, "task: No tasks with description available. Try --list-all to list all tasks\n"}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithSilent(true), - ) - require.NoError(t, e.Setup()) - - err := e.Run(context.Background(), &task.Call{Task: test.task}) - if test.expectedErr { - require.Error(t, err) - } else { - require.NoError(t, err) - } - assert.Equal(t, test.expectedOutput, buff.String()) - }) - } -} - -func TestIncludesFlatten(t *testing.T) { - t.Parallel() - - const dir = "testdata/includes_flatten" - tests := []struct { - name string - taskfile string - task string - expectedErr bool - expectedOutput string - }{ - {name: "included flatten", taskfile: "Taskfile.yml", task: "gen", expectedOutput: "gen from included\n"}, - {name: "included flatten with default", taskfile: "Taskfile.yml", task: "default", expectedOutput: "default from included flatten\n"}, - {name: "included flatten can call entrypoint tasks", taskfile: "Taskfile.yml", task: "from_entrypoint", expectedOutput: "from entrypoint\n"}, - {name: "included flatten with deps", taskfile: "Taskfile.yml", task: "with_deps", expectedOutput: "gen from included\nwith_deps from included\n"}, - {name: "included flatten nested", taskfile: "Taskfile.yml", task: "from_nested", expectedOutput: "from nested\n"}, - {name: "included flatten multiple same task", taskfile: "Taskfile.multiple.yml", task: "gen", expectedErr: true, expectedOutput: "task: Found multiple tasks (gen) included by \"included\"\""}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithEntrypoint(dir+"/"+test.taskfile), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithSilent(true), - ) - err := e.Setup() - if test.expectedErr { - assert.EqualError(t, err, test.expectedOutput) - } else { - require.NoError(t, err) - _ = e.Run(context.Background(), &task.Call{Task: test.task}) - assert.Equal(t, test.expectedOutput, buff.String()) - } - }) - } -} - -func TestIncludesInterpolation(t *testing.T) { // nolint:paralleltest // cannot run in parallel - const dir = "testdata/includes_interpolation" - tests := []struct { - name string - task string - expectedErr bool - expectedOutput string - }{ - {"include", "include", false, "include\n"}, - {"include_with_env_variable", "include-with-env-variable", false, "include_with_env_variable\n"}, - {"include_with_dir", "include-with-dir", false, "included\n"}, - } - t.Setenv("MODULE", "included") - - for _, test := range tests { // nolint:paralleltest // cannot run in parallel - t.Run(test.name, func(t *testing.T) { - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(filepath.Join(dir, test.name)), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithSilent(true), - ) - require.NoError(t, e.Setup()) - - err := e.Run(context.Background(), &task.Call{Task: test.task}) - if test.expectedErr { - require.Error(t, err) - } else { - require.NoError(t, err) - } - assert.Equal(t, test.expectedOutput, buff.String()) - }) - } -} - -func TestIncludesWithExclude(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir("testdata/includes_with_excludes"), - task.WithSilent(true), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - require.NoError(t, e.Setup()) - - err := e.Run(context.Background(), &task.Call{Task: "included:bar"}) - require.NoError(t, err) - assert.Equal(t, "bar\n", buff.String()) - buff.Reset() - - err = e.Run(context.Background(), &task.Call{Task: "included:foo"}) - require.Error(t, err) - buff.Reset() - - err = e.Run(context.Background(), &task.Call{Task: "bar"}) - require.Error(t, err) - buff.Reset() - - err = e.Run(context.Background(), &task.Call{Task: "foo"}) - require.NoError(t, err) - assert.Equal(t, "foo\n", buff.String()) -} - -func TestIncludedTaskfileVarMerging(t *testing.T) { - t.Parallel() - - const dir = "testdata/included_taskfile_var_merging" - tests := []struct { - name string - task string - expectedOutput string - }{ - {"foo", "foo:pwd", "included_taskfile_var_merging/foo\n"}, - {"bar", "bar:pwd", "included_taskfile_var_merging/bar\n"}, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithSilent(true), - ) - require.NoError(t, e.Setup()) - - err := e.Run(context.Background(), &task.Call{Task: test.task}) - require.NoError(t, err) - assert.Contains(t, buff.String(), test.expectedOutput) - }) - } -} - -func TestInternalTask(t *testing.T) { - t.Parallel() - - const dir = "testdata/internal_task" - tests := []struct { - name string - task string - expectedErr bool - expectedOutput string - }{ - {"internal task via task", "task-1", false, "Hello, World!\n"}, - {"internal task via dep", "task-2", false, "Hello, World!\n"}, - {"internal direct", "task-3", true, ""}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithSilent(true), - ) - require.NoError(t, e.Setup()) - - err := e.Run(context.Background(), &task.Call{Task: test.task}) - if test.expectedErr { - require.Error(t, err) - } else { - require.NoError(t, err) - } - assert.Equal(t, test.expectedOutput, buff.String()) - }) - } -} - -func TestIncludesShadowedDefault(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/includes_shadowed_default", - Target: "included", - TrimSpace: true, - Files: map[string]string{ - "file.txt": "shadowed", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestIncludesUnshadowedDefault(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/includes_unshadowed_default", - Target: "included", - TrimSpace: true, - Files: map[string]string{ - "file.txt": "included", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestSupportedFileNames(t *testing.T) { - t.Parallel() - - fileNames := []string{ - "Taskfile.yml", - "Taskfile.yaml", - "Taskfile.dist.yml", - "Taskfile.dist.yaml", - } - for _, fileName := range fileNames { - t.Run(fileName, func(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: fmt.Sprintf("testdata/file_names/%s", fileName), - Target: "default", - TrimSpace: true, - Files: map[string]string{ - "output.txt": "hello", - }, - } - tt.Run(t) - }) - } -} - -func TestSummary(t *testing.T) { - t.Parallel() - - const dir = "testdata/summary" - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithSummary(true), - task.WithSilent(true), - ) - require.NoError(t, e.Setup()) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "task-with-summary"}, &task.Call{Task: "other-task-with-summary"})) - - data, err := os.ReadFile(filepathext.SmartJoin(dir, "task-with-summary.txt")) - require.NoError(t, err) - - expectedOutput := string(data) - if runtime.GOOS == "windows" { - expectedOutput = strings.ReplaceAll(expectedOutput, "\r\n", "\n") - } - - assert.Equal(t, expectedOutput, buff.String()) -} - -func TestWhenNoDirAttributeItRunsInSameDirAsTaskfile(t *testing.T) { - t.Parallel() - - const expected = "dir" - const dir = "testdata/" + expected - var out bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&out), - task.WithStderr(&out), - ) - - require.NoError(t, e.Setup()) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "whereami"})) - - // got should be the "dir" part of "testdata/dir" - got := strings.TrimSuffix(filepath.Base(out.String()), "\n") - assert.Equal(t, expected, got, "Mismatch in the working directory") -} - -func TestWhenDirAttributeAndDirExistsItRunsInThatDir(t *testing.T) { - t.Parallel() - - const expected = "exists" - const dir = "testdata/dir/explicit_exists" - var out bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&out), - task.WithStderr(&out), - ) - - require.NoError(t, e.Setup()) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "whereami"})) - - got := strings.TrimSuffix(filepath.Base(out.String()), "\n") - assert.Equal(t, expected, got, "Mismatch in the working directory") -} - -func TestWhenDirAttributeItCreatesMissingAndRunsInThatDir(t *testing.T) { - t.Parallel() - - const expected = "createme" - const dir = "testdata/dir/explicit_doesnt_exist/" - const toBeCreated = dir + expected - const target = "whereami" - var out bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&out), - task.WithStderr(&out), - ) - - // Ensure that the directory to be created doesn't actually exist. - _ = os.RemoveAll(toBeCreated) - if _, err := os.Stat(toBeCreated); err == nil { - t.Errorf("Directory should not exist: %v", err) - } - require.NoError(t, e.Setup()) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: target})) - - got := strings.TrimSuffix(filepath.Base(out.String()), "\n") - assert.Equal(t, expected, got, "Mismatch in the working directory") - - // Clean-up after ourselves only if no error. - _ = os.RemoveAll(toBeCreated) -} - -func TestDynamicVariablesRunOnTheNewCreatedDir(t *testing.T) { - t.Parallel() - - const expected = "created" - const dir = "testdata/dir/dynamic_var_on_created_dir/" - const toBeCreated = dir + expected - const target = "default" - var out bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&out), - task.WithStderr(&out), - ) - - // Ensure that the directory to be created doesn't actually exist. - _ = os.RemoveAll(toBeCreated) - if _, err := os.Stat(toBeCreated); err == nil { - t.Errorf("Directory should not exist: %v", err) - } - require.NoError(t, e.Setup()) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: target})) - - got := strings.TrimSuffix(filepath.Base(out.String()), "\n") - assert.Equal(t, expected, got, "Mismatch in the working directory") - - // Clean-up after ourselves only if no error. - _ = os.RemoveAll(toBeCreated) -} - -func TestDynamicVariablesShouldRunOnTheTaskDir(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/dir/dynamic_var", - Target: "default", - TrimSpace: false, - Files: map[string]string{ - "subdirectory/from_root_taskfile.txt": "subdirectory\n", - "subdirectory/from_included_taskfile.txt": "subdirectory\n", - "subdirectory/from_included_taskfile_task.txt": "subdirectory\n", - "subdirectory/from_interpolated_dir.txt": "subdirectory\n", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestDisplaysErrorOnVersion1Schema(t *testing.T) { - t.Parallel() - - e := task.NewExecutor( - task.WithDir("testdata/version/v1"), - task.WithStdout(io.Discard), - task.WithStderr(io.Discard), - task.WithVersionCheck(true), - ) - err := e.Setup() - require.Error(t, err) - assert.Regexp(t, regexp.MustCompile(`task: Invalid schema version in Taskfile \".*testdata\/version\/v1\/Taskfile\.yml\":\nSchema version \(1\.0\.0\) no longer supported\. Please use v3 or above`), err.Error()) -} - -func TestDisplaysErrorOnVersion2Schema(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir("testdata/version/v2"), - task.WithStdout(io.Discard), - task.WithStderr(&buff), - task.WithVersionCheck(true), - ) - err := e.Setup() - require.Error(t, err) - assert.Regexp(t, regexp.MustCompile(`task: Invalid schema version in Taskfile \".*testdata\/version\/v2\/Taskfile\.yml\":\nSchema version \(2\.0\.0\) no longer supported\. Please use v3 or above`), err.Error()) -} - -func TestShortTaskNotation(t *testing.T) { - t.Parallel() - - const dir = "testdata/short_task_notation" - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithSilent(true), - ) - require.NoError(t, e.Setup()) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"})) - assert.Equal(t, "string-slice-1\nstring-slice-2\nstring\n", buff.String()) -} - -func TestDotenvShouldIncludeAllEnvFiles(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/dotenv/default", - Target: "default", - TrimSpace: false, - Files: map[string]string{ - "include.txt": "INCLUDE1='from_include1' INCLUDE2='from_include2'\n", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestDotenvShouldErrorWhenIncludingDependantDotenvs(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir("testdata/dotenv/error_included_envs"), - task.WithSummary(true), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - - err := e.Setup() - require.Error(t, err) - assert.Contains(t, err.Error(), "move the dotenv") -} - -func TestDotenvShouldAllowMissingEnv(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/dotenv/missing_env", - Target: "default", - TrimSpace: false, - Files: map[string]string{ - "include.txt": "INCLUDE1='' INCLUDE2=''\n", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestDotenvHasLocalEnvInPath(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/dotenv/local_env_in_path", - Target: "default", - TrimSpace: false, - Files: map[string]string{ - "var.txt": "VAR='var_in_dot_env_1'\n", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestDotenvHasLocalVarInPath(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/dotenv/local_var_in_path", - Target: "default", - TrimSpace: false, - Files: map[string]string{ - "var.txt": "VAR='var_in_dot_env_3'\n", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestDotenvHasEnvVarInPath(t *testing.T) { // nolint:paralleltest // cannot run in parallel - t.Setenv("ENV_VAR", "testing") - - tt := fileContentTest{ - Dir: "testdata/dotenv/env_var_in_path", - Target: "default", - TrimSpace: false, - Files: map[string]string{ - "var.txt": "VAR='var_in_dot_env_2'\n", - }, - } - tt.Run(t) -} - -func TestTaskDotenvParseErrorMessage(t *testing.T) { - t.Parallel() - - e := task.NewExecutor( - task.WithDir("testdata/dotenv/parse_error"), - ) - - path, _ := filepath.Abs(filepath.Join(e.Dir, ".env-with-error")) - expected := fmt.Sprintf("error reading env file %s:", path) - - err := e.Setup() - require.ErrorContains(t, err, expected) -} - -func TestTaskDotenv(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/dotenv_task/default", - Target: "dotenv", - TrimSpace: true, - Files: map[string]string{ - "dotenv.txt": "foo", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestTaskDotenvFail(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/dotenv_task/default", - Target: "no-dotenv", - TrimSpace: true, - Files: map[string]string{ - "no-dotenv.txt": "global", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestTaskDotenvOverriddenByEnv(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/dotenv_task/default", - Target: "dotenv-overridden-by-env", - TrimSpace: true, - Files: map[string]string{ - "dotenv-overridden-by-env.txt": "overridden", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestTaskDotenvWithVarName(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/dotenv_task/default", - Target: "dotenv-with-var-name", - TrimSpace: true, - Files: map[string]string{ - "dotenv-with-var-name.txt": "foo", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestExitImmediately(t *testing.T) { - t.Parallel() - - const dir = "testdata/exit_immediately" - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithSilent(true), - ) - require.NoError(t, e.Setup()) - - require.Error(t, e.Run(context.Background(), &task.Call{Task: "default"})) - assert.Contains(t, buff.String(), `"this_should_fail": executable file not found in $PATH`) -} - -func TestRunOnlyRunsJobsHashOnce(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/run", - Target: "generate-hash", - Files: map[string]string{ - "hash.txt": "starting 1\n1\n2\n", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestRunOnceSharedDeps(t *testing.T) { - t.Parallel() - - const dir = "testdata/run_once_shared_deps" - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithForceAll(true), - ) - require.NoError(t, e.Setup()) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build"})) - - rx := regexp.MustCompile(`task: \[service-[a,b]:library:build\] echo "build library"`) - matches := rx.FindAllStringSubmatch(buff.String(), -1) - assert.Len(t, matches, 1) - assert.Contains(t, buff.String(), `task: [service-a:build] echo "build a"`) - assert.Contains(t, buff.String(), `task: [service-b:build] echo "build b"`) -} - -func TestDeferredCmds(t *testing.T) { - t.Parallel() - - const dir = "testdata/deferred" - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - require.NoError(t, e.Setup()) - - expectedOutputOrder := strings.TrimSpace(` -task: [task-2] echo 'cmd ran' -cmd ran -task: [task-2] exit 1 -task: [task-2] echo 'failing' && exit 2 -failing -echo ran -task-1 ran successfully -task: [task-1] echo 'task-1 ran successfully' -task-1 ran successfully -`) - require.Error(t, e.Run(context.Background(), &task.Call{Task: "task-2"})) - assert.Contains(t, buff.String(), expectedOutputOrder) - buff.Reset() - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "parent"})) - assert.Contains(t, buff.String(), "child task deferred value-from-parent") -} - -func TestExitCodeZero(t *testing.T) { - t.Parallel() - - const dir = "testdata/exit_code" - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - require.NoError(t, e.Setup()) - - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "exit-zero"})) - assert.Equal(t, "FOO=bar - DYNAMIC_FOO=bar - EXIT_CODE=", strings.TrimSpace(buff.String())) -} - -func TestExitCodeOne(t *testing.T) { - t.Parallel() - - const dir = "testdata/exit_code" - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - require.NoError(t, e.Setup()) - - require.Error(t, e.Run(context.Background(), &task.Call{Task: "exit-one"})) - assert.Equal(t, "FOO=bar - DYNAMIC_FOO=bar - EXIT_CODE=1", strings.TrimSpace(buff.String())) -} - -func TestIgnoreNilElements(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - dir string - }{ - {"nil cmd", "testdata/ignore_nil_elements/cmds"}, - {"nil dep", "testdata/ignore_nil_elements/deps"}, - {"nil include", "testdata/ignore_nil_elements/includes"}, - {"nil precondition", "testdata/ignore_nil_elements/preconditions"}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(test.dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithSilent(true), - ) - require.NoError(t, e.Setup()) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"})) - assert.Equal(t, "string-slice-1\n", buff.String()) - }) - } -} - -func TestOutputGroup(t *testing.T) { - t.Parallel() - - const dir = "testdata/output_group" - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - require.NoError(t, e.Setup()) - - expectedOutputOrder := strings.TrimSpace(` -task: [hello] echo 'Hello!' -::group::hello -Hello! -::endgroup:: -task: [bye] echo 'Bye!' -::group::bye -Bye! -::endgroup:: -`) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "bye"})) - t.Log(buff.String()) - assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder) -} - -func TestOutputGroupErrorOnlySwallowsOutputOnSuccess(t *testing.T) { - t.Parallel() - - const dir = "testdata/output_group_error_only" - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - require.NoError(t, e.Setup()) - - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "passing"})) - t.Log(buff.String()) - assert.Empty(t, buff.String()) -} - -func TestOutputGroupErrorOnlyShowsOutputOnFailure(t *testing.T) { - t.Parallel() - - const dir = "testdata/output_group_error_only" - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - require.NoError(t, e.Setup()) - - require.Error(t, e.Run(context.Background(), &task.Call{Task: "failing"})) - t.Log(buff.String()) - assert.Contains(t, "failing-output", strings.TrimSpace(buff.String())) - assert.NotContains(t, "passing", strings.TrimSpace(buff.String())) -} - -func TestIncludedVars(t *testing.T) { - t.Parallel() - - const dir = "testdata/include_with_vars" - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - require.NoError(t, e.Setup()) - - expectedOutputOrder := strings.TrimSpace(` -task: [included1:task1] echo "VAR_1 is included1-var1" -VAR_1 is included1-var1 -task: [included1:task1] echo "VAR_2 is included-default-var2" -VAR_2 is included-default-var2 -task: [included2:task1] echo "VAR_1 is included2-var1" -VAR_1 is included2-var1 -task: [included2:task1] echo "VAR_2 is included-default-var2" -VAR_2 is included-default-var2 -task: [included3:task1] echo "VAR_1 is included-default-var1" -VAR_1 is included-default-var1 -task: [included3:task1] echo "VAR_2 is included-default-var2" -VAR_2 is included-default-var2 -`) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "task1"})) - t.Log(buff.String()) - assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder) -} - -func TestIncludeWithVarsInInclude(t *testing.T) { - t.Parallel() - - const dir = "testdata/include_with_vars_inside_include" - var buff bytes.Buffer - e := task.Executor{ - Dir: dir, - Stdout: &buff, - Stderr: &buff, - } - require.NoError(t, e.Setup()) -} - -func TestIncludedVarsMultiLevel(t *testing.T) { - t.Parallel() - - const dir = "testdata/include_with_vars_multi_level" - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - require.NoError(t, e.Setup()) - - expectedOutputOrder := strings.TrimSpace(` -task: [lib:greet] echo 'Hello world' -Hello world -task: [foo:lib:greet] echo 'Hello foo' -Hello foo -task: [bar:lib:greet] echo 'Hello bar' -Hello bar -`) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"})) - t.Log(buff.String()) - assert.Equal(t, expectedOutputOrder, strings.TrimSpace(buff.String())) -} - -func TestErrorCode(t *testing.T) { - t.Parallel() - - const dir = "testdata/error_code" - tests := []struct { - name string - task string - expected int - }{ - { - name: "direct task", - task: "direct", - expected: 42, - }, { - name: "indirect task", - task: "indirect", - expected: 42, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithSilent(true), - ) - require.NoError(t, e.Setup()) - - err := e.Run(context.Background(), &task.Call{Task: test.task}) - require.Error(t, err) - taskRunErr, ok := err.(*errors.TaskRunError) - assert.True(t, ok, "cannot cast returned error to *task.TaskRunError") - assert.Equal(t, test.expected, taskRunErr.TaskExitCode(), "unexpected exit code from task") - }) - } -} - -func TestEvaluateSymlinksInPaths(t *testing.T) { // nolint:paralleltest // cannot run in parallel - const dir = "testdata/evaluate_symlinks_in_paths" - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithSilent(false), - ) - tests := []struct { - name string - task string - expected string - }{ - { - name: "default (1)", - task: "default", - expected: "task: [default] echo \"some job\"\nsome job", - }, - { - name: "test-sym (1)", - task: "test-sym", - expected: "task: [test-sym] echo \"shared file source changed\" > src/shared/b", - }, - { - name: "default (2)", - task: "default", - expected: "task: [default] echo \"some job\"\nsome job", - }, - { - name: "default (3)", - task: "default", - expected: `task: Task "default" is up to date`, - }, - { - name: "reset", - task: "reset", - expected: "task: [reset] echo \"shared file source\" > src/shared/b\ntask: [reset] echo \"file source\" > src/a", - }, - } - for _, test := range tests { // nolint:paralleltest // cannot run in parallel - t.Run(test.name, func(t *testing.T) { - require.NoError(t, e.Setup()) - err := e.Run(context.Background(), &task.Call{Task: test.task}) - require.NoError(t, err) - assert.Equal(t, test.expected, strings.TrimSpace(buff.String())) - buff.Reset() - }) - } - err := os.RemoveAll(dir + "/.task") - require.NoError(t, err) -} - -func TestTaskfileWalk(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - dir string - expected string - }{ - { - name: "walk from root directory", - dir: "testdata/taskfile_walk", - expected: "foo\n", - }, { - name: "walk from sub directory", - dir: "testdata/taskfile_walk/foo", - expected: "foo\n", - }, { - name: "walk from sub sub directory", - dir: "testdata/taskfile_walk/foo/bar", - expected: "foo\n", - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir(test.dir), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - require.NoError(t, e.Setup()) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"})) - assert.Equal(t, test.expected, buff.String()) - }) - } -} - -func TestUserWorkingDirectory(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir("testdata/user_working_dir"), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - wd, err := os.Getwd() - require.NoError(t, err) - require.NoError(t, e.Setup()) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"})) - assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String()) -} - -func TestUserWorkingDirectoryWithIncluded(t *testing.T) { - t.Parallel() - - wd, err := os.Getwd() - require.NoError(t, err) - - wd = filepathext.SmartJoin(wd, "testdata/user_working_dir_with_includes/somedir") - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir("testdata/user_working_dir_with_includes"), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - e.UserWorkingDir = wd - - require.NoError(t, err) - require.NoError(t, e.Setup()) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "included:echo"})) - assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String()) -} - -func TestPlatforms(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir("testdata/platforms"), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - require.NoError(t, e.Setup()) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build-" + runtime.GOOS})) - assert.Equal(t, fmt.Sprintf("task: [build-%s] echo 'Running task on %s'\nRunning task on %s\n", runtime.GOOS, runtime.GOOS, runtime.GOOS), buff.String()) -} - -func TestPOSIXShellOptsGlobalLevel(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir("testdata/shopts/global_level"), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - require.NoError(t, e.Setup()) - - err := e.Run(context.Background(), &task.Call{Task: "pipefail"}) - require.NoError(t, err) - assert.Equal(t, "pipefail\ton\n", buff.String()) -} - -func TestPOSIXShellOptsTaskLevel(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir("testdata/shopts/task_level"), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - require.NoError(t, e.Setup()) - - err := e.Run(context.Background(), &task.Call{Task: "pipefail"}) - require.NoError(t, err) - assert.Equal(t, "pipefail\ton\n", buff.String()) -} - -func TestPOSIXShellOptsCommandLevel(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir("testdata/shopts/command_level"), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - require.NoError(t, e.Setup()) - - err := e.Run(context.Background(), &task.Call{Task: "pipefail"}) - require.NoError(t, err) - assert.Equal(t, "pipefail\ton\n", buff.String()) -} - -func TestBashShellOptsGlobalLevel(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir("testdata/shopts/global_level"), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - require.NoError(t, e.Setup()) - - err := e.Run(context.Background(), &task.Call{Task: "globstar"}) - require.NoError(t, err) - assert.Equal(t, "globstar\ton\n", buff.String()) -} - -func TestBashShellOptsTaskLevel(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir("testdata/shopts/task_level"), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - require.NoError(t, e.Setup()) - - err := e.Run(context.Background(), &task.Call{Task: "globstar"}) - require.NoError(t, err) - assert.Equal(t, "globstar\ton\n", buff.String()) -} - -func TestBashShellOptsCommandLevel(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir("testdata/shopts/command_level"), - task.WithStdout(&buff), - task.WithStderr(&buff), - ) - require.NoError(t, e.Setup()) - - err := e.Run(context.Background(), &task.Call{Task: "globstar"}) - require.NoError(t, err) - assert.Equal(t, "globstar\ton\n", buff.String()) -} - -func TestSplitArgs(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir("testdata/split_args"), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithSilent(true), - ) - require.NoError(t, e.Setup()) - - vars := ast.NewVars() - vars.Set("CLI_ARGS", ast.Var{Value: "foo bar 'foo bar baz'"}) - - err := e.Run(context.Background(), &task.Call{Task: "default", Vars: vars}) - require.NoError(t, err) - assert.Equal(t, "3\n", buff.String()) -} - -func TestSingleCmdDep(t *testing.T) { - t.Parallel() - - tt := fileContentTest{ - Dir: "testdata/single_cmd_dep", - Target: "foo", - Files: map[string]string{ - "foo.txt": "foo\n", - "bar.txt": "bar\n", - }, - } - t.Run("", func(t *testing.T) { - t.Parallel() - tt.Run(t) - }) -} - -func TestSilence(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir("testdata/silent"), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithSilent(false), - ) - require.NoError(t, e.Setup()) - - // First verify that the silent flag is in place. - fetchedTask, err := e.GetTask(&task.Call{Task: "task-test-silent-calls-chatty-silenced"}) - require.NoError(t, err, "Unable to look up task task-test-silent-calls-chatty-silenced") - require.True(t, fetchedTask.Cmds[0].Silent, "The task task-test-silent-calls-chatty-silenced should have a silent call to chatty") - - // Then test the two basic cases where the task is silent or not. - // A silenced task. - err = e.Run(context.Background(), &task.Call{Task: "silent"}) - require.NoError(t, err) - require.Empty(t, buff.String(), "siWhile running lent: Expected not see output, because the task is silent") - - buff.Reset() - - // A chatty (not silent) task. - err = e.Run(context.Background(), &task.Call{Task: "chatty"}) - require.NoError(t, err) - require.NotEmpty(t, buff.String(), "chWhile running atty: Expected to see output, because the task is not silent") - - buff.Reset() - - // Then test invoking the two task from other tasks. - // A silenced task that calls a chatty task. - err = e.Run(context.Background(), &task.Call{Task: "task-test-silent-calls-chatty-non-silenced"}) - require.NoError(t, err) - require.NotEmpty(t, buff.String(), "While running task-test-silent-calls-chatty-non-silenced: Expected to see output. The task is silenced, but the called task is not. Silence does not propagate to called tasks.") - - buff.Reset() - - // A silent task that does a silent call to a chatty task. - err = e.Run(context.Background(), &task.Call{Task: "task-test-silent-calls-chatty-silenced"}) - require.NoError(t, err) - require.Empty(t, buff.String(), "While running task-test-silent-calls-chatty-silenced: Expected not to see output. The task calls chatty task, but the call is silenced.") - - buff.Reset() - - // A chatty task that does a call to a chatty task. - err = e.Run(context.Background(), &task.Call{Task: "task-test-chatty-calls-chatty-non-silenced"}) - require.NoError(t, err) - require.NotEmpty(t, buff.String(), "While running task-test-chatty-calls-chatty-non-silenced: Expected to see output. Both caller and callee are chatty and not silenced.") - - buff.Reset() - - // A chatty task that does a silenced call to a chatty task. - err = e.Run(context.Background(), &task.Call{Task: "task-test-chatty-calls-chatty-silenced"}) - require.NoError(t, err) - require.NotEmpty(t, buff.String(), "While running task-test-chatty-calls-chatty-silenced: Expected to see output. Call to a chatty task is silenced, but the parent task is not.") - - buff.Reset() - - // A chatty task with no cmd's of its own that does a silenced call to a chatty task. - err = e.Run(context.Background(), &task.Call{Task: "task-test-no-cmds-calls-chatty-silenced"}) - require.NoError(t, err) - require.Empty(t, buff.String(), "While running task-test-no-cmds-calls-chatty-silenced: Expected not to see output. While the task itself is not silenced, it does not have any cmds and only does an invocation of a silenced task.") - - buff.Reset() - - // A chatty task that does a silenced invocation of a task. - err = e.Run(context.Background(), &task.Call{Task: "task-test-chatty-calls-silenced-cmd"}) - require.NoError(t, err) - require.Empty(t, buff.String(), "While running task-test-chatty-calls-silenced-cmd: Expected not to see output. While the task itself is not silenced, its call to the chatty task is silent.") - - buff.Reset() - - // Then test calls via dependencies. - // A silent task that depends on a chatty task. - err = e.Run(context.Background(), &task.Call{Task: "task-test-is-silent-depends-on-chatty-non-silenced"}) - require.NoError(t, err) - require.NotEmpty(t, buff.String(), "While running task-test-is-silent-depends-on-chatty-non-silenced: Expected to see output. The task is silent and depends on a chatty task. Dependencies does not inherit silence.") - - buff.Reset() - - // A silent task that depends on a silenced chatty task. - err = e.Run(context.Background(), &task.Call{Task: "task-test-is-silent-depends-on-chatty-silenced"}) - require.NoError(t, err) - require.Empty(t, buff.String(), "While running task-test-is-silent-depends-on-chatty-silenced: Expected not to see output. The task is silent and has a silenced dependency on a chatty task.") - - buff.Reset() - - // A chatty task that, depends on a silenced chatty task. - err = e.Run(context.Background(), &task.Call{Task: "task-test-is-chatty-depends-on-chatty-silenced"}) - require.NoError(t, err) - require.Empty(t, buff.String(), "While running task-test-is-chatty-depends-on-chatty-silenced: Expected not to see output. The task is chatty but does not have commands and has a silenced dependency on a chatty task.") - - buff.Reset() -} - -func TestForce(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - env map[string]string - force bool - forceAll bool - }{ - { - name: "force", - force: true, - }, - { - name: "force-all", - forceAll: true, - }, - { - name: "force with gentle force experiment", - force: true, - env: map[string]string{ - "TASK_X_GENTLE_FORCE": "1", - }, - }, - { - name: "force-all with gentle force experiment", - forceAll: true, - env: map[string]string{ - "TASK_X_GENTLE_FORCE": "1", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir("testdata/force"), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithForce(tt.force), - task.WithForceAll(tt.forceAll), - ) - require.NoError(t, e.Setup()) - require.NoError(t, e.Run(context.Background(), &task.Call{Task: "task-with-dep"})) - }) - } -} - -func TestWildcard(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - call string - expectedOutput string - wantErr bool - }{ - { - name: "basic wildcard", - call: "wildcard-foo", - expectedOutput: "Hello foo\n", - }, - { - name: "double wildcard", - call: "foo-wildcard-bar", - expectedOutput: "Hello foo bar\n", - }, - { - name: "store wildcard", - call: "start-foo", - expectedOutput: "Starting foo\n", - }, - { - name: "matches exactly", - call: "matches-exactly-*", - expectedOutput: "I don't consume matches: []\n", - }, - { - name: "no matches", - call: "no-match", - wantErr: true, - }, - { - name: "multiple matches", - call: "wildcard-foo-bar", - expectedOutput: "Hello foo-bar\n", - }, - } - - for _, test := range tests { - t.Run(test.call, func(t *testing.T) { - t.Parallel() - - var buff bytes.Buffer - e := task.NewExecutor( - task.WithDir("testdata/wildcards"), - task.WithStdout(&buff), - task.WithStderr(&buff), - task.WithSilent(true), - task.WithForce(true), - ) - require.NoError(t, e.Setup()) - if test.wantErr { - require.Error(t, e.Run(context.Background(), &task.Call{Task: test.call})) - return - } - require.NoError(t, e.Run(context.Background(), &task.Call{Task: test.call})) - assert.Equal(t, test.expectedOutput, buff.String()) - }) - } -} +// func TestGenerates(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/generates" + +// const ( +// srcTask = "sub/src.txt" +// relTask = "rel.txt" +// absTask = "abs.txt" +// fileWithSpaces = "my text file.txt" +// ) + +// srcFile := filepathext.SmartJoin(dir, srcTask) + +// for _, task := range []string{srcTask, relTask, absTask, fileWithSpaces} { +// path := filepathext.SmartJoin(dir, task) +// _ = os.Remove(path) +// if _, err := os.Stat(path); err == nil { +// t.Errorf("File should not exist: %v", err) +// } +// } + +// buff := bytes.NewBuffer(nil) +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(buff), +// task.WithStderr(buff), +// ) +// require.NoError(t, e.Setup()) + +// for _, theTask := range []string{relTask, absTask, fileWithSpaces} { +// destFile := filepathext.SmartJoin(dir, theTask) +// upToDate := fmt.Sprintf("task: Task \"%s\" is up to date\n", srcTask) + +// fmt.Sprintf("task: Task \"%s\" is up to date\n", theTask) + +// // Run task for the first time. +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: theTask})) + +// if _, err := os.Stat(srcFile); err != nil { +// t.Errorf("File should exist: %v", err) +// } +// if _, err := os.Stat(destFile); err != nil { +// t.Errorf("File should exist: %v", err) +// } +// // Ensure task was not incorrectly found to be up-to-date on first run. +// if buff.String() == upToDate { +// t.Errorf("Wrong output message: %s", buff.String()) +// } +// buff.Reset() + +// // Re-run task to ensure it's now found to be up-to-date. +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: theTask})) +// if buff.String() != upToDate { +// t.Errorf("Wrong output message: %s", buff.String()) +// } +// buff.Reset() +// } +// } + +// func TestStatusChecksum(t *testing.T) { // nolint:paralleltest // cannot run in parallel +// const dir = "testdata/checksum" + +// tests := []struct { +// files []string +// task string +// }{ +// {[]string{"generated.txt", ".task/checksum/build"}, "build"}, +// {[]string{"generated.txt", ".task/checksum/build-with-status"}, "build-with-status"}, +// } + +// for _, test := range tests { // nolint:paralleltest // cannot run in parallel +// t.Run(test.task, func(t *testing.T) { +// for _, f := range test.files { +// _ = os.Remove(filepathext.SmartJoin(dir, f)) + +// _, err := os.Stat(filepathext.SmartJoin(dir, f)) +// require.Error(t, err) +// } + +// var buff bytes.Buffer +// tempDir := task.TempDir{ +// Remote: filepathext.SmartJoin(dir, ".task"), +// Fingerprint: filepathext.SmartJoin(dir, ".task"), +// } +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithTempDir(tempDir), +// ) +// require.NoError(t, e.Setup()) + +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: test.task})) +// for _, f := range test.files { +// _, err := os.Stat(filepathext.SmartJoin(dir, f)) +// require.NoError(t, err) +// } + +// // Capture the modification time, so we can ensure the checksum file +// // is not regenerated when the hash hasn't changed. +// s, err := os.Stat(filepathext.SmartJoin(tempDir.Fingerprint, "checksum/"+test.task)) +// require.NoError(t, err) +// time := s.ModTime() + +// buff.Reset() +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: test.task})) +// assert.Equal(t, `task: Task "`+test.task+`" is up to date`+"\n", buff.String()) + +// s, err = os.Stat(filepathext.SmartJoin(tempDir.Fingerprint, "checksum/"+test.task)) +// require.NoError(t, err) +// assert.Equal(t, time, s.ModTime()) +// }) +// } +// } + +// func TestStatusVariables(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/status_vars" + +// _ = os.RemoveAll(filepathext.SmartJoin(dir, ".task")) +// _ = os.Remove(filepathext.SmartJoin(dir, "generated.txt")) + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithTempDir(task.TempDir{ +// Remote: filepathext.SmartJoin(dir, ".task"), +// Fingerprint: filepathext.SmartJoin(dir, ".task"), +// }), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithSilent(false), +// task.WithVerbose(true), +// ) +// require.NoError(t, e.Setup()) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build-checksum"})) + +// assert.Contains(t, buff.String(), "3e464c4b03f4b65d740e1e130d4d108a") + +// buff.Reset() +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build-ts"})) + +// inf, err := os.Stat(filepathext.SmartJoin(dir, "source.txt")) +// require.NoError(t, err) +// ts := fmt.Sprintf("%d", inf.ModTime().Unix()) +// tf := inf.ModTime().String() + +// assert.Contains(t, buff.String(), ts) +// assert.Contains(t, buff.String(), tf) +// } + +// func TestCmdsVariables(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/cmds_vars" + +// _ = os.RemoveAll(filepathext.SmartJoin(dir, ".task")) + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithTempDir(task.TempDir{ +// Remote: filepathext.SmartJoin(dir, ".task"), +// Fingerprint: filepathext.SmartJoin(dir, ".task"), +// }), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithSilent(false), +// task.WithVerbose(true), +// ) +// require.NoError(t, e.Setup()) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build-checksum"})) + +// assert.Contains(t, buff.String(), "3e464c4b03f4b65d740e1e130d4d108a") + +// buff.Reset() +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build-ts"})) +// inf, err := os.Stat(filepathext.SmartJoin(dir, "source.txt")) +// require.NoError(t, err) +// ts := fmt.Sprintf("%d", inf.ModTime().Unix()) +// tf := inf.ModTime().String() + +// assert.Contains(t, buff.String(), ts) +// assert.Contains(t, buff.String(), tf) +// } + +// func TestCyclicDep(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/cyclic" + +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(io.Discard), +// task.WithStderr(io.Discard), +// ) +// require.NoError(t, e.Setup()) +// assert.IsType(t, &errors.TaskCalledTooManyTimesError{}, e.Run(context.Background(), &task.Call{Task: "task-1"})) +// } + +// func TestTaskVersion(t *testing.T) { +// t.Parallel() + +// tests := []struct { +// Dir string +// Version *semver.Version +// wantErr bool +// }{ +// {"testdata/version/v1", semver.MustParse("1"), true}, +// {"testdata/version/v2", semver.MustParse("2"), true}, +// {"testdata/version/v3", semver.MustParse("3"), false}, +// } + +// for _, test := range tests { +// t.Run(test.Dir, func(t *testing.T) { +// t.Parallel() + +// e := task.NewExecutor( +// task.WithDir(test.Dir), +// task.WithStdout(io.Discard), +// task.WithStderr(io.Discard), +// task.WithVersionCheck(true), +// ) +// err := e.Setup() +// if test.wantErr { +// require.Error(t, err) +// return +// } +// require.NoError(t, err) +// assert.Equal(t, test.Version, e.Taskfile.Version) +// assert.Equal(t, 2, e.Taskfile.Tasks.Len()) +// }) +// } +// } + +// func TestTaskIgnoreErrors(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/ignore_errors" + +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(io.Discard), +// task.WithStderr(io.Discard), +// ) +// require.NoError(t, e.Setup()) + +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "task-should-pass"})) +// require.Error(t, e.Run(context.Background(), &task.Call{Task: "task-should-fail"})) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "cmd-should-pass"})) +// require.Error(t, e.Run(context.Background(), &task.Call{Task: "cmd-should-fail"})) +// } + +// func TestExpand(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/expand" + +// home, err := os.UserHomeDir() +// if err != nil { +// t.Errorf("Couldn't get $HOME: %v", err) +// } +// var buff bytes.Buffer + +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// require.NoError(t, e.Setup()) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "pwd"})) +// assert.Equal(t, home, strings.TrimSpace(buff.String())) +// } + +// func TestDry(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/dry" + +// file := filepathext.SmartJoin(dir, "file.txt") +// _ = os.Remove(file) + +// var buff bytes.Buffer + +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithDry(true), +// ) +// require.NoError(t, e.Setup()) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build"})) + +// assert.Equal(t, "task: [build] touch file.txt", strings.TrimSpace(buff.String())) +// if _, err := os.Stat(file); err == nil { +// t.Errorf("File should not exist %s", file) +// } +// } + +// // TestDryChecksum tests if the checksum file is not being written to disk +// // if the dry mode is enabled. +// func TestDryChecksum(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/dry_checksum" + +// checksumFile := filepathext.SmartJoin(dir, ".task/checksum/default") +// _ = os.Remove(checksumFile) + +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithTempDir(task.TempDir{ +// Remote: filepathext.SmartJoin(dir, ".task"), +// Fingerprint: filepathext.SmartJoin(dir, ".task"), +// }), +// task.WithStdout(io.Discard), +// task.WithStderr(io.Discard), +// task.WithDry(true), +// ) +// require.NoError(t, e.Setup()) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"})) + +// _, err := os.Stat(checksumFile) +// require.Error(t, err, "checksum file should not exist") + +// e.Dry = false +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"})) +// _, err = os.Stat(checksumFile) +// require.NoError(t, err, "checksum file should exist") +// } + +// func TestIncludes(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/includes", +// Target: "default", +// TrimSpace: true, +// Files: map[string]string{ +// "main.txt": "main", +// "included_directory.txt": "included_directory", +// "included_directory_without_dir.txt": "included_directory_without_dir", +// "included_taskfile_without_dir.txt": "included_taskfile_without_dir", +// "./module2/included_directory_with_dir.txt": "included_directory_with_dir", +// "./module2/included_taskfile_with_dir.txt": "included_taskfile_with_dir", +// "os_include.txt": "os", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestIncludesMultiLevel(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/includes_multi_level", +// Target: "default", +// TrimSpace: true, +// Files: map[string]string{ +// "called_one.txt": "one", +// "called_two.txt": "two", +// "called_three.txt": "three", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestIncludesRemote(t *testing.T) { +// enableExperimentForTest(t, &experiments.RemoteTaskfiles, 1) + +// dir := "testdata/includes_remote" +// os.RemoveAll(filepath.Join(dir, ".task", "remote")) + +// srv := httptest.NewServer(http.FileServer(http.Dir(dir))) +// defer srv.Close() + +// tcs := []struct { +// firstRemote string +// secondRemote string +// }{ +// { +// firstRemote: srv.URL + "/first/Taskfile.yml", +// secondRemote: srv.URL + "/first/second/Taskfile.yml", +// }, +// { +// firstRemote: srv.URL + "/first/Taskfile.yml", +// secondRemote: "./second/Taskfile.yml", +// }, +// { +// firstRemote: srv.URL + "/first/", +// secondRemote: srv.URL + "/first/second/", +// }, +// } + +// taskCalls := []*task.Call{ +// {Task: "first:write-file"}, +// {Task: "first:second:write-file"}, +// } + +// for i, tc := range tcs { +// t.Run(fmt.Sprint(i), func(t *testing.T) { +// t.Setenv("FIRST_REMOTE_URL", tc.firstRemote) +// t.Setenv("SECOND_REMOTE_URL", tc.secondRemote) + +// var buff SyncBuffer + +// executors := []struct { +// name string +// executor *task.Executor +// }{ +// { +// name: "online, always download", +// executor: task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithTimeout(time.Minute), +// task.WithInsecure(true), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithVerbose(true), + +// // Without caching +// task.WithAssumeYes(true), +// task.WithDownload(true), +// ), +// }, +// { +// name: "offline, use cache", +// executor: task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithTimeout(time.Minute), +// task.WithInsecure(true), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithVerbose(true), + +// // With caching +// task.WithAssumeYes(false), +// task.WithDownload(false), +// task.WithOffline(true), +// ), +// }, +// } + +// for _, e := range executors { +// t.Run(e.name, func(t *testing.T) { +// require.NoError(t, e.executor.Setup()) + +// for k, taskCall := range taskCalls { +// t.Run(taskCall.Task, func(t *testing.T) { +// expectedContent := fmt.Sprint(rand.Int64()) +// t.Setenv("CONTENT", expectedContent) + +// outputFile := fmt.Sprintf("%d.%d.txt", i, k) +// t.Setenv("OUTPUT_FILE", outputFile) + +// path := filepath.Join(dir, outputFile) +// require.NoError(t, os.RemoveAll(path)) + +// require.NoError(t, e.executor.Run(context.Background(), taskCall)) + +// actualContent, err := os.ReadFile(path) +// require.NoError(t, err) +// assert.Equal(t, expectedContent, strings.TrimSpace(string(actualContent))) +// }) +// } +// }) +// } + +// t.Log("\noutput:\n", buff.buf.String()) +// }) +// } +// } + +// func TestIncludeCycle(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/includes_cycle" + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithSilent(true), +// ) + +// err := e.Setup() +// require.Error(t, err) +// assert.Contains(t, err.Error(), "task: include cycle detected between") +// } + +// func TestIncludesIncorrect(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/includes_incorrect" + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithSilent(true), +// ) + +// err := e.Setup() +// require.Error(t, err) +// assert.Contains(t, err.Error(), "Failed to parse testdata/includes_incorrect/incomplete.yml:", err.Error()) +// } + +// func TestIncludesEmptyMain(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/includes_empty", +// Target: "included:default", +// TrimSpace: true, +// Files: map[string]string{ +// "file.txt": "default", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestIncludesHttp(t *testing.T) { +// enableExperimentForTest(t, &experiments.RemoteTaskfiles, 1) + +// dir, err := filepath.Abs("testdata/includes_http") +// require.NoError(t, err) + +// srv := httptest.NewServer(http.FileServer(http.Dir(dir))) +// defer srv.Close() + +// t.Cleanup(func() { +// // This test fills the .task/remote directory with cache entries because the include URL +// // is different on every test due to the dynamic nature of the TCP port in srv.URL +// if err := os.RemoveAll(filepath.Join(dir, ".task")); err != nil { +// t.Logf("error cleaning up: %s", err) +// } +// }) + +// taskfiles, err := fs.Glob(os.DirFS(dir), "root-taskfile-*.yml") +// require.NoError(t, err) + +// remotes := []struct { +// name string +// root string +// }{ +// { +// name: "local", +// root: ".", +// }, +// { +// name: "http-remote", +// root: srv.URL, +// }, +// } + +// for _, taskfile := range taskfiles { +// t.Run(taskfile, func(t *testing.T) { +// for _, remote := range remotes { +// t.Run(remote.name, func(t *testing.T) { +// t.Setenv("INCLUDE_ROOT", remote.root) +// entrypoint := filepath.Join(dir, taskfile) + +// var buff SyncBuffer +// e := task.NewExecutor( +// task.WithEntrypoint(entrypoint), +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithInsecure(true), +// task.WithDownload(true), +// task.WithAssumeYes(true), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithVerbose(true), +// task.WithTimeout(time.Minute), +// ) +// require.NoError(t, e.Setup()) +// defer func() { t.Log("output:", buff.buf.String()) }() + +// tcs := []struct { +// name, dir string +// }{ +// { +// name: "second-with-dir-1:third-with-dir-1:default", +// dir: filepath.Join(dir, "dir-1"), +// }, +// { +// name: "second-with-dir-1:third-with-dir-2:default", +// dir: filepath.Join(dir, "dir-2"), +// }, +// } + +// for _, tc := range tcs { +// t.Run(tc.name, func(t *testing.T) { +// t.Parallel() +// task, err := e.CompiledTask(&task.Call{Task: tc.name}) +// require.NoError(t, err) +// assert.Equal(t, tc.dir, task.Dir) +// }) +// } +// }) +// } +// }) +// } +// } + +// func TestIncludesDependencies(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/includes_deps", +// Target: "default", +// TrimSpace: true, +// Files: map[string]string{ +// "default.txt": "default", +// "called_dep.txt": "called_dep", +// "called_task.txt": "called_task", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestIncludesCallingRoot(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/includes_call_root_task", +// Target: "included:call-root", +// TrimSpace: true, +// Files: map[string]string{ +// "root_task.txt": "root task", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestIncludesOptional(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/includes_optional", +// Target: "default", +// TrimSpace: true, +// Files: map[string]string{ +// "called_dep.txt": "called_dep", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestIncludesOptionalImplicitFalse(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/includes_optional_implicit_false" +// wd, _ := os.Getwd() + +// message := "stat %s/%s/TaskfileOptional.yml: no such file or directory" +// expected := fmt.Sprintf(message, wd, dir) + +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(io.Discard), +// task.WithStderr(io.Discard), +// ) + +// err := e.Setup() +// require.Error(t, err) +// assert.Equal(t, expected, err.Error()) +// } + +// func TestIncludesOptionalExplicitFalse(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/includes_optional_explicit_false" +// wd, _ := os.Getwd() + +// message := "stat %s/%s/TaskfileOptional.yml: no such file or directory" +// expected := fmt.Sprintf(message, wd, dir) + +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(io.Discard), +// task.WithStderr(io.Discard), +// ) + +// err := e.Setup() +// require.Error(t, err) +// assert.Equal(t, expected, err.Error()) +// } + +// func TestIncludesFromCustomTaskfile(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Entrypoint: "testdata/includes_yaml/Custom.ext", +// Dir: "testdata/includes_yaml", +// Target: "default", +// TrimSpace: true, +// Files: map[string]string{ +// "main.txt": "main", +// "included_with_yaml_extension.txt": "included_with_yaml_extension", +// "included_with_custom_file.txt": "included_with_custom_file", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestIncludesRelativePath(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/includes_rel_path" + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) + +// require.NoError(t, e.Setup()) + +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "common:pwd"})) +// assert.Contains(t, buff.String(), "testdata/includes_rel_path/common") + +// buff.Reset() +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "included:common:pwd"})) +// assert.Contains(t, buff.String(), "testdata/includes_rel_path/common") +// } + +// func TestIncludesInternal(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/internal_task" +// tests := []struct { +// name string +// task string +// expectedErr bool +// expectedOutput string +// }{ +// {"included internal task via task", "task-1", false, "Hello, World!\n"}, +// {"included internal task via dep", "task-2", false, "Hello, World!\n"}, +// {"included internal direct", "included:task-3", true, "task: No tasks with description available. Try --list-all to list all tasks\n"}, +// } + +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithSilent(true), +// ) +// require.NoError(t, e.Setup()) + +// err := e.Run(context.Background(), &task.Call{Task: test.task}) +// if test.expectedErr { +// require.Error(t, err) +// } else { +// require.NoError(t, err) +// } +// assert.Equal(t, test.expectedOutput, buff.String()) +// }) +// } +// } + +// func TestIncludesFlatten(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/includes_flatten" +// tests := []struct { +// name string +// taskfile string +// task string +// expectedErr bool +// expectedOutput string +// }{ +// {name: "included flatten", taskfile: "Taskfile.yml", task: "gen", expectedOutput: "gen from included\n"}, +// {name: "included flatten with default", taskfile: "Taskfile.yml", task: "default", expectedOutput: "default from included flatten\n"}, +// {name: "included flatten can call entrypoint tasks", taskfile: "Taskfile.yml", task: "from_entrypoint", expectedOutput: "from entrypoint\n"}, +// {name: "included flatten with deps", taskfile: "Taskfile.yml", task: "with_deps", expectedOutput: "gen from included\nwith_deps from included\n"}, +// {name: "included flatten nested", taskfile: "Taskfile.yml", task: "from_nested", expectedOutput: "from nested\n"}, +// {name: "included flatten multiple same task", taskfile: "Taskfile.multiple.yml", task: "gen", expectedErr: true, expectedOutput: "task: Found multiple tasks (gen) included by \"included\"\""}, +// } + +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithEntrypoint(dir+"/"+test.taskfile), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithSilent(true), +// ) +// err := e.Setup() +// if test.expectedErr { +// assert.EqualError(t, err, test.expectedOutput) +// } else { +// require.NoError(t, err) +// _ = e.Run(context.Background(), &task.Call{Task: test.task}) +// assert.Equal(t, test.expectedOutput, buff.String()) +// } +// }) +// } +// } + +// func TestIncludesInterpolation(t *testing.T) { // nolint:paralleltest // cannot run in parallel +// const dir = "testdata/includes_interpolation" +// tests := []struct { +// name string +// task string +// expectedErr bool +// expectedOutput string +// }{ +// {"include", "include", false, "include\n"}, +// {"include_with_env_variable", "include-with-env-variable", false, "include_with_env_variable\n"}, +// {"include_with_dir", "include-with-dir", false, "included\n"}, +// } +// t.Setenv("MODULE", "included") + +// for _, test := range tests { // nolint:paralleltest // cannot run in parallel +// t.Run(test.name, func(t *testing.T) { +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(filepath.Join(dir, test.name)), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithSilent(true), +// ) +// require.NoError(t, e.Setup()) + +// err := e.Run(context.Background(), &task.Call{Task: test.task}) +// if test.expectedErr { +// require.Error(t, err) +// } else { +// require.NoError(t, err) +// } +// assert.Equal(t, test.expectedOutput, buff.String()) +// }) +// } +// } + +// func TestIncludesWithExclude(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir("testdata/includes_with_excludes"), +// task.WithSilent(true), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// require.NoError(t, e.Setup()) + +// err := e.Run(context.Background(), &task.Call{Task: "included:bar"}) +// require.NoError(t, err) +// assert.Equal(t, "bar\n", buff.String()) +// buff.Reset() + +// err = e.Run(context.Background(), &task.Call{Task: "included:foo"}) +// require.Error(t, err) +// buff.Reset() + +// err = e.Run(context.Background(), &task.Call{Task: "bar"}) +// require.Error(t, err) +// buff.Reset() + +// err = e.Run(context.Background(), &task.Call{Task: "foo"}) +// require.NoError(t, err) +// assert.Equal(t, "foo\n", buff.String()) +// } + +// func TestIncludedTaskfileVarMerging(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/included_taskfile_var_merging" +// tests := []struct { +// name string +// task string +// expectedOutput string +// }{ +// {"foo", "foo:pwd", "included_taskfile_var_merging/foo\n"}, +// {"bar", "bar:pwd", "included_taskfile_var_merging/bar\n"}, +// } +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithSilent(true), +// ) +// require.NoError(t, e.Setup()) + +// err := e.Run(context.Background(), &task.Call{Task: test.task}) +// require.NoError(t, err) +// assert.Contains(t, buff.String(), test.expectedOutput) +// }) +// } +// } + +// func TestInternalTask(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/internal_task" +// tests := []struct { +// name string +// task string +// expectedErr bool +// expectedOutput string +// }{ +// {"internal task via task", "task-1", false, "Hello, World!\n"}, +// {"internal task via dep", "task-2", false, "Hello, World!\n"}, +// {"internal direct", "task-3", true, ""}, +// } + +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithSilent(true), +// ) +// require.NoError(t, e.Setup()) + +// err := e.Run(context.Background(), &task.Call{Task: test.task}) +// if test.expectedErr { +// require.Error(t, err) +// } else { +// require.NoError(t, err) +// } +// assert.Equal(t, test.expectedOutput, buff.String()) +// }) +// } +// } + +// func TestIncludesShadowedDefault(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/includes_shadowed_default", +// Target: "included", +// TrimSpace: true, +// Files: map[string]string{ +// "file.txt": "shadowed", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestIncludesUnshadowedDefault(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/includes_unshadowed_default", +// Target: "included", +// TrimSpace: true, +// Files: map[string]string{ +// "file.txt": "included", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestSupportedFileNames(t *testing.T) { +// t.Parallel() + +// fileNames := []string{ +// "Taskfile.yml", +// "Taskfile.yaml", +// "Taskfile.dist.yml", +// "Taskfile.dist.yaml", +// } +// for _, fileName := range fileNames { +// t.Run(fileName, func(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: fmt.Sprintf("testdata/file_names/%s", fileName), +// Target: "default", +// TrimSpace: true, +// Files: map[string]string{ +// "output.txt": "hello", +// }, +// } +// tt.Run(t) +// }) +// } +// } + +// func TestSummary(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/summary" + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithSummary(true), +// task.WithSilent(true), +// ) +// require.NoError(t, e.Setup()) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "task-with-summary"}, &task.Call{Task: "other-task-with-summary"})) + +// data, err := os.ReadFile(filepathext.SmartJoin(dir, "task-with-summary.txt")) +// require.NoError(t, err) + +// expectedOutput := string(data) +// if runtime.GOOS == "windows" { +// expectedOutput = strings.ReplaceAll(expectedOutput, "\r\n", "\n") +// } + +// assert.Equal(t, expectedOutput, buff.String()) +// } + +// func TestWhenNoDirAttributeItRunsInSameDirAsTaskfile(t *testing.T) { +// t.Parallel() + +// const expected = "dir" +// const dir = "testdata/" + expected +// var out bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&out), +// task.WithStderr(&out), +// ) + +// require.NoError(t, e.Setup()) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "whereami"})) + +// // got should be the "dir" part of "testdata/dir" +// got := strings.TrimSuffix(filepath.Base(out.String()), "\n") +// assert.Equal(t, expected, got, "Mismatch in the working directory") +// } + +// func TestWhenDirAttributeAndDirExistsItRunsInThatDir(t *testing.T) { +// t.Parallel() + +// const expected = "exists" +// const dir = "testdata/dir/explicit_exists" +// var out bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&out), +// task.WithStderr(&out), +// ) + +// require.NoError(t, e.Setup()) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "whereami"})) + +// got := strings.TrimSuffix(filepath.Base(out.String()), "\n") +// assert.Equal(t, expected, got, "Mismatch in the working directory") +// } + +// func TestWhenDirAttributeItCreatesMissingAndRunsInThatDir(t *testing.T) { +// t.Parallel() + +// const expected = "createme" +// const dir = "testdata/dir/explicit_doesnt_exist/" +// const toBeCreated = dir + expected +// const target = "whereami" +// var out bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&out), +// task.WithStderr(&out), +// ) + +// // Ensure that the directory to be created doesn't actually exist. +// _ = os.RemoveAll(toBeCreated) +// if _, err := os.Stat(toBeCreated); err == nil { +// t.Errorf("Directory should not exist: %v", err) +// } +// require.NoError(t, e.Setup()) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: target})) + +// got := strings.TrimSuffix(filepath.Base(out.String()), "\n") +// assert.Equal(t, expected, got, "Mismatch in the working directory") + +// // Clean-up after ourselves only if no error. +// _ = os.RemoveAll(toBeCreated) +// } + +// func TestDynamicVariablesRunOnTheNewCreatedDir(t *testing.T) { +// t.Parallel() + +// const expected = "created" +// const dir = "testdata/dir/dynamic_var_on_created_dir/" +// const toBeCreated = dir + expected +// const target = "default" +// var out bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&out), +// task.WithStderr(&out), +// ) + +// // Ensure that the directory to be created doesn't actually exist. +// _ = os.RemoveAll(toBeCreated) +// if _, err := os.Stat(toBeCreated); err == nil { +// t.Errorf("Directory should not exist: %v", err) +// } +// require.NoError(t, e.Setup()) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: target})) + +// got := strings.TrimSuffix(filepath.Base(out.String()), "\n") +// assert.Equal(t, expected, got, "Mismatch in the working directory") + +// // Clean-up after ourselves only if no error. +// _ = os.RemoveAll(toBeCreated) +// } + +// func TestDynamicVariablesShouldRunOnTheTaskDir(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/dir/dynamic_var", +// Target: "default", +// TrimSpace: false, +// Files: map[string]string{ +// "subdirectory/from_root_taskfile.txt": "subdirectory\n", +// "subdirectory/from_included_taskfile.txt": "subdirectory\n", +// "subdirectory/from_included_taskfile_task.txt": "subdirectory\n", +// "subdirectory/from_interpolated_dir.txt": "subdirectory\n", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestDisplaysErrorOnVersion1Schema(t *testing.T) { +// t.Parallel() + +// e := task.NewExecutor( +// task.WithDir("testdata/version/v1"), +// task.WithStdout(io.Discard), +// task.WithStderr(io.Discard), +// task.WithVersionCheck(true), +// ) +// err := e.Setup() +// require.Error(t, err) +// assert.Regexp(t, regexp.MustCompile(`task: Invalid schema version in Taskfile \".*testdata\/version\/v1\/Taskfile\.yml\":\nSchema version \(1\.0\.0\) no longer supported\. Please use v3 or above`), err.Error()) +// } + +// func TestDisplaysErrorOnVersion2Schema(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir("testdata/version/v2"), +// task.WithStdout(io.Discard), +// task.WithStderr(&buff), +// task.WithVersionCheck(true), +// ) +// err := e.Setup() +// require.Error(t, err) +// assert.Regexp(t, regexp.MustCompile(`task: Invalid schema version in Taskfile \".*testdata\/version\/v2\/Taskfile\.yml\":\nSchema version \(2\.0\.0\) no longer supported\. Please use v3 or above`), err.Error()) +// } + +// func TestShortTaskNotation(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/short_task_notation" + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithSilent(true), +// ) +// require.NoError(t, e.Setup()) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"})) +// assert.Equal(t, "string-slice-1\nstring-slice-2\nstring\n", buff.String()) +// } + +// func TestDotenvShouldIncludeAllEnvFiles(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/dotenv/default", +// Target: "default", +// TrimSpace: false, +// Files: map[string]string{ +// "include.txt": "INCLUDE1='from_include1' INCLUDE2='from_include2'\n", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestDotenvShouldErrorWhenIncludingDependantDotenvs(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir("testdata/dotenv/error_included_envs"), +// task.WithSummary(true), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) + +// err := e.Setup() +// require.Error(t, err) +// assert.Contains(t, err.Error(), "move the dotenv") +// } + +// func TestDotenvShouldAllowMissingEnv(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/dotenv/missing_env", +// Target: "default", +// TrimSpace: false, +// Files: map[string]string{ +// "include.txt": "INCLUDE1='' INCLUDE2=''\n", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestDotenvHasLocalEnvInPath(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/dotenv/local_env_in_path", +// Target: "default", +// TrimSpace: false, +// Files: map[string]string{ +// "var.txt": "VAR='var_in_dot_env_1'\n", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestDotenvHasLocalVarInPath(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/dotenv/local_var_in_path", +// Target: "default", +// TrimSpace: false, +// Files: map[string]string{ +// "var.txt": "VAR='var_in_dot_env_3'\n", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestDotenvHasEnvVarInPath(t *testing.T) { // nolint:paralleltest // cannot run in parallel +// t.Setenv("ENV_VAR", "testing") + +// tt := fileContentTest{ +// Dir: "testdata/dotenv/env_var_in_path", +// Target: "default", +// TrimSpace: false, +// Files: map[string]string{ +// "var.txt": "VAR='var_in_dot_env_2'\n", +// }, +// } +// tt.Run(t) +// } + +// func TestTaskDotenvParseErrorMessage(t *testing.T) { +// t.Parallel() + +// e := task.NewExecutor( +// task.WithDir("testdata/dotenv/parse_error"), +// ) + +// path, _ := filepath.Abs(filepath.Join(e.Dir, ".env-with-error")) +// expected := fmt.Sprintf("error reading env file %s:", path) + +// err := e.Setup() +// require.ErrorContains(t, err, expected) +// } + +// func TestTaskDotenv(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/dotenv_task/default", +// Target: "dotenv", +// TrimSpace: true, +// Files: map[string]string{ +// "dotenv.txt": "foo", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestTaskDotenvFail(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/dotenv_task/default", +// Target: "no-dotenv", +// TrimSpace: true, +// Files: map[string]string{ +// "no-dotenv.txt": "global", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestTaskDotenvOverriddenByEnv(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/dotenv_task/default", +// Target: "dotenv-overridden-by-env", +// TrimSpace: true, +// Files: map[string]string{ +// "dotenv-overridden-by-env.txt": "overridden", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestTaskDotenvWithVarName(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/dotenv_task/default", +// Target: "dotenv-with-var-name", +// TrimSpace: true, +// Files: map[string]string{ +// "dotenv-with-var-name.txt": "foo", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestExitImmediately(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/exit_immediately" + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithSilent(true), +// ) +// require.NoError(t, e.Setup()) + +// require.Error(t, e.Run(context.Background(), &task.Call{Task: "default"})) +// assert.Contains(t, buff.String(), `"this_should_fail": executable file not found in $PATH`) +// } + +// func TestRunOnlyRunsJobsHashOnce(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/run", +// Target: "generate-hash", +// Files: map[string]string{ +// "hash.txt": "starting 1\n1\n2\n", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestRunOnceSharedDeps(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/run_once_shared_deps" + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithForceAll(true), +// ) +// require.NoError(t, e.Setup()) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build"})) + +// rx := regexp.MustCompile(`task: \[service-[a,b]:library:build\] echo "build library"`) +// matches := rx.FindAllStringSubmatch(buff.String(), -1) +// assert.Len(t, matches, 1) +// assert.Contains(t, buff.String(), `task: [service-a:build] echo "build a"`) +// assert.Contains(t, buff.String(), `task: [service-b:build] echo "build b"`) +// } + +// func TestDeferredCmds(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/deferred" +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// require.NoError(t, e.Setup()) + +// expectedOutputOrder := strings.TrimSpace(` +// task: [task-2] echo 'cmd ran' +// cmd ran +// task: [task-2] exit 1 +// task: [task-2] echo 'failing' && exit 2 +// failing +// echo ran +// task-1 ran successfully +// task: [task-1] echo 'task-1 ran successfully' +// task-1 ran successfully +// `) +// require.Error(t, e.Run(context.Background(), &task.Call{Task: "task-2"})) +// assert.Contains(t, buff.String(), expectedOutputOrder) +// buff.Reset() +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "parent"})) +// assert.Contains(t, buff.String(), "child task deferred value-from-parent") +// } + +// func TestExitCodeZero(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/exit_code" +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// require.NoError(t, e.Setup()) + +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "exit-zero"})) +// assert.Equal(t, "FOO=bar - DYNAMIC_FOO=bar - EXIT_CODE=", strings.TrimSpace(buff.String())) +// } + +// func TestExitCodeOne(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/exit_code" +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// require.NoError(t, e.Setup()) + +// require.Error(t, e.Run(context.Background(), &task.Call{Task: "exit-one"})) +// assert.Equal(t, "FOO=bar - DYNAMIC_FOO=bar - EXIT_CODE=1", strings.TrimSpace(buff.String())) +// } + +// func TestIgnoreNilElements(t *testing.T) { +// t.Parallel() + +// tests := []struct { +// name string +// dir string +// }{ +// {"nil cmd", "testdata/ignore_nil_elements/cmds"}, +// {"nil dep", "testdata/ignore_nil_elements/deps"}, +// {"nil include", "testdata/ignore_nil_elements/includes"}, +// {"nil precondition", "testdata/ignore_nil_elements/preconditions"}, +// } + +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(test.dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithSilent(true), +// ) +// require.NoError(t, e.Setup()) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"})) +// assert.Equal(t, "string-slice-1\n", buff.String()) +// }) +// } +// } + +// func TestOutputGroup(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/output_group" +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// require.NoError(t, e.Setup()) + +// expectedOutputOrder := strings.TrimSpace(` +// task: [hello] echo 'Hello!' +// ::group::hello +// Hello! +// ::endgroup:: +// task: [bye] echo 'Bye!' +// ::group::bye +// Bye! +// ::endgroup:: +// `) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "bye"})) +// t.Log(buff.String()) +// assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder) +// } + +// func TestOutputGroupErrorOnlySwallowsOutputOnSuccess(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/output_group_error_only" +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// require.NoError(t, e.Setup()) + +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "passing"})) +// t.Log(buff.String()) +// assert.Empty(t, buff.String()) +// } + +// func TestOutputGroupErrorOnlyShowsOutputOnFailure(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/output_group_error_only" +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// require.NoError(t, e.Setup()) + +// require.Error(t, e.Run(context.Background(), &task.Call{Task: "failing"})) +// t.Log(buff.String()) +// assert.Contains(t, "failing-output", strings.TrimSpace(buff.String())) +// assert.NotContains(t, "passing", strings.TrimSpace(buff.String())) +// } + +// func TestIncludedVars(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/include_with_vars" +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// require.NoError(t, e.Setup()) + +// expectedOutputOrder := strings.TrimSpace(` +// task: [included1:task1] echo "VAR_1 is included1-var1" +// VAR_1 is included1-var1 +// task: [included1:task1] echo "VAR_2 is included-default-var2" +// VAR_2 is included-default-var2 +// task: [included2:task1] echo "VAR_1 is included2-var1" +// VAR_1 is included2-var1 +// task: [included2:task1] echo "VAR_2 is included-default-var2" +// VAR_2 is included-default-var2 +// task: [included3:task1] echo "VAR_1 is included-default-var1" +// VAR_1 is included-default-var1 +// task: [included3:task1] echo "VAR_2 is included-default-var2" +// VAR_2 is included-default-var2 +// `) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "task1"})) +// t.Log(buff.String()) +// assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder) +// } + +// func TestIncludeWithVarsInInclude(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/include_with_vars_inside_include" +// var buff bytes.Buffer +// e := task.Executor{ +// Dir: dir, +// Stdout: &buff, +// Stderr: &buff, +// } +// require.NoError(t, e.Setup()) +// } + +// func TestIncludedVarsMultiLevel(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/include_with_vars_multi_level" +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// require.NoError(t, e.Setup()) + +// expectedOutputOrder := strings.TrimSpace(` +// task: [lib:greet] echo 'Hello world' +// Hello world +// task: [foo:lib:greet] echo 'Hello foo' +// Hello foo +// task: [bar:lib:greet] echo 'Hello bar' +// Hello bar +// `) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"})) +// t.Log(buff.String()) +// assert.Equal(t, expectedOutputOrder, strings.TrimSpace(buff.String())) +// } + +// func TestErrorCode(t *testing.T) { +// t.Parallel() + +// const dir = "testdata/error_code" +// tests := []struct { +// name string +// task string +// expected int +// }{ +// { +// name: "direct task", +// task: "direct", +// expected: 42, +// }, { +// name: "indirect task", +// task: "indirect", +// expected: 42, +// }, +// } + +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithSilent(true), +// ) +// require.NoError(t, e.Setup()) + +// err := e.Run(context.Background(), &task.Call{Task: test.task}) +// require.Error(t, err) +// taskRunErr, ok := err.(*errors.TaskRunError) +// assert.True(t, ok, "cannot cast returned error to *task.TaskRunError") +// assert.Equal(t, test.expected, taskRunErr.TaskExitCode(), "unexpected exit code from task") +// }) +// } +// } + +// func TestEvaluateSymlinksInPaths(t *testing.T) { // nolint:paralleltest // cannot run in parallel +// const dir = "testdata/evaluate_symlinks_in_paths" +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithSilent(false), +// ) +// tests := []struct { +// name string +// task string +// expected string +// }{ +// { +// name: "default (1)", +// task: "default", +// expected: "task: [default] echo \"some job\"\nsome job", +// }, +// { +// name: "test-sym (1)", +// task: "test-sym", +// expected: "task: [test-sym] echo \"shared file source changed\" > src/shared/b", +// }, +// { +// name: "default (2)", +// task: "default", +// expected: "task: [default] echo \"some job\"\nsome job", +// }, +// { +// name: "default (3)", +// task: "default", +// expected: `task: Task "default" is up to date`, +// }, +// { +// name: "reset", +// task: "reset", +// expected: "task: [reset] echo \"shared file source\" > src/shared/b\ntask: [reset] echo \"file source\" > src/a", +// }, +// } +// for _, test := range tests { // nolint:paralleltest // cannot run in parallel +// t.Run(test.name, func(t *testing.T) { +// require.NoError(t, e.Setup()) +// err := e.Run(context.Background(), &task.Call{Task: test.task}) +// require.NoError(t, err) +// assert.Equal(t, test.expected, strings.TrimSpace(buff.String())) +// buff.Reset() +// }) +// } +// err := os.RemoveAll(dir + "/.task") +// require.NoError(t, err) +// } + +// func TestTaskfileWalk(t *testing.T) { +// t.Parallel() + +// tests := []struct { +// name string +// dir string +// expected string +// }{ +// { +// name: "walk from root directory", +// dir: "testdata/taskfile_walk", +// expected: "foo\n", +// }, { +// name: "walk from sub directory", +// dir: "testdata/taskfile_walk/foo", +// expected: "foo\n", +// }, { +// name: "walk from sub sub directory", +// dir: "testdata/taskfile_walk/foo/bar", +// expected: "foo\n", +// }, +// } +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir(test.dir), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// require.NoError(t, e.Setup()) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"})) +// assert.Equal(t, test.expected, buff.String()) +// }) +// } +// } + +// func TestUserWorkingDirectory(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir("testdata/user_working_dir"), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// wd, err := os.Getwd() +// require.NoError(t, err) +// require.NoError(t, e.Setup()) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"})) +// assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String()) +// } + +// func TestUserWorkingDirectoryWithIncluded(t *testing.T) { +// t.Parallel() + +// wd, err := os.Getwd() +// require.NoError(t, err) + +// wd = filepathext.SmartJoin(wd, "testdata/user_working_dir_with_includes/somedir") + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir("testdata/user_working_dir_with_includes"), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// e.UserWorkingDir = wd + +// require.NoError(t, err) +// require.NoError(t, e.Setup()) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "included:echo"})) +// assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String()) +// } + +// func TestPlatforms(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir("testdata/platforms"), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// require.NoError(t, e.Setup()) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build-" + runtime.GOOS})) +// assert.Equal(t, fmt.Sprintf("task: [build-%s] echo 'Running task on %s'\nRunning task on %s\n", runtime.GOOS, runtime.GOOS, runtime.GOOS), buff.String()) +// } + +// func TestPOSIXShellOptsGlobalLevel(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir("testdata/shopts/global_level"), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// require.NoError(t, e.Setup()) + +// err := e.Run(context.Background(), &task.Call{Task: "pipefail"}) +// require.NoError(t, err) +// assert.Equal(t, "pipefail\ton\n", buff.String()) +// } + +// func TestPOSIXShellOptsTaskLevel(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir("testdata/shopts/task_level"), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// require.NoError(t, e.Setup()) + +// err := e.Run(context.Background(), &task.Call{Task: "pipefail"}) +// require.NoError(t, err) +// assert.Equal(t, "pipefail\ton\n", buff.String()) +// } + +// func TestPOSIXShellOptsCommandLevel(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir("testdata/shopts/command_level"), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// require.NoError(t, e.Setup()) + +// err := e.Run(context.Background(), &task.Call{Task: "pipefail"}) +// require.NoError(t, err) +// assert.Equal(t, "pipefail\ton\n", buff.String()) +// } + +// func TestBashShellOptsGlobalLevel(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir("testdata/shopts/global_level"), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// require.NoError(t, e.Setup()) + +// err := e.Run(context.Background(), &task.Call{Task: "globstar"}) +// require.NoError(t, err) +// assert.Equal(t, "globstar\ton\n", buff.String()) +// } + +// func TestBashShellOptsTaskLevel(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir("testdata/shopts/task_level"), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// require.NoError(t, e.Setup()) + +// err := e.Run(context.Background(), &task.Call{Task: "globstar"}) +// require.NoError(t, err) +// assert.Equal(t, "globstar\ton\n", buff.String()) +// } + +// func TestBashShellOptsCommandLevel(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir("testdata/shopts/command_level"), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// ) +// require.NoError(t, e.Setup()) + +// err := e.Run(context.Background(), &task.Call{Task: "globstar"}) +// require.NoError(t, err) +// assert.Equal(t, "globstar\ton\n", buff.String()) +// } + +// func TestSplitArgs(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir("testdata/split_args"), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithSilent(true), +// ) +// require.NoError(t, e.Setup()) + +// vars := ast.NewVars() +// vars.Set("CLI_ARGS", ast.Var{Value: "foo bar 'foo bar baz'"}) + +// err := e.Run(context.Background(), &task.Call{Task: "default", Vars: vars}) +// require.NoError(t, err) +// assert.Equal(t, "3\n", buff.String()) +// } + +// func TestSingleCmdDep(t *testing.T) { +// t.Parallel() + +// tt := fileContentTest{ +// Dir: "testdata/single_cmd_dep", +// Target: "foo", +// Files: map[string]string{ +// "foo.txt": "foo\n", +// "bar.txt": "bar\n", +// }, +// } +// t.Run("", func(t *testing.T) { +// t.Parallel() +// tt.Run(t) +// }) +// } + +// func TestSilence(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir("testdata/silent"), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithSilent(false), +// ) +// require.NoError(t, e.Setup()) + +// // First verify that the silent flag is in place. +// fetchedTask, err := e.GetTask(&task.Call{Task: "task-test-silent-calls-chatty-silenced"}) +// require.NoError(t, err, "Unable to look up task task-test-silent-calls-chatty-silenced") +// require.True(t, fetchedTask.Cmds[0].Silent, "The task task-test-silent-calls-chatty-silenced should have a silent call to chatty") + +// // Then test the two basic cases where the task is silent or not. +// // A silenced task. +// err = e.Run(context.Background(), &task.Call{Task: "silent"}) +// require.NoError(t, err) +// require.Empty(t, buff.String(), "siWhile running lent: Expected not see output, because the task is silent") + +// buff.Reset() + +// // A chatty (not silent) task. +// err = e.Run(context.Background(), &task.Call{Task: "chatty"}) +// require.NoError(t, err) +// require.NotEmpty(t, buff.String(), "chWhile running atty: Expected to see output, because the task is not silent") + +// buff.Reset() + +// // Then test invoking the two task from other tasks. +// // A silenced task that calls a chatty task. +// err = e.Run(context.Background(), &task.Call{Task: "task-test-silent-calls-chatty-non-silenced"}) +// require.NoError(t, err) +// require.NotEmpty(t, buff.String(), "While running task-test-silent-calls-chatty-non-silenced: Expected to see output. The task is silenced, but the called task is not. Silence does not propagate to called tasks.") + +// buff.Reset() + +// // A silent task that does a silent call to a chatty task. +// err = e.Run(context.Background(), &task.Call{Task: "task-test-silent-calls-chatty-silenced"}) +// require.NoError(t, err) +// require.Empty(t, buff.String(), "While running task-test-silent-calls-chatty-silenced: Expected not to see output. The task calls chatty task, but the call is silenced.") + +// buff.Reset() + +// // A chatty task that does a call to a chatty task. +// err = e.Run(context.Background(), &task.Call{Task: "task-test-chatty-calls-chatty-non-silenced"}) +// require.NoError(t, err) +// require.NotEmpty(t, buff.String(), "While running task-test-chatty-calls-chatty-non-silenced: Expected to see output. Both caller and callee are chatty and not silenced.") + +// buff.Reset() + +// // A chatty task that does a silenced call to a chatty task. +// err = e.Run(context.Background(), &task.Call{Task: "task-test-chatty-calls-chatty-silenced"}) +// require.NoError(t, err) +// require.NotEmpty(t, buff.String(), "While running task-test-chatty-calls-chatty-silenced: Expected to see output. Call to a chatty task is silenced, but the parent task is not.") + +// buff.Reset() + +// // A chatty task with no cmd's of its own that does a silenced call to a chatty task. +// err = e.Run(context.Background(), &task.Call{Task: "task-test-no-cmds-calls-chatty-silenced"}) +// require.NoError(t, err) +// require.Empty(t, buff.String(), "While running task-test-no-cmds-calls-chatty-silenced: Expected not to see output. While the task itself is not silenced, it does not have any cmds and only does an invocation of a silenced task.") + +// buff.Reset() + +// // A chatty task that does a silenced invocation of a task. +// err = e.Run(context.Background(), &task.Call{Task: "task-test-chatty-calls-silenced-cmd"}) +// require.NoError(t, err) +// require.Empty(t, buff.String(), "While running task-test-chatty-calls-silenced-cmd: Expected not to see output. While the task itself is not silenced, its call to the chatty task is silent.") + +// buff.Reset() + +// // Then test calls via dependencies. +// // A silent task that depends on a chatty task. +// err = e.Run(context.Background(), &task.Call{Task: "task-test-is-silent-depends-on-chatty-non-silenced"}) +// require.NoError(t, err) +// require.NotEmpty(t, buff.String(), "While running task-test-is-silent-depends-on-chatty-non-silenced: Expected to see output. The task is silent and depends on a chatty task. Dependencies does not inherit silence.") + +// buff.Reset() + +// // A silent task that depends on a silenced chatty task. +// err = e.Run(context.Background(), &task.Call{Task: "task-test-is-silent-depends-on-chatty-silenced"}) +// require.NoError(t, err) +// require.Empty(t, buff.String(), "While running task-test-is-silent-depends-on-chatty-silenced: Expected not to see output. The task is silent and has a silenced dependency on a chatty task.") + +// buff.Reset() + +// // A chatty task that, depends on a silenced chatty task. +// err = e.Run(context.Background(), &task.Call{Task: "task-test-is-chatty-depends-on-chatty-silenced"}) +// require.NoError(t, err) +// require.Empty(t, buff.String(), "While running task-test-is-chatty-depends-on-chatty-silenced: Expected not to see output. The task is chatty but does not have commands and has a silenced dependency on a chatty task.") + +// buff.Reset() +// } + +// func TestForce(t *testing.T) { +// t.Parallel() + +// tests := []struct { +// name string +// env map[string]string +// force bool +// forceAll bool +// }{ +// { +// name: "force", +// force: true, +// }, +// { +// name: "force-all", +// forceAll: true, +// }, +// { +// name: "force with gentle force experiment", +// force: true, +// env: map[string]string{ +// "TASK_X_GENTLE_FORCE": "1", +// }, +// }, +// { +// name: "force-all with gentle force experiment", +// forceAll: true, +// env: map[string]string{ +// "TASK_X_GENTLE_FORCE": "1", +// }, +// }, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// t.Parallel() + +// var buff bytes.Buffer +// e := task.NewExecutor( +// task.WithDir("testdata/force"), +// task.WithStdout(&buff), +// task.WithStderr(&buff), +// task.WithForce(tt.force), +// task.WithForceAll(tt.forceAll), +// ) +// require.NoError(t, e.Setup()) +// require.NoError(t, e.Run(context.Background(), &task.Call{Task: "task-with-dep"})) +// }) +// } +// } // enableExperimentForTest enables the experiment behind pointer e for the duration of test t and sub-tests, // with the experiment being restored to its previous state when tests complete. diff --git a/taskfile/node.go b/taskfile/node.go index 3dc3344c6c..7824d6fee4 100644 --- a/taskfile/node.go +++ b/taskfile/node.go @@ -3,7 +3,6 @@ package taskfile import ( "context" "strings" - "time" giturls "github.com/chainguard-dev/git-urls" @@ -33,7 +32,6 @@ func NewRootNode( entrypoint string, dir string, insecure bool, - timeout time.Duration, ) (Node, error) { dir = fsext.DefaultDir(entrypoint, dir) // If the entrypoint is "-", we read from stdin diff --git a/temp_dir.go b/temp_dir.go new file mode 100644 index 0000000000..98ed8ecf71 --- /dev/null +++ b/temp_dir.go @@ -0,0 +1,78 @@ +package task + +import ( + "path/filepath" + "strings" + + "github.com/go-task/task/v3/internal/env" + "github.com/go-task/task/v3/internal/execext" + "github.com/go-task/task/v3/internal/filepathext" +) + +type TempDir struct { + Remote string + Fingerprint string +} + +func NewTempDir(dir string) (*TempDir, error) { + tempDir, err := setupTempDirFingerprint(dir) + if err != nil { + return nil, err + } + + err = setupTempDirRemote(dir, tempDir) + if err != nil { + return nil, err + } + + return tempDir, nil +} + +func setupTempDirFingerprint(dir string) (*TempDir, error) { + tempDir := env.GetTaskEnv("TEMP_DIR") + + if tempDir == "" { + return &TempDir{ + Remote: filepathext.SmartJoin(dir, ".task"), + Fingerprint: filepathext.SmartJoin(dir, ".task"), + }, nil + } + + if filepath.IsAbs(tempDir) || strings.HasPrefix(tempDir, "~") { + tempDir, err := execext.ExpandLiteral(tempDir) + if err != nil { + return nil, err + } + projectDir, _ := filepath.Abs(dir) + projectName := filepath.Base(projectDir) + return &TempDir{ + Remote: tempDir, + Fingerprint: filepathext.SmartJoin(tempDir, projectName), + }, nil + } + + return &TempDir{ + Remote: filepathext.SmartJoin(dir, tempDir), + Fingerprint: filepathext.SmartJoin(dir, tempDir), + }, nil +} + +func setupTempDirRemote(dir string, tempDir *TempDir) error { + remoteDir := env.GetTaskEnv("REMOTE_DIR") + + if remoteDir == "" { + return nil + } + + if filepath.IsAbs(remoteDir) || strings.HasPrefix(remoteDir, "~") { + remoteTempDir, err := execext.ExpandLiteral(remoteDir) + if err != nil { + return err + } + tempDir.Remote = remoteTempDir + return nil + } + + tempDir.Remote = filepathext.SmartJoin(dir, ".task") + return nil +} diff --git a/testdata/empty_taskfile/testdata/TestEmptyTaskfile-err-setup.golden b/testdata/empty_taskfile/testdata/TestEmptyTaskfile-err-reader.golden similarity index 100% rename from testdata/empty_taskfile/testdata/TestEmptyTaskfile-err-setup.golden rename to testdata/empty_taskfile/testdata/TestEmptyTaskfile-err-reader.golden diff --git a/testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect-err-setup.golden b/testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect-err-reader.golden similarity index 100% rename from testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect-err-setup.golden rename to testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect-err-reader.golden diff --git a/testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-root-dir.golden b/testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-root-dir.golden similarity index 100% rename from testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-root-dir.golden rename to testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-root-dir.golden diff --git a/testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-task.golden b/testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-task.golden similarity index 100% rename from testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-task.golden rename to testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-task.golden diff --git a/testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-taskfile-dir.golden b/testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-taskfile-dir.golden similarity index 100% rename from testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-taskfile-dir.golden rename to testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-taskfile-dir.golden diff --git a/testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-taskfile.golden b/testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-taskfile.golden similarity index 100% rename from testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-taskfile.golden rename to testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-taskfile.golden diff --git a/testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-root-dir.golden b/testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-subdir-print-root-dir.golden similarity index 100% rename from testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-root-dir.golden rename to testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-subdir-print-root-dir.golden diff --git a/testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-task-dir.golden b/testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-subdir-print-task-dir.golden similarity index 100% rename from testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-task-dir.golden rename to testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-subdir-print-task-dir.golden diff --git a/testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-task.golden b/testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-subdir-print-task.golden similarity index 100% rename from testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-task.golden rename to testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-subdir-print-task.golden diff --git a/testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-taskfile-dir.golden b/testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-subdir-print-taskfile-dir.golden similarity index 100% rename from testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-taskfile-dir.golden rename to testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-subdir-print-taskfile-dir.golden diff --git a/testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-taskfile.golden b/testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-subdir-print-taskfile.golden similarity index 100% rename from testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-taskfile.golden rename to testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-subdir-print-taskfile.golden diff --git a/testdata/wildcards/testdata/TestWildcard-basic_wildcard.golden b/testdata/wildcards/testdata/TestWildcard-basic_wildcard.golden new file mode 100644 index 0000000000..dc1b2474ca --- /dev/null +++ b/testdata/wildcards/testdata/TestWildcard-basic_wildcard.golden @@ -0,0 +1 @@ +Hello foo diff --git a/testdata/wildcards/testdata/TestWildcard-double_wildcard.golden b/testdata/wildcards/testdata/TestWildcard-double_wildcard.golden new file mode 100644 index 0000000000..e2781e68bd --- /dev/null +++ b/testdata/wildcards/testdata/TestWildcard-double_wildcard.golden @@ -0,0 +1 @@ +Hello foo bar diff --git a/testdata/wildcards/testdata/TestWildcard-matches_exactly.golden b/testdata/wildcards/testdata/TestWildcard-matches_exactly.golden new file mode 100644 index 0000000000..37963f7996 --- /dev/null +++ b/testdata/wildcards/testdata/TestWildcard-matches_exactly.golden @@ -0,0 +1 @@ +I don't consume matches: [] diff --git a/testdata/wildcards/testdata/TestWildcard-multiple_matches.golden b/testdata/wildcards/testdata/TestWildcard-multiple_matches.golden new file mode 100644 index 0000000000..08332b29f0 --- /dev/null +++ b/testdata/wildcards/testdata/TestWildcard-multiple_matches.golden @@ -0,0 +1 @@ +Hello foo-bar diff --git a/testdata/wildcards/testdata/TestWildcard-no_matches-err-run.golden b/testdata/wildcards/testdata/TestWildcard-no_matches-err-run.golden new file mode 100644 index 0000000000..87b1f384a8 --- /dev/null +++ b/testdata/wildcards/testdata/TestWildcard-no_matches-err-run.golden @@ -0,0 +1 @@ +task: Task "no-match" does not exist \ No newline at end of file diff --git a/testdata/wildcards/testdata/TestWildcard-no_matches.golden b/testdata/wildcards/testdata/TestWildcard-no_matches.golden new file mode 100644 index 0000000000..56e8128e82 --- /dev/null +++ b/testdata/wildcards/testdata/TestWildcard-no_matches.golden @@ -0,0 +1 @@ +task: No tasks with description available. Try --list-all to list all tasks diff --git a/testdata/wildcards/testdata/TestWildcard-store_wildcard.golden b/testdata/wildcards/testdata/TestWildcard-store_wildcard.golden new file mode 100644 index 0000000000..571f4d0afb --- /dev/null +++ b/testdata/wildcards/testdata/TestWildcard-store_wildcard.golden @@ -0,0 +1 @@ +Starting foo