Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 215 additions & 0 deletions internal/fingerprint/glob_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package fingerprint

import (
"os"
"path/filepath"
"sort"
"testing"

"github.com/stretchr/testify/require"

"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/fsext"
"github.com/go-task/task/v3/taskfile/ast"
)

type routedGlob struct {
glob *ast.Glob
fast bool
}

func TestGlobsRecursiveIncludesAllFilesAndHonorsExclusions(t *testing.T) {
t.Parallel()

dir := t.TempDir()
files := []string{
"input tree/folder-a/.hidden-file-a.aaa",
"input tree/folder-a/file-b.bbb",
"input tree/folder-a/folder-b/.hidden-file-c.ccc",
"input tree/folder-a/folder-b/file-d.ddd",
"input tree/folder-a/folder-c/.hidden-folder-a/file-e.eee",
"input tree/folder-a/folder-c/folder-d/.hidden-file-f.fff",
"input tree/folder-a/folder-c/folder-d/file-g.ggg",
"input tree/folder-a/excluded-file-a.xxx",
"input tree/folder-a/excluded-folder-a/.hidden-file-b.yyy",
"input tree/folder-a/excluded-folder-a/.hidden-folder-b/file-c.zzz",
"input tree/folder-a/excluded-folder-a/file-d.www",
"input tree/folder-a/excluded-folder-a/folder-b/file-e.vvv",
}
writeGlobFiles(t, dir, files)

sourceRoot := filepath.Join("input tree", "folder-a")
tests := []struct {
name string
globs []routedGlob
}{
{
name: "optimized inclusion and exclusion",
globs: []routedGlob{
{glob: &ast.Glob{Glob: filepath.Join(sourceRoot, "**", "*")}, fast: true},
{glob: &ast.Glob{Glob: filepath.Join(sourceRoot, "excluded-file-a.xxx"), Negate: true}, fast: false},
{glob: &ast.Glob{Glob: filepath.Join(sourceRoot, "excluded-folder-a", "**", "*"), Negate: true}, fast: true},
},
},
{
name: "optimized inclusion and fallback exclusion",
globs: []routedGlob{
{glob: &ast.Glob{Glob: filepath.Join(sourceRoot, "**", "*")}, fast: true},
{glob: &ast.Glob{Glob: filepath.Join(sourceRoot, "excluded-file-a.xxx"), Negate: true}, fast: false},
{glob: &ast.Glob{Glob: filepath.Join(sourceRoot, "**", "excluded-folder-a", "**", "*"), Negate: true}, fast: false},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

got, err := Globs(dir, requireGlobRoutes(t, dir, tt.globs), false)
require.NoError(t, err)
require.Equal(t, globPaths(dir, files[:7]), got)
})
}
}

func TestGlobsFallbackPatternsIncludeHiddenEntries(t *testing.T) {
t.Parallel()

dir := t.TempDir()
files := []string{
"input tree/folder-a/.hidden-file-a.aaa",
"input tree/folder-a/file-a.aaa",
"input tree/folder-a/file-a.ccc",
"input tree/folder-a/file-a.bbb",
"input tree/folder-a/nested/.hidden-file-b.bbb",
"input tree/folder-a/nested/file-b.aaa",
"input tree/folder-a/nested/deeper-folder-a/file-c.aaa",
"input tree/folder-a/.hidden-folder-a/file-d.aaa",
"input tree/folder-a/.hidden-folder-a/nested/.hidden-file-e.aaa",
"input tree/folder-a/.hidden-folder-a/folder-b/file-f.bbb",
"input tree/folder-a/folder-b/.hidden-file-g.bbb",
"input tree/folder-a/folder-b/file-g.aaa",
"input tree/folder-a/folder-b/deeper-folder-b/file-h.aaa",
"input tree/.hidden-folder-b/.hidden-root-file-a.aaa",
"input tree/.hidden-folder-b/nested/file-i.bbb",
"input tree/.hidden-folder-b/folder-b/file-j.aaa",
"input tree/folder-c/file-k.ccc",
"input tree/folder-c/nested/file-k.aaa",
"input tree/folder-c/folder-b/file-l.aaa",
}
writeGlobFiles(t, dir, files)

tests := []struct {
name string
pattern string
expected []string
}{
{
name: "nonrecursive",
pattern: filepath.Join("input tree", "folder-a", "*"),
expected: []string{
"input tree/folder-a/.hidden-file-a.aaa",
"input tree/folder-a/file-a.aaa",
"input tree/folder-a/file-a.ccc",
"input tree/folder-a/file-a.bbb",
},
},
{
name: "wildcard root",
pattern: filepath.Join("input tree", "*", "**", "*"),
expected: files,
},
{
name: "nested suffix",
pattern: filepath.Join("input tree", "**", "nested", "*"),
expected: []string{
"input tree/folder-a/nested/.hidden-file-b.bbb",
"input tree/folder-a/nested/file-b.aaa",
"input tree/folder-a/.hidden-folder-a/nested/.hidden-file-e.aaa",
"input tree/.hidden-folder-b/nested/file-i.bbb",
"input tree/folder-c/nested/file-k.aaa",
},
},
{
name: "brace suffix",
pattern: filepath.Join("input tree", "**", "*.{aaa,bbb}"),
expected: []string{
"input tree/folder-a/.hidden-file-a.aaa",
"input tree/folder-a/file-a.aaa",
"input tree/folder-a/file-a.bbb",
"input tree/folder-a/nested/.hidden-file-b.bbb",
"input tree/folder-a/nested/file-b.aaa",
"input tree/folder-a/nested/deeper-folder-a/file-c.aaa",
"input tree/folder-a/.hidden-folder-a/file-d.aaa",
"input tree/folder-a/.hidden-folder-a/nested/.hidden-file-e.aaa",
"input tree/folder-a/.hidden-folder-a/folder-b/file-f.bbb",
"input tree/folder-a/folder-b/.hidden-file-g.bbb",
"input tree/folder-a/folder-b/file-g.aaa",
"input tree/folder-a/folder-b/deeper-folder-b/file-h.aaa",
"input tree/.hidden-folder-b/.hidden-root-file-a.aaa",
"input tree/.hidden-folder-b/nested/file-i.bbb",
"input tree/.hidden-folder-b/folder-b/file-j.aaa",
"input tree/folder-c/nested/file-k.aaa",
"input tree/folder-c/folder-b/file-l.aaa",
},
},
{
name: "multiple recursive markers",
pattern: filepath.Join("input tree", "**", "folder-b", "**", "*"),
expected: []string{
"input tree/folder-a/.hidden-folder-a/folder-b/file-f.bbb",
"input tree/folder-a/folder-b/.hidden-file-g.bbb",
"input tree/folder-a/folder-b/file-g.aaa",
"input tree/folder-a/folder-b/deeper-folder-b/file-h.aaa",
"input tree/.hidden-folder-b/folder-b/file-j.aaa",
"input tree/folder-c/folder-b/file-l.aaa",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

globs := requireGlobRoutes(t, dir, []routedGlob{{
glob: &ast.Glob{Glob: tt.pattern},
fast: false,
}})
got, err := Globs(dir, globs, false)
require.NoError(t, err)
require.Equal(t, globPaths(dir, tt.expected), got)
})
}
}

func requireGlobRoutes(t *testing.T, dir string, globs []routedGlob) []*ast.Glob {
t.Helper()

patterns := make([]*ast.Glob, 0, len(globs))
for _, routed := range globs {
_, fast, err := fsext.FastRecursiveGlob(filepathext.SmartJoin(dir, routed.glob.Glob))
require.NoError(t, err)
require.Equal(t, routed.fast, fast, "glob route for %q", routed.glob.Glob)
patterns = append(patterns, routed.glob)
}
return patterns
}

func writeGlobFiles(t *testing.T, dir string, files []string) {
t.Helper()

for _, file := range files {
path := filepath.Join(dir, filepath.FromSlash(file))
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
require.NoError(t, os.WriteFile(path, []byte(file), 0o600))
}
}

func globPaths(dir string, files []string) []string {
paths := make([]string, 0, len(files))
for _, file := range files {
paths = append(paths, filepath.ToSlash(filepath.Join(dir, filepath.FromSlash(file))))
}
sort.Strings(paths)
return paths
}
137 changes: 137 additions & 0 deletions internal/fingerprint/sources_checksum_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,148 @@
package fingerprint

import (
"fmt"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/go-task/task/v3/taskfile/ast"
)

func TestChecksumCheckerRecursiveSourcesAndRequiredGenerates(t *testing.T) {
t.Parallel()

sourceRoot := filepath.Join("input tree", "folder-a")
outputRoot := filepath.Join("output tree", "folder-a")
includedSources := []string{
filepath.Join(sourceRoot, ".hidden-file-a.aaa"),
filepath.Join(sourceRoot, ".hidden-file-b.bbb"),
filepath.Join(sourceRoot, "file-c.ccc"),
filepath.Join(sourceRoot, "file-d.ddd"),
filepath.Join(sourceRoot, ".hidden-folder-a", "file-e.eee"),
filepath.Join(sourceRoot, "folder-b", "folder-c", "file-f.fff"),
filepath.Join(sourceRoot, "folder-b", "folder-c", "file-g.ggg"),
filepath.Join(sourceRoot, "folder-b", "folder-c", "file-h.hhh"),
filepath.Join(sourceRoot, "folder-b", "folder-c", "file-i.iii"),
filepath.Join(sourceRoot, "folder-b", "folder-c", "folder-d", ".hidden-file-j.jjj"),
filepath.Join(sourceRoot, "folder-b", "folder-e", "file-k.kkk"),
}
excludedSources := []string{
filepath.Join(sourceRoot, "excluded-file-a.xxx"),
filepath.Join(sourceRoot, "excluded-folder-a", "file-b.yyy"),
filepath.Join(sourceRoot, "excluded-folder-a", "folder-b", ".hidden-file-c.zzz"),
filepath.Join(sourceRoot, "excluded-folder-a", ".hidden-folder-b", "file-d.www"),
}
generatedOutputs := []string{
filepath.Join(outputRoot, ".hidden-file-a.aaa"),
filepath.Join(outputRoot, "folder-b", "file-b.bbb"),
filepath.Join(outputRoot, "folder-b", "file-c.ccc"),
filepath.Join(outputRoot, "folder-b", "file-d.ddd"),
filepath.Join(outputRoot, "folder-b", "file-e.eee"),
filepath.Join(outputRoot, "folder-b", "file-f.fff"),
filepath.Join(outputRoot, "folder-b", "file-g.ggg"),
filepath.Join(outputRoot, "folder-b", "file-h.hhh"),
filepath.Join(outputRoot, "folder-b", "folder-c", "folder-d", "file-i.iii"),
}

tests := []struct {
name string
sources []routedGlob
}{
{
name: "optimized inclusion and exclusion",
sources: []routedGlob{
{glob: &ast.Glob{Glob: filepath.Join(sourceRoot, "**", "*")}, fast: true},
{glob: &ast.Glob{Glob: filepath.Join(sourceRoot, "excluded-file-a.xxx"), Negate: true}, fast: false},
{glob: &ast.Glob{Glob: filepath.Join(sourceRoot, "excluded-folder-a", "**", "*"), Negate: true}, fast: true},
},
},
{
name: "fallback inclusion",
sources: []routedGlob{
{glob: &ast.Glob{Glob: filepath.Join("input tree", "*", "**", "*")}, fast: false},
{glob: &ast.Glob{Glob: filepath.Join(sourceRoot, "excluded-file-a.xxx"), Negate: true}, fast: false},
{glob: &ast.Glob{Glob: filepath.Join(sourceRoot, "excluded-folder-a", "**", "*"), Negate: true}, fast: true},
},
},
{
name: "fallback exclusion",
sources: []routedGlob{
{glob: &ast.Glob{Glob: filepath.Join(sourceRoot, "**", "*")}, fast: true},
{glob: &ast.Glob{Glob: filepath.Join(sourceRoot, "excluded-file-a.xxx"), Negate: true}, fast: false},
{glob: &ast.Glob{Glob: filepath.Join(sourceRoot, "**", "excluded-folder-a", "**", "*"), Negate: true}, fast: false},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

rootDir := t.TempDir()
writeFile := func(relativePath, contents string) {
t.Helper()
path := filepath.Join(rootDir, relativePath)
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
require.NoError(t, os.WriteFile(path, []byte(contents), 0o600))
}
for i, source := range includedSources {
writeFile(source, fmt.Sprintf("included source %d\n", i))
}
for i, source := range excludedSources {
writeFile(source, fmt.Sprintf("excluded source %d\n", i))
}
for i, output := range generatedOutputs {
writeFile(output, fmt.Sprintf("generated output %d\n", i))
}

generates := make([]*ast.Glob, 0, len(generatedOutputs))
for _, output := range generatedOutputs {
generates = append(generates, &ast.Glob{Glob: output})
}
task := &ast.Task{
Task: "cache-" + tt.name,
Dir: rootDir,
Sources: requireGlobRoutes(t, rootDir, tt.sources),
Generates: generates,
}
checker := NewChecksumChecker(filepath.Join(rootDir, ".task-cache"), false)
isUpToDate := func() bool {
t.Helper()
upToDate, err := checker.IsUpToDate(task)
require.NoError(t, err)
return upToDate
}

require.False(t, isUpToDate(), "the first check should prime the source checksum")
require.True(t, isUpToDate(), "an unchanged task should be cached")

for i, source := range includedSources {
writeFile(source, fmt.Sprintf("included source %d mutation\n", i))
assert.False(t, isUpToDate(), "mutating included source %q should invalidate the cache", source)
require.True(t, isUpToDate(), "the updated checksum for included source %q should be cached", source)
}

for i, source := range excludedSources {
writeFile(source, fmt.Sprintf("excluded source %d mutation\n", i))
assert.True(t, isUpToDate(), "mutating excluded source %q should preserve the cache", source)
require.True(t, isUpToDate(), "the current checksum for excluded source %q should be cached", source)
}

for i, output := range generatedOutputs {
path := filepath.Join(rootDir, output)
require.NoError(t, os.Remove(path))
require.False(t, isUpToDate(), "missing required output %q should invalidate the cache", output)
writeFile(output, fmt.Sprintf("generated output %d\n", i))
require.True(t, isUpToDate(), "restoring required output %q should restore the cache", output)
}
})
}
}

func TestNormalizeFilename(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading