12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855 |
- 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) {
- 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) {
- 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) {
- 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",
- },
- }
- 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) {
- 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",
- },
- }
- tt.Run(t)
- }
- func TestRequires(t *testing.T) {
- 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.Vars{}
- 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()
- }
- func TestSpecialVars(t *testing.T) {
- 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"},
- // 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) {
- 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: test.target}))
- assert.Equal(t, test.expected+"\n", buff.String())
- })
- }
- }
- }
- func TestConcurrency(t *testing.T) {
- 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) {
- 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",
- },
- }
- tt.Run(t)
- }
- func TestDeps(t *testing.T) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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 {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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 TestInit(t *testing.T) {
- const dir = "testdata/init"
- file := filepathext.SmartJoin(dir, "Taskfile.yml")
- _ = os.Remove(file)
- if _, err := os.Stat(file); err == nil {
- t.Errorf("Taskfile.yml should not exist")
- }
- if err := task.InitTaskfile(io.Discard, dir); err != nil {
- t.Error(err)
- }
- if _, err := os.Stat(file); err != nil {
- t.Errorf("Taskfile.yml should exist")
- }
- _ = os.Remove(file)
- }
- func TestCyclicDep(t *testing.T) {
- 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) {
- 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) {
- e := task.Executor{
- Dir: test.Dir,
- Stdout: io.Discard,
- Stderr: io.Discard,
- }
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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",
- },
- }
- tt.Run(t)
- }
- func TestIncludesMultiLevel(t *testing.T) {
- 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",
- },
- }
- 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) {
- 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) {
- 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) {
- tt := fileContentTest{
- Dir: "testdata/includes_empty",
- Target: "included:default",
- TrimSpace: true,
- Files: map[string]string{
- "file.txt": "default",
- },
- }
- 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",
- Target: "default",
- TrimSpace: true,
- Files: map[string]string{
- "default.txt": "default",
- "called_dep.txt": "called_dep",
- "called_task.txt": "called_task",
- },
- }
- tt.Run(t)
- }
- func TestIncludesCallingRoot(t *testing.T) {
- tt := fileContentTest{
- Dir: "testdata/includes_call_root_task",
- Target: "included:call-root",
- TrimSpace: true,
- Files: map[string]string{
- "root_task.txt": "root task",
- },
- }
- tt.Run(t)
- }
- func TestIncludesOptional(t *testing.T) {
- tt := fileContentTest{
- Dir: "testdata/includes_optional",
- Target: "default",
- TrimSpace: true,
- Files: map[string]string{
- "called_dep.txt": "called_dep",
- },
- }
- tt.Run(t)
- }
- func TestIncludesOptionalImplicitFalse(t *testing.T) {
- 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) {
- 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) {
- 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",
- },
- }
- tt.Run(t)
- }
- func TestIncludesRelativePath(t *testing.T) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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 {
- 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 TestIncludedTaskfileVarMerging(t *testing.T) {
- 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) {
- 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) {
- 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) {
- 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) {
- tt := fileContentTest{
- Dir: "testdata/includes_shadowed_default",
- Target: "included",
- TrimSpace: true,
- Files: map[string]string{
- "file.txt": "shadowed",
- },
- }
- tt.Run(t)
- }
- func TestIncludesUnshadowedDefault(t *testing.T) {
- tt := fileContentTest{
- Dir: "testdata/includes_unshadowed_default",
- Target: "included",
- TrimSpace: true,
- Files: map[string]string{
- "file.txt": "included",
- },
- }
- tt.Run(t)
- }
- func TestSupportedFileNames(t *testing.T) {
- fileNames := []string{
- "Taskfile.yml",
- "Taskfile.yaml",
- "Taskfile.dist.yml",
- "Taskfile.dist.yaml",
- }
- for _, fileName := range fileNames {
- t.Run(fileName, func(t *testing.T) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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",
- },
- }
- tt.Run(t)
- }
- func TestDisplaysErrorOnVersion1Schema(t *testing.T) {
- e := task.Executor{
- Dir: "testdata/version/v1",
- Stdout: io.Discard,
- Stderr: io.Discard,
- }
- 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) {
- var buff bytes.Buffer
- e := task.Executor{
- Dir: "testdata/version/v2",
- Stdout: io.Discard,
- Stderr: &buff,
- }
- 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) {
- 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) {
- tt := fileContentTest{
- Dir: "testdata/dotenv/default",
- Target: "default",
- TrimSpace: false,
- Files: map[string]string{
- "include.txt": "INCLUDE1='from_include1' INCLUDE2='from_include2'\n",
- },
- }
- tt.Run(t)
- }
- func TestDotenvShouldErrorWhenIncludingDependantDotenvs(t *testing.T) {
- 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) {
- tt := fileContentTest{
- Dir: "testdata/dotenv/missing_env",
- Target: "default",
- TrimSpace: false,
- Files: map[string]string{
- "include.txt": "INCLUDE1='' INCLUDE2=''\n",
- },
- }
- tt.Run(t)
- }
- func TestDotenvHasLocalEnvInPath(t *testing.T) {
- 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",
- },
- }
- tt.Run(t)
- }
- func TestDotenvHasLocalVarInPath(t *testing.T) {
- 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",
- },
- }
- tt.Run(t)
- }
- func TestDotenvHasEnvVarInPath(t *testing.T) {
- os.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) {
- 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) {
- tt := fileContentTest{
- Dir: "testdata/dotenv_task/default",
- Target: "dotenv",
- TrimSpace: true,
- Files: map[string]string{
- "dotenv.txt": "foo",
- },
- }
- tt.Run(t)
- }
- func TestTaskDotenvFail(t *testing.T) {
- tt := fileContentTest{
- Dir: "testdata/dotenv_task/default",
- Target: "no-dotenv",
- TrimSpace: true,
- Files: map[string]string{
- "no-dotenv.txt": "global",
- },
- }
- tt.Run(t)
- }
- func TestTaskDotenvOverriddenByEnv(t *testing.T) {
- tt := fileContentTest{
- Dir: "testdata/dotenv_task/default",
- Target: "dotenv-overridden-by-env",
- TrimSpace: true,
- Files: map[string]string{
- "dotenv-overridden-by-env.txt": "overridden",
- },
- }
- tt.Run(t)
- }
- func TestTaskDotenvWithVarName(t *testing.T) {
- tt := fileContentTest{
- Dir: "testdata/dotenv_task/default",
- Target: "dotenv-with-var-name",
- TrimSpace: true,
- Files: map[string]string{
- "dotenv-with-var-name.txt": "foo",
- },
- }
- tt.Run(t)
- }
- func TestExitImmediately(t *testing.T) {
- 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) {
- tt := fileContentTest{
- Dir: "testdata/run",
- Target: "generate-hash",
- Files: map[string]string{
- "hash.txt": "starting 1\n1\n2\n",
- },
- }
- tt.Run(t)
- }
- func TestRunOnceSharedDeps(t *testing.T) {
- 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) {
- 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
- task: [task-2] echo 'echo ran'
- echo ran
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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 {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- var buff bytes.Buffer
- e := task.Executor{
- Dir: "testdata/split_args",
- Stdout: &buff,
- Stderr: &buff,
- Silent: true,
- }
- require.NoError(t, e.Setup())
- vars := &ast.Vars{}
- 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) {
- tt := fileContentTest{
- Dir: "testdata/single_cmd_dep",
- Target: "foo",
- Files: map[string]string{
- "foo.txt": "foo\n",
- "bar.txt": "bar\n",
- },
- }
- tt.Run(t)
- }
- func TestSilence(t *testing.T) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- // 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- prev := *e
- *e = experiments.Experiment{
- Name: prev.Name,
- Enabled: true,
- Value: val,
- }
- t.Cleanup(func() { *e = prev })
- }
|