mirror of
https://github.com/go-task/task.git
synced 2025-04-21 12:17:07 +02:00
* change what is printed when creating Taskfile When using --init to create a new Taskfile, it used to print the whole contents of the file to the terminal, which was unnecessarily verbose (and honestly felt unintentional). Now only the filename is printed by default and the --silent and --verbose flags can be used to control the behavior (print nothing or content + filename, respectively). * include additional new line with -i -v it looks slightly better in the terminal. * print init success text in green * fix TestInit, create and pass in a logger * move logging outside of InitTaskfile - revert API changes made to InitTaskfile - make consts in init.go public so they can be accessed from task.go - rename variable "logger" to "log" in task.go to fix conflict with logger package * move TestInit into init_test.go file as requested by pd93.
3222 lines
79 KiB
Go
3222 lines
79 KiB
Go
package task_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
rand "math/rand/v2"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
"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/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"
|
|
)
|
|
|
|
func init() {
|
|
_ = os.Setenv("NO_COLOR", "1")
|
|
}
|
|
|
|
// SyncBuffer is a threadsafe buffer for testing.
|
|
// Some times replace stdout/stderr with a buffer to capture output.
|
|
// stdout and stderr are threadsafe, but a regular bytes.Buffer is not.
|
|
// Using this instead helps prevents race conditions with output.
|
|
type SyncBuffer struct {
|
|
buf bytes.Buffer
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func (sb *SyncBuffer) Write(p []byte) (n int, err error) {
|
|
sb.mu.Lock()
|
|
defer sb.mu.Unlock()
|
|
return sb.buf.Write(p)
|
|
}
|
|
|
|
// fileContentTest provides a basic reusable test-case for running a Taskfile
|
|
// and inspect generated files.
|
|
type fileContentTest struct {
|
|
Dir string
|
|
Entrypoint string
|
|
Target string
|
|
TrimSpace bool
|
|
Files map[string]string
|
|
}
|
|
|
|
func (fct fileContentTest) name(file string) string {
|
|
return fmt.Sprintf("target=%q,file=%q", fct.Target, file)
|
|
}
|
|
|
|
func (fct fileContentTest) Run(t *testing.T) {
|
|
t.Helper()
|
|
|
|
for f := range fct.Files {
|
|
_ = os.Remove(filepathext.SmartJoin(fct.Dir, f))
|
|
}
|
|
e := &task.Executor{
|
|
Dir: fct.Dir,
|
|
TempDir: task.TempDir{
|
|
Remote: filepathext.SmartJoin(fct.Dir, ".task"),
|
|
Fingerprint: filepathext.SmartJoin(fct.Dir, ".task"),
|
|
},
|
|
Entrypoint: fct.Entrypoint,
|
|
Stdout: io.Discard,
|
|
Stderr: io.Discard,
|
|
}
|
|
|
|
require.NoError(t, e.Setup(), "e.Setup()")
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: fct.Target}), "e.Run(target)")
|
|
for name, expectContent := range fct.Files {
|
|
t.Run(fct.name(name), func(t *testing.T) {
|
|
path := filepathext.SmartJoin(e.Dir, name)
|
|
b, err := os.ReadFile(path)
|
|
require.NoError(t, err, "Error reading file")
|
|
s := string(b)
|
|
if fct.TrimSpace {
|
|
s = strings.TrimSpace(s)
|
|
}
|
|
assert.Equal(t, expectContent, s, "unexpected file content in %s", path)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEmptyTask(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
e := &task.Executor{
|
|
Dir: "testdata/empty_task",
|
|
Stdout: io.Discard,
|
|
Stderr: io.Discard,
|
|
}
|
|
require.NoError(t, e.Setup(), "e.Setup()")
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "default"}))
|
|
}
|
|
|
|
func TestEmptyTaskfile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
e := &task.Executor{
|
|
Dir: "testdata/empty_taskfile",
|
|
Stdout: io.Discard,
|
|
Stderr: io.Discard,
|
|
}
|
|
require.Error(t, e.Setup(), "e.Setup()")
|
|
}
|
|
|
|
func TestEnv(t *testing.T) {
|
|
t.Setenv("QUX", "from_os")
|
|
tt := fileContentTest{
|
|
Dir: "testdata/env",
|
|
Target: "default",
|
|
TrimSpace: false,
|
|
Files: map[string]string{
|
|
"local.txt": "GOOS='linux' GOARCH='amd64' CGO_ENABLED='0'\n",
|
|
"global.txt": "FOO='foo' BAR='overridden' BAZ='baz'\n",
|
|
"multiple_type.txt": "FOO='1' BAR='true' BAZ='1.1'\n",
|
|
"not-overridden.txt": "QUX='from_os'\n",
|
|
"dynamic.txt": "foo\n",
|
|
},
|
|
}
|
|
tt.Run(t)
|
|
t.Setenv("TASK_X_ENV_PRECEDENCE", "1")
|
|
experiments.EnvPrecedence = experiments.New("ENV_PRECEDENCE")
|
|
ttt := fileContentTest{
|
|
Dir: "testdata/env",
|
|
Target: "overridden",
|
|
TrimSpace: false,
|
|
Files: map[string]string{
|
|
"overridden.txt": "QUX='from_taskfile'\n",
|
|
},
|
|
}
|
|
ttt.Run(t)
|
|
}
|
|
|
|
func TestVars(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tt := fileContentTest{
|
|
Dir: "testdata/vars",
|
|
Target: "default",
|
|
Files: map[string]string{
|
|
"missing-var.txt": "\n",
|
|
"var-order.txt": "ABCDEF\n",
|
|
"dependent-sh.txt": "123456\n",
|
|
"with-call.txt": "Hi, ABC123!\n",
|
|
"from-dot-env.txt": "From .env file\n",
|
|
},
|
|
}
|
|
t.Run("", func(t *testing.T) {
|
|
t.Parallel()
|
|
tt.Run(t)
|
|
})
|
|
}
|
|
|
|
func TestRequires(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/requires"
|
|
|
|
var buff bytes.Buffer
|
|
e := &task.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
|
|
require.NoError(t, e.Setup())
|
|
require.ErrorContains(t, e.Run(context.Background(), &ast.Call{Task: "missing-var"}), "task: Task \"missing-var\" cancelled because it is missing required variables: foo")
|
|
buff.Reset()
|
|
require.NoError(t, e.Setup())
|
|
|
|
vars := ast.NewVars()
|
|
vars.Set("foo", ast.Var{Value: "bar"})
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{
|
|
Task: "missing-var",
|
|
Vars: vars,
|
|
}))
|
|
buff.Reset()
|
|
|
|
require.NoError(t, e.Setup())
|
|
require.ErrorContains(t, e.Run(context.Background(), &ast.Call{Task: "validation-var", Vars: vars}), "task: Task \"validation-var\" cancelled because it is missing required variables:\n - foo has an invalid value : 'bar' (allowed values : [one two])")
|
|
buff.Reset()
|
|
|
|
require.NoError(t, e.Setup())
|
|
vars.Set("foo", ast.Var{Value: "one"})
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "validation-var", Vars: vars}))
|
|
buff.Reset()
|
|
|
|
require.NoError(t, e.Setup())
|
|
require.ErrorContains(t, e.Run(context.Background(), &ast.Call{Task: "require-before-compile"}), "task: Task \"require-before-compile\" cancelled because it is missing required variables: MY_VAR")
|
|
buff.Reset()
|
|
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "var-defined-in-task"}))
|
|
buff.Reset()
|
|
}
|
|
|
|
func TestSpecialVars(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/special_vars"
|
|
const subdir = "testdata/special_vars/subdir"
|
|
toAbs := func(rel string) string {
|
|
abs, err := filepath.Abs(rel)
|
|
assert.NoError(t, err)
|
|
return abs
|
|
}
|
|
|
|
tests := []struct {
|
|
target string
|
|
expected string
|
|
}{
|
|
// Root
|
|
{target: "print-task", expected: "print-task"},
|
|
{target: "print-root-dir", expected: toAbs(dir)},
|
|
{target: "print-taskfile", expected: toAbs(dir) + "/Taskfile.yml"},
|
|
{target: "print-taskfile-dir", expected: toAbs(dir)},
|
|
{target: "print-task-version", expected: "unknown"},
|
|
{target: "print-task-dir", expected: toAbs(dir) + "/foo"},
|
|
// Included
|
|
{target: "included:print-task", expected: "included:print-task"},
|
|
{target: "included:print-root-dir", expected: toAbs(dir)},
|
|
{target: "included:print-taskfile", expected: toAbs(dir) + "/included/Taskfile.yml"},
|
|
{target: "included:print-taskfile-dir", expected: toAbs(dir) + "/included"},
|
|
{target: "included:print-task-version", expected: "unknown"},
|
|
}
|
|
|
|
for _, dir := range []string{dir, subdir} {
|
|
for _, test := range tests {
|
|
t.Run(test.target, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var buff bytes.Buffer
|
|
e := &task.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: true,
|
|
EnableVersionCheck: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: test.target}))
|
|
assert.Equal(t, test.expected+"\n", buff.String())
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConcurrency(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
dir = "testdata/concurrency"
|
|
target = "default"
|
|
)
|
|
|
|
e := &task.Executor{
|
|
Dir: dir,
|
|
Stdout: io.Discard,
|
|
Stderr: io.Discard,
|
|
Concurrency: 1,
|
|
}
|
|
require.NoError(t, e.Setup(), "e.Setup()")
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: target}), "e.Run(target)")
|
|
}
|
|
|
|
func TestParams(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tt := fileContentTest{
|
|
Dir: "testdata/params",
|
|
Target: "default",
|
|
TrimSpace: false,
|
|
Files: map[string]string{
|
|
"hello.txt": "Hello\n",
|
|
"world.txt": "World\n",
|
|
"exclamation.txt": "!\n",
|
|
"dep1.txt": "Dependence1\n",
|
|
"dep2.txt": "Dependence2\n",
|
|
"spanish.txt": "¡Holla mundo!\n",
|
|
"spanish-dep.txt": "¡Holla dependencia!\n",
|
|
"portuguese.txt": "Olá, mundo!\n",
|
|
"portuguese2.txt": "Olá, mundo!\n",
|
|
"german.txt": "Welt!\n",
|
|
},
|
|
}
|
|
t.Run("", func(t *testing.T) {
|
|
t.Parallel()
|
|
tt.Run(t)
|
|
})
|
|
}
|
|
|
|
func TestDeps(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/deps"
|
|
|
|
files := []string{
|
|
"d1.txt",
|
|
"d2.txt",
|
|
"d3.txt",
|
|
"d11.txt",
|
|
"d12.txt",
|
|
"d13.txt",
|
|
"d21.txt",
|
|
"d22.txt",
|
|
"d23.txt",
|
|
"d31.txt",
|
|
"d32.txt",
|
|
"d33.txt",
|
|
}
|
|
|
|
for _, f := range files {
|
|
_ = os.Remove(filepathext.SmartJoin(dir, f))
|
|
}
|
|
|
|
e := &task.Executor{
|
|
Dir: dir,
|
|
Stdout: io.Discard,
|
|
Stderr: io.Discard,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "default"}))
|
|
|
|
for _, f := range files {
|
|
f = filepathext.SmartJoin(dir, f)
|
|
if _, err := os.Stat(f); err != nil {
|
|
t.Errorf("File %s should exist", f)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStatus(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/status"
|
|
|
|
files := []string{
|
|
"foo.txt",
|
|
"bar.txt",
|
|
"baz.txt",
|
|
}
|
|
|
|
for _, f := range files {
|
|
path := filepathext.SmartJoin(dir, f)
|
|
_ = os.Remove(path)
|
|
if _, err := os.Stat(path); err == nil {
|
|
t.Errorf("File should not exist: %v", err)
|
|
}
|
|
}
|
|
|
|
var buff bytes.Buffer
|
|
e := &task.Executor{
|
|
Dir: dir,
|
|
TempDir: task.TempDir{
|
|
Remote: filepathext.SmartJoin(dir, ".task"),
|
|
Fingerprint: filepathext.SmartJoin(dir, ".task"),
|
|
},
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
// gen-foo creates foo.txt, and will always fail it's status check.
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "gen-foo"}))
|
|
// gen-foo creates bar.txt, and will pass its status-check the 3. time it
|
|
// is run. It creates bar.txt, but also lists it as its source. So, the checksum
|
|
// for the file won't match before after the second run as we the file
|
|
// only exists after the first run.
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "gen-bar"}))
|
|
// gen-silent-baz is marked as being silent, and should only produce output
|
|
// if e.Verbose is set to true.
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "gen-silent-baz"}))
|
|
|
|
for _, f := range files {
|
|
if _, err := os.Stat(filepathext.SmartJoin(dir, f)); err != nil {
|
|
t.Errorf("File should exist: %v", err)
|
|
}
|
|
}
|
|
|
|
// Run gen-bar a second time to produce a checksum file that matches bar.txt
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "gen-bar"}))
|
|
|
|
// Run gen-bar a third time, to make sure we've triggered the status check.
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "gen-bar"}))
|
|
|
|
// We're silent, so no output should have been produced.
|
|
assert.Empty(t, buff.String())
|
|
|
|
// Now, let's remove source file, and run the task again to to prepare
|
|
// for the next test.
|
|
err := os.Remove(filepathext.SmartJoin(dir, "bar.txt"))
|
|
require.NoError(t, err)
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "gen-bar"}))
|
|
buff.Reset()
|
|
|
|
// Global silence switched of, so we should see output unless the task itself
|
|
// is silent.
|
|
e.Silent = false
|
|
|
|
// all: not up-to-date
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "gen-foo"}))
|
|
assert.Equal(t, "task: [gen-foo] touch foo.txt", strings.TrimSpace(buff.String()))
|
|
buff.Reset()
|
|
// status: not up-to-date
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "gen-foo"}))
|
|
assert.Equal(t, "task: [gen-foo] touch foo.txt", strings.TrimSpace(buff.String()))
|
|
buff.Reset()
|
|
|
|
// sources: not up-to-date
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "gen-bar"}))
|
|
assert.Equal(t, "task: [gen-bar] touch bar.txt", strings.TrimSpace(buff.String()))
|
|
buff.Reset()
|
|
// all: up-to-date
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "gen-bar"}))
|
|
assert.Equal(t, `task: Task "gen-bar" is up to date`, strings.TrimSpace(buff.String()))
|
|
buff.Reset()
|
|
|
|
// sources: not up-to-date, no output produced.
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "gen-silent-baz"}))
|
|
assert.Empty(t, buff.String())
|
|
|
|
// up-to-date, no output produced
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "gen-silent-baz"}))
|
|
assert.Empty(t, buff.String())
|
|
|
|
e.Verbose = true
|
|
// up-to-date, output produced due to Verbose mode.
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "gen-silent-baz"}))
|
|
assert.Equal(t, `task: Task "gen-silent-baz" is up to date`, strings.TrimSpace(buff.String()))
|
|
buff.Reset()
|
|
}
|
|
|
|
func TestPrecondition(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/precondition"
|
|
|
|
var buff bytes.Buffer
|
|
e := &task.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
|
|
// A precondition that has been met
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "foo"}))
|
|
if buff.String() != "" {
|
|
t.Errorf("Got Output when none was expected: %s", buff.String())
|
|
}
|
|
|
|
// A precondition that was not met
|
|
require.Error(t, e.Run(context.Background(), &ast.Call{Task: "impossible"}))
|
|
|
|
if buff.String() != "task: 1 != 0 obviously!\n" {
|
|
t.Errorf("Wrong output message: %s", buff.String())
|
|
}
|
|
buff.Reset()
|
|
|
|
// Calling a task with a precondition in a dependency fails the task
|
|
require.Error(t, e.Run(context.Background(), &ast.Call{Task: "depends_on_impossible"}))
|
|
|
|
if buff.String() != "task: 1 != 0 obviously!\n" {
|
|
t.Errorf("Wrong output message: %s", buff.String())
|
|
}
|
|
buff.Reset()
|
|
|
|
// Calling a task with a precondition in a cmd fails the task
|
|
require.Error(t, e.Run(context.Background(), &ast.Call{Task: "executes_failing_task_as_cmd"}))
|
|
if buff.String() != "task: 1 != 0 obviously!\n" {
|
|
t.Errorf("Wrong output message: %s", buff.String())
|
|
}
|
|
buff.Reset()
|
|
}
|
|
|
|
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.Executor{
|
|
Dir: dir,
|
|
Stdout: buff,
|
|
Stderr: 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(), &ast.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(), &ast.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.Executor{
|
|
Dir: dir,
|
|
TempDir: tempdir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
require.NoError(t, e.Run(context.Background(), &ast.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(), &ast.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 TestAlias(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/alias"
|
|
|
|
data, err := os.ReadFile(filepathext.SmartJoin(dir, "alias.txt"))
|
|
require.NoError(t, err)
|
|
|
|
var buff bytes.Buffer
|
|
e := task.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "f"}))
|
|
assert.Equal(t, string(data), buff.String())
|
|
}
|
|
|
|
func TestDuplicateAlias(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/alias"
|
|
|
|
var buff bytes.Buffer
|
|
e := task.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.Error(t, e.Run(context.Background(), &ast.Call{Task: "x"}))
|
|
assert.Equal(t, "", buff.String())
|
|
}
|
|
|
|
func TestAliasSummary(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/alias"
|
|
|
|
data, err := os.ReadFile(filepathext.SmartJoin(dir, "alias-summary.txt"))
|
|
require.NoError(t, err)
|
|
|
|
var buff bytes.Buffer
|
|
e := task.Executor{
|
|
Dir: dir,
|
|
Summary: true,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "f"}))
|
|
assert.Equal(t, string(data), buff.String())
|
|
}
|
|
|
|
func TestLabelUpToDate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/label_uptodate"
|
|
|
|
var buff bytes.Buffer
|
|
e := task.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "foo"}))
|
|
assert.Contains(t, buff.String(), "foobar")
|
|
}
|
|
|
|
func TestLabelSummary(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/label_summary"
|
|
|
|
var buff bytes.Buffer
|
|
e := task.Executor{
|
|
Dir: dir,
|
|
Summary: true,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "foo"}))
|
|
assert.Contains(t, buff.String(), "foobar")
|
|
}
|
|
|
|
func TestLabelInStatus(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/label_status"
|
|
|
|
e := task.Executor{
|
|
Dir: dir,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
err := e.Status(context.Background(), &ast.Call{Task: "foo"})
|
|
assert.ErrorContains(t, err, "foobar")
|
|
}
|
|
|
|
func TestLabelWithVariableExpansion(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/label_var"
|
|
|
|
var buff bytes.Buffer
|
|
e := task.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "foo"}))
|
|
assert.Contains(t, buff.String(), "foobaz")
|
|
}
|
|
|
|
func TestLabelInSummary(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/label_summary"
|
|
|
|
var buff bytes.Buffer
|
|
e := task.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "foo"}))
|
|
assert.Contains(t, buff.String(), "foobar")
|
|
}
|
|
|
|
func TestPromptInSummary(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/prompt"
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantError bool
|
|
}{
|
|
{"test short approval", "y\n", false},
|
|
{"test long approval", "yes\n", false},
|
|
{"test uppercase approval", "Y\n", false},
|
|
{"test stops task", "n\n", true},
|
|
{"test junk value stops task", "foobar\n", true},
|
|
{"test Enter stops task", "\n", true},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var inBuff bytes.Buffer
|
|
var outBuff bytes.Buffer
|
|
var errBuff bytes.Buffer
|
|
|
|
inBuff.Write([]byte(test.input))
|
|
|
|
e := task.Executor{
|
|
Dir: dir,
|
|
Stdin: &inBuff,
|
|
Stdout: &outBuff,
|
|
Stderr: &errBuff,
|
|
AssumeTerm: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
err := e.Run(context.Background(), &ast.Call{Task: "foo"})
|
|
|
|
if test.wantError {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPromptWithIndirectTask(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/prompt"
|
|
var inBuff bytes.Buffer
|
|
var outBuff bytes.Buffer
|
|
var errBuff bytes.Buffer
|
|
|
|
inBuff.Write([]byte("y\n"))
|
|
|
|
e := task.Executor{
|
|
Dir: dir,
|
|
Stdin: &inBuff,
|
|
Stdout: &outBuff,
|
|
Stderr: &errBuff,
|
|
AssumeTerm: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
err := e.Run(context.Background(), &ast.Call{Task: "bar"})
|
|
assert.Contains(t, outBuff.String(), "show-prompt")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestPromptAssumeYes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/prompt"
|
|
tests := []struct {
|
|
name string
|
|
assumeYes bool
|
|
}{
|
|
{"--yes flag should skip prompt", true},
|
|
{"task should raise errors.TaskCancelledError", false},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var inBuff bytes.Buffer
|
|
var outBuff bytes.Buffer
|
|
var errBuff bytes.Buffer
|
|
|
|
// always cancel the prompt so we can require.Error
|
|
inBuff.Write([]byte("\n"))
|
|
|
|
e := task.Executor{
|
|
Dir: dir,
|
|
Stdin: &inBuff,
|
|
Stdout: &outBuff,
|
|
Stderr: &errBuff,
|
|
AssumeYes: test.assumeYes,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
err := e.Run(context.Background(), &ast.Call{Task: "foo"})
|
|
|
|
if !test.assumeYes {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNoLabelInList(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/label_list"
|
|
|
|
var buff bytes.Buffer
|
|
e := task.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
if _, err := e.ListTasks(task.ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil {
|
|
t.Error(err)
|
|
}
|
|
assert.Contains(t, buff.String(), "foo")
|
|
}
|
|
|
|
// task -al case 1: listAll list all tasks
|
|
func TestListAllShowsNoDesc(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/list_mixed_desc"
|
|
|
|
var buff bytes.Buffer
|
|
e := task.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
|
|
require.NoError(t, e.Setup())
|
|
|
|
var title string
|
|
if _, err := e.ListTasks(task.ListOptions{ListAllTasks: true}); err != nil {
|
|
t.Error(err)
|
|
}
|
|
for _, title = range []string{
|
|
"foo",
|
|
"voo",
|
|
"doo",
|
|
} {
|
|
assert.Contains(t, buff.String(), title)
|
|
}
|
|
}
|
|
|
|
// task -al case 2: !listAll list some tasks (only those with desc)
|
|
func TestListCanListDescOnly(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/list_mixed_desc"
|
|
|
|
var buff bytes.Buffer
|
|
e := task.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
|
|
require.NoError(t, e.Setup())
|
|
if _, err := e.ListTasks(task.ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
var title string
|
|
assert.Contains(t, buff.String(), "foo")
|
|
for _, title = range []string{
|
|
"voo",
|
|
"doo",
|
|
} {
|
|
assert.NotContains(t, buff.String(), title)
|
|
}
|
|
}
|
|
|
|
func TestListDescInterpolation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/list_desc_interpolation"
|
|
|
|
var buff bytes.Buffer
|
|
e := task.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
|
|
require.NoError(t, e.Setup())
|
|
if _, err := e.ListTasks(task.ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
assert.Contains(t, buff.String(), "foo-var")
|
|
assert.Contains(t, buff.String(), "bar-var")
|
|
}
|
|
|
|
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.Executor{
|
|
Dir: dir,
|
|
TempDir: task.TempDir{
|
|
Remote: filepathext.SmartJoin(dir, ".task"),
|
|
Fingerprint: filepathext.SmartJoin(dir, ".task"),
|
|
},
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: false,
|
|
Verbose: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "build"}))
|
|
|
|
assert.Contains(t, buff.String(), "3e464c4b03f4b65d740e1e130d4d108a")
|
|
|
|
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.Executor{
|
|
Dir: dir,
|
|
TempDir: task.TempDir{
|
|
Remote: filepathext.SmartJoin(dir, ".task"),
|
|
Fingerprint: filepathext.SmartJoin(dir, ".task"),
|
|
},
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: false,
|
|
Verbose: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "build"}))
|
|
|
|
assert.Contains(t, buff.String(), "3e464c4b03f4b65d740e1e130d4d108a")
|
|
|
|
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.Executor{
|
|
Dir: dir,
|
|
Stdout: io.Discard,
|
|
Stderr: io.Discard,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
assert.IsType(t, &errors.TaskCalledTooManyTimesError{}, e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: test.Dir,
|
|
Stdout: io.Discard,
|
|
Stderr: io.Discard,
|
|
EnableVersionCheck: 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.Executor{
|
|
Dir: dir,
|
|
Stdout: io.Discard,
|
|
Stderr: io.Discard,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "task-should-pass"}))
|
|
require.Error(t, e.Run(context.Background(), &ast.Call{Task: "task-should-fail"}))
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "cmd-should-pass"}))
|
|
require.Error(t, e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Dry: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: dir,
|
|
TempDir: task.TempDir{
|
|
Remote: filepathext.SmartJoin(dir, ".task"),
|
|
Fingerprint: filepathext.SmartJoin(dir, ".task"),
|
|
},
|
|
Stdout: io.Discard,
|
|
Stderr: io.Discard,
|
|
Dry: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.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(), &ast.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"
|
|
|
|
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",
|
|
},
|
|
}
|
|
|
|
tasks := []string{
|
|
"first:write-file",
|
|
"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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Timeout: time.Minute,
|
|
Insecure: true,
|
|
Logger: &logger.Logger{Stdout: &buff, Stderr: &buff, Verbose: true},
|
|
|
|
// Without caching
|
|
AssumeYes: true,
|
|
Download: true,
|
|
},
|
|
},
|
|
{
|
|
name: "offline, use cache",
|
|
executor: &task.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Timeout: time.Minute,
|
|
Insecure: true,
|
|
Logger: &logger.Logger{Stdout: &buff, Stderr: &buff, Verbose: true},
|
|
|
|
// With caching
|
|
AssumeYes: false,
|
|
Download: false,
|
|
Offline: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
for j, e := range executors {
|
|
t.Run(fmt.Sprint(j), func(t *testing.T) {
|
|
require.NoError(t, e.executor.Setup())
|
|
|
|
for k, task := range tasks {
|
|
t.Run(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(), &ast.Call{Task: task}))
|
|
|
|
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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: 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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: 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.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) {
|
|
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.Executor{
|
|
Dir: dir,
|
|
Stdout: io.Discard,
|
|
Stderr: 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.Executor{
|
|
Dir: dir,
|
|
Stdout: io.Discard,
|
|
Stderr: 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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
|
|
require.NoError(t, e.Setup())
|
|
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "common:pwd"}))
|
|
assert.Contains(t, buff.String(), "testdata/includes_rel_path/common")
|
|
|
|
buff.Reset()
|
|
require.NoError(t, e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
err := e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Entrypoint: dir + "/" + test.taskfile,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: true,
|
|
}
|
|
err := e.Setup()
|
|
if test.expectedErr {
|
|
assert.EqualError(t, err, test.expectedOutput)
|
|
} else {
|
|
require.NoError(t, err)
|
|
_ = e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: filepath.Join(dir, test.name),
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
err := e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: "testdata/includes_with_excludes",
|
|
Silent: true,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
err := e.Run(context.Background(), &ast.Call{Task: "included:bar"})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "bar\n", buff.String())
|
|
buff.Reset()
|
|
|
|
err = e.Run(context.Background(), &ast.Call{Task: "included:foo"})
|
|
require.Error(t, err)
|
|
buff.Reset()
|
|
|
|
err = e.Run(context.Background(), &ast.Call{Task: "bar"})
|
|
require.Error(t, err)
|
|
buff.Reset()
|
|
|
|
err = e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
err := e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
err := e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Summary: true,
|
|
Silent: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "task-with-summary"}, &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &out,
|
|
Stderr: &out,
|
|
}
|
|
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &out,
|
|
Stderr: &out,
|
|
}
|
|
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &out,
|
|
Stderr: &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(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &out,
|
|
Stderr: &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(), &ast.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.Executor{
|
|
Dir: "testdata/version/v1",
|
|
Stdout: io.Discard,
|
|
Stderr: io.Discard,
|
|
EnableVersionCheck: 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.Executor{
|
|
Dir: "testdata/version/v2",
|
|
Stdout: io.Discard,
|
|
Stderr: &buff,
|
|
EnableVersionCheck: 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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: "testdata/dotenv/error_included_envs",
|
|
Summary: true,
|
|
Stdout: &buff,
|
|
Stderr: &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.Executor{
|
|
Dir: "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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
require.Error(t, e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
ForceAll: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &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(), &ast.Call{Task: "task-2"}))
|
|
assert.Contains(t, buff.String(), expectedOutputOrder)
|
|
}
|
|
|
|
func TestExitCodeZero(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/exit_code"
|
|
var buff bytes.Buffer
|
|
e := task.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
require.NoError(t, e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
require.Error(t, e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: test.dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &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(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
require.NoError(t, e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
require.Error(t, e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &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(), &ast.Call{Task: "task1"}))
|
|
t.Log(buff.String())
|
|
assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder)
|
|
}
|
|
|
|
func TestIncludedVarsMultiLevel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const dir = "testdata/include_with_vars_multi_level"
|
|
var buff bytes.Buffer
|
|
e := task.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &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(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
err := e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: 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(), &ast.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.Executor{
|
|
Dir: test.dir,
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "default"}))
|
|
assert.Equal(t, test.expected, buff.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserWorkingDirectory(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var buff bytes.Buffer
|
|
e := task.Executor{
|
|
Dir: "testdata/user_working_dir",
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
wd, err := os.Getwd()
|
|
require.NoError(t, err)
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.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.Executor{
|
|
UserWorkingDir: wd,
|
|
Dir: "testdata/user_working_dir_with_includes",
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: "testdata/platforms",
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: "testdata/shopts/global_level",
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
err := e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: "testdata/shopts/task_level",
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
err := e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: "testdata/shopts/command_level",
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
err := e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: "testdata/shopts/global_level",
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
err := e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: "testdata/shopts/task_level",
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
err := e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: "testdata/shopts/command_level",
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
err := e.Run(context.Background(), &ast.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.Executor{
|
|
Dir: "testdata/split_args",
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: 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(), &ast.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.Executor{
|
|
Dir: "testdata/silent",
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: false,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
|
|
// First verify that the silent flag is in place.
|
|
task, err := e.GetTask(&ast.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, task.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(), &ast.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(), &ast.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(), &ast.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(), &ast.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(), &ast.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(), &ast.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(), &ast.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(), &ast.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(), &ast.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(), &ast.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(), &ast.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.Executor{
|
|
Dir: "testdata/force",
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Force: tt.force,
|
|
ForceAll: tt.forceAll,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "task-with-dep"}))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestForCmds(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
expectedOutput string
|
|
}{
|
|
{
|
|
name: "loop-explicit",
|
|
expectedOutput: "a\nb\nc\n",
|
|
},
|
|
{
|
|
name: "loop-matrix",
|
|
expectedOutput: "windows/amd64\nwindows/arm64\nlinux/amd64\nlinux/arm64\ndarwin/amd64\ndarwin/arm64\n",
|
|
},
|
|
{
|
|
name: "loop-sources",
|
|
expectedOutput: "bar\nfoo\n",
|
|
},
|
|
{
|
|
name: "loop-sources-glob",
|
|
expectedOutput: "bar\nfoo\n",
|
|
},
|
|
{
|
|
name: "loop-vars",
|
|
expectedOutput: "foo\nbar\n",
|
|
},
|
|
{
|
|
name: "loop-vars-sh",
|
|
expectedOutput: "bar\nfoo\n",
|
|
},
|
|
{
|
|
name: "loop-task",
|
|
expectedOutput: "foo\nbar\n",
|
|
},
|
|
{
|
|
name: "loop-task-as",
|
|
expectedOutput: "foo\nbar\n",
|
|
},
|
|
{
|
|
name: "loop-different-tasks",
|
|
expectedOutput: "1\n2\n3\n",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var stdOut bytes.Buffer
|
|
var stdErr bytes.Buffer
|
|
e := task.Executor{
|
|
Dir: "testdata/for/cmds",
|
|
Stdout: &stdOut,
|
|
Stderr: &stdErr,
|
|
Silent: true,
|
|
Force: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: test.name}))
|
|
assert.Equal(t, test.expectedOutput, stdOut.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestForDeps(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
expectedOutputContains []string
|
|
}{
|
|
{
|
|
name: "loop-explicit",
|
|
expectedOutputContains: []string{"a\n", "b\n", "c\n"},
|
|
},
|
|
{
|
|
name: "loop-matrix",
|
|
expectedOutputContains: []string{
|
|
"windows/amd64\n",
|
|
"windows/arm64\n",
|
|
"linux/amd64\n",
|
|
"linux/arm64\n",
|
|
"darwin/amd64\n",
|
|
"darwin/arm64\n",
|
|
},
|
|
},
|
|
{
|
|
name: "loop-sources",
|
|
expectedOutputContains: []string{"bar\n", "foo\n"},
|
|
},
|
|
{
|
|
name: "loop-sources-glob",
|
|
expectedOutputContains: []string{"bar\n", "foo\n"},
|
|
},
|
|
{
|
|
name: "loop-vars",
|
|
expectedOutputContains: []string{"foo\n", "bar\n"},
|
|
},
|
|
{
|
|
name: "loop-vars-sh",
|
|
expectedOutputContains: []string{"bar\n", "foo\n"},
|
|
},
|
|
{
|
|
name: "loop-task",
|
|
expectedOutputContains: []string{"foo\n", "bar\n"},
|
|
},
|
|
{
|
|
name: "loop-task-as",
|
|
expectedOutputContains: []string{"foo\n", "bar\n"},
|
|
},
|
|
{
|
|
name: "loop-different-tasks",
|
|
expectedOutputContains: []string{"1\n", "2\n", "3\n"},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// We need to use a sync buffer here as deps are run concurrently
|
|
var buff SyncBuffer
|
|
e := task.Executor{
|
|
Dir: "testdata/for/deps",
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: true,
|
|
Force: true,
|
|
// Force output of each dep to be grouped together to prevent interleaving
|
|
OutputStyle: ast.Output{Name: "group"},
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: test.name}))
|
|
for _, expectedOutputContains := range test.expectedOutputContains {
|
|
assert.Contains(t, buff.buf.String(), expectedOutputContains)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.call, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var buff bytes.Buffer
|
|
e := task.Executor{
|
|
Dir: "testdata/wildcards",
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: true,
|
|
Force: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
if test.wantErr {
|
|
require.Error(t, e.Run(context.Background(), &ast.Call{Task: test.call}))
|
|
return
|
|
}
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: test.call}))
|
|
assert.Equal(t, test.expectedOutput, buff.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReference(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
call string
|
|
expectedOutput string
|
|
}{
|
|
{
|
|
name: "reference in command",
|
|
call: "ref-cmd",
|
|
expectedOutput: "1\n",
|
|
},
|
|
{
|
|
name: "reference in dependency",
|
|
call: "ref-dep",
|
|
expectedOutput: "1\n",
|
|
},
|
|
{
|
|
name: "reference using templating resolver",
|
|
call: "ref-resolver",
|
|
expectedOutput: "1\n",
|
|
},
|
|
{
|
|
name: "reference using templating resolver and dynamic var",
|
|
call: "ref-resolver-sh",
|
|
expectedOutput: "Alice has 3 children called Bob, Charlie, and Diane\n",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.call, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var buff bytes.Buffer
|
|
e := task.Executor{
|
|
Dir: "testdata/var_references",
|
|
Stdout: &buff,
|
|
Stderr: &buff,
|
|
Silent: true,
|
|
Force: true,
|
|
}
|
|
require.NoError(t, e.Setup())
|
|
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: test.call}))
|
|
assert.Equal(t, test.expectedOutput, buff.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
t.Helper()
|
|
|
|
prev := *e
|
|
*e = experiments.Experiment{
|
|
Name: prev.Name,
|
|
Enabled: true,
|
|
Value: val,
|
|
}
|
|
t.Cleanup(func() { *e = prev })
|
|
}
|