diff --git a/task_test.go b/task_test.go index 8b6a005e2b..3411c5ebd0 100644 --- a/task_test.go +++ b/task_test.go @@ -5,6 +5,9 @@ import ( "context" "fmt" "io" + "io/fs" + "net/http" + "net/http/httptest" "os" "path/filepath" "regexp" @@ -12,6 +15,7 @@ import ( "strings" "sync" "testing" + "time" "github.com/Masterminds/semver/v3" "github.com/stretchr/testify/assert" @@ -21,6 +25,7 @@ import ( "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/experiments" "github.com/go-task/task/v3/internal/filepathext" + "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/taskfile/ast" ) @@ -1086,6 +1091,88 @@ func TestIncludesEmptyMain(t *testing.T) { 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.Executor{ + Entrypoint: entrypoint, + Dir: dir, + Stdout: &buff, + Stderr: &buff, + Insecure: true, + Download: true, + AssumeYes: true, + Logger: &logger.Logger{Stdout: &buff, Stderr: &buff, Verbose: true}, + Timeout: 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) { + task, err := e.CompiledTask(&ast.Call{Task: tc.name}) + require.NoError(t, err) + assert.Equal(t, tc.dir, task.Dir) + }) + } + }) + } + }) + } +} + func TestIncludesDependencies(t *testing.T) { tt := fileContentTest{ Dir: "testdata/includes_deps", @@ -2616,3 +2703,18 @@ func TestReference(t *testing.T) { }) } } + +// 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. +// +// Typically experiments are controlled via TASK_X_ env vars, but we cannot use those in tests +// because the experiment settings are parsed during experiments.init(), before any tests run. +func enableExperimentForTest(t *testing.T, e *experiments.Experiment, val string) { + prev := *e + *e = experiments.Experiment{ + Name: prev.Name, + Enabled: true, + Value: val, + } + t.Cleanup(func() { *e = prev }) +} diff --git a/taskfile/node_http.go b/taskfile/node_http.go index d6fb2201bf..fc9058487f 100644 --- a/taskfile/node_http.go +++ b/taskfile/node_http.go @@ -110,8 +110,12 @@ func (node *HTTPNode) ResolveDir(dir string) (string, error) { // NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory // This means that files are included relative to one another - entrypointDir := filepath.Dir(node.Dir()) - return filepathext.SmartJoin(entrypointDir, path), nil + parent := node.Dir() + if node.Parent() != nil { + parent = node.Parent().Dir() + } + + return filepathext.SmartJoin(parent, path), nil } func (node *HTTPNode) FilenameAndLastDir() (string, string) { diff --git a/testdata/includes_http/child-taskfile2.yml b/testdata/includes_http/child-taskfile2.yml new file mode 100644 index 0000000000..84690ed588 --- /dev/null +++ b/testdata/includes_http/child-taskfile2.yml @@ -0,0 +1,9 @@ +version: '3' + +includes: + third-with-dir-1: + taskfile: "{{.INCLUDE_ROOT}}/child-taskfile3.yml" + dir: ./dir-1 + third-with-dir-2: + taskfile: "{{.INCLUDE_ROOT}}/child-taskfile3.yml" + dir: ./dir-2 diff --git a/testdata/includes_http/child-taskfile3.yml b/testdata/includes_http/child-taskfile3.yml new file mode 100644 index 0000000000..3449a19b42 --- /dev/null +++ b/testdata/includes_http/child-taskfile3.yml @@ -0,0 +1,4 @@ +version: '3' + +tasks: + default: "true" diff --git a/testdata/includes_http/root-taskfile-remotefile-empty-dir-1st.yml b/testdata/includes_http/root-taskfile-remotefile-empty-dir-1st.yml new file mode 100644 index 0000000000..ff948f3961 --- /dev/null +++ b/testdata/includes_http/root-taskfile-remotefile-empty-dir-1st.yml @@ -0,0 +1,8 @@ +version: '3' + +includes: + second-no-dir: + taskfile: "{{.INCLUDE_ROOT}}/child-taskfile2.yml" + second-with-dir-1: + taskfile: "{{.INCLUDE_ROOT}}/child-taskfile2.yml" + dir: ./dir-1 diff --git a/testdata/includes_http/root-taskfile-remotefile-empty-dir-2nd.yml b/testdata/includes_http/root-taskfile-remotefile-empty-dir-2nd.yml new file mode 100644 index 0000000000..69080d55d8 --- /dev/null +++ b/testdata/includes_http/root-taskfile-remotefile-empty-dir-2nd.yml @@ -0,0 +1,8 @@ +version: '3' + +includes: + second-with-dir-1: + taskfile: "{{.INCLUDE_ROOT}}/child-taskfile2.yml" + dir: ./dir-1 + second-no-dir: + taskfile: "{{.INCLUDE_ROOT}}/child-taskfile2.yml"