diff --git a/internal/fingerprint/sources_checksum.go b/internal/fingerprint/sources_checksum.go index 16a98c1640..787b70c1d9 100644 --- a/internal/fingerprint/sources_checksum.go +++ b/internal/fingerprint/sources_checksum.go @@ -44,7 +44,9 @@ func (checker *ChecksumChecker) IsUpToDate(t *ast.Task) (bool, error) { } if !checker.dry && oldHash != newHash { - _ = os.MkdirAll(filepathext.SmartJoin(checker.tempDir, "checksum"), 0o755) + + // if Task is executed in directory different from the task directory, + _ = os.MkdirAll(filepathext.SmartJoin(filepath.Join(checker.tempDir, t.Dir), "checksum"), 0o755) if err = os.WriteFile(checksumFile, []byte(newHash+"\n"), 0o644); err != nil { return false, err } @@ -115,7 +117,7 @@ func (c *ChecksumChecker) checksum(t *ast.Task) (string, error) { } func (checker *ChecksumChecker) checksumFilePath(t *ast.Task) string { - return filepath.Join(checker.tempDir, "checksum", normalizeFilename(t.Name())) + return filepath.Join(checker.tempDir, t.Dir, "checksum", normalizeFilename(t.Name())) } var checksumFilenameRegexp = regexp.MustCompile("[^A-z0-9]") diff --git a/internal/fingerprint/sources_timestamp.go b/internal/fingerprint/sources_timestamp.go index b1a6f299d5..fcfbddaf70 100644 --- a/internal/fingerprint/sources_timestamp.go +++ b/internal/fingerprint/sources_timestamp.go @@ -147,5 +147,5 @@ func (*TimestampChecker) OnError(t *ast.Task) error { } func (checker *TimestampChecker) timestampFilePath(t *ast.Task) string { - return filepath.Join(checker.tempDir, "timestamp", normalizeFilename(t.Task)) + return filepath.Join(checker.tempDir, t.Dir, "timestamp", normalizeFilename(t.Task)) } diff --git a/internal/fingerprint/sources_timestamp_test.go b/internal/fingerprint/sources_timestamp_test.go new file mode 100644 index 0000000000..4db4341bb9 --- /dev/null +++ b/internal/fingerprint/sources_timestamp_test.go @@ -0,0 +1,100 @@ +package fingerprint + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/go-task/task/v3/taskfile/ast" +) + +func TestTimestampFileLocation(t *testing.T) { + t.Parallel() + + // Create temporary directory for test + tempDir, err := os.MkdirTemp("", "task-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // Create test directories + rootDir := filepath.Join(tempDir, "root") + subDir := filepath.Join(rootDir, "subdir") + require.NoError(t, os.MkdirAll(rootDir, 0755)) + require.NoError(t, os.MkdirAll(subDir, 0755)) + + // Create source files + rootSourceFile := filepath.Join(rootDir, "root.txt") + subSourceFile := filepath.Join(subDir, "sub.txt") + require.NoError(t, os.WriteFile(rootSourceFile, []byte("root source"), 0644)) + require.NoError(t, os.WriteFile(subSourceFile, []byte("sub source"), 0644)) + + // Create generate files + rootGenerateFile := filepath.Join(rootDir, "root.txt.processed") + subGenerateFile := filepath.Join(subDir, "sub.txt.processed") + require.NoError(t, os.WriteFile(rootGenerateFile, []byte("Processing root.txt"), 0644)) + require.NoError(t, os.WriteFile(subGenerateFile, []byte("Processing sub.txt"), 0644)) + + // Set file times + now := time.Now() + sourceTime := now.Add(-1 * time.Hour) + generateTime := now + require.NoError(t, os.Chtimes(rootSourceFile, sourceTime, sourceTime)) + require.NoError(t, os.Chtimes(subSourceFile, sourceTime, sourceTime)) + require.NoError(t, os.Chtimes(rootGenerateFile, generateTime, generateTime)) + require.NoError(t, os.Chtimes(subGenerateFile, generateTime, generateTime)) + + // Create tasks + rootTask := &ast.Task{ + Task: "root", + Dir: rootDir, + Sources: []*ast.Glob{{Glob: "*.txt"}}, + Generates: []*ast.Glob{{Glob: "*.txt.processed"}}, + Method: "timestamp", + } + subTask := &ast.Task{ + Task: "sub", + Dir: subDir, + Sources: []*ast.Glob{{Glob: "*.txt"}}, + Generates: []*ast.Glob{{Glob: "*.txt.processed"}}, + Method: "timestamp", + } + + // Create checker + checker := NewTimestampChecker(tempDir, false) + + // Test root task + rootUpToDate, err := checker.IsUpToDate(rootTask) + require.NoError(t, err) + assert.True(t, rootUpToDate, "Root task should be up-to-date") + + // Test sub task + subUpToDate, err := checker.IsUpToDate(subTask) + require.NoError(t, err) + assert.True(t, subUpToDate, "Sub task should be up-to-date") + + // Verify timestamp files were created in the correct locations + rootTimestampFile := filepath.Join(tempDir, rootDir, "timestamp", normalizeFilename(rootTask.Task)) + subTimestampFile := filepath.Join(tempDir, subDir, "timestamp", normalizeFilename(subTask.Task)) + + _, err = os.Stat(rootTimestampFile) + assert.NoError(t, err, "Root timestamp file should exist") + _, err = os.Stat(subTimestampFile) + assert.NoError(t, err, "Sub timestamp file should exist") + + // Test that modifying a source file makes the task not up-to-date + newSourceTime := now.Add(1 * time.Hour) + require.NoError(t, os.Chtimes(rootSourceFile, newSourceTime, newSourceTime)) + + rootUpToDate, err = checker.IsUpToDate(rootTask) + require.NoError(t, err) + assert.False(t, rootUpToDate, "Root task should not be up-to-date after source file modification") + + // Sub task should still be up-to-date + subUpToDate, err = checker.IsUpToDate(subTask) + require.NoError(t, err) + assert.True(t, subUpToDate, "Sub task should still be up-to-date") +} diff --git a/internal/fingerprint/timestamp_checker_test.go b/internal/fingerprint/timestamp_checker_test.go new file mode 100644 index 0000000000..4279e14625 --- /dev/null +++ b/internal/fingerprint/timestamp_checker_test.go @@ -0,0 +1,179 @@ +package fingerprint + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/go-task/task/v3/taskfile/ast" +) + +type TestDefinition struct { + name string + setup func(t *testing.T, dir string) *ast.Task + expected bool +} + +func TestTimestampCheckerIsUpToDate(t *testing.T) { + t.Parallel() + + tests := []TestDefinition{ + { + name: "empty sources", + setup: func(t *testing.T, dir string) *ast.Task { + return &ast.Task{ + Dir: dir, + Sources: nil, + Generates: nil, + } + }, + expected: false, + }, + { + name: "sources newer than generates", + setup: func(t *testing.T, dir string) *ast.Task { + // Create source file + sourceFile := filepath.Join(dir, "source.txt") + err := os.WriteFile(sourceFile, []byte("source"), 0644) + require.NoError(t, err) + + // Create generate file with older timestamp + generateFile := filepath.Join(dir, "generate.txt") + err = os.WriteFile(generateFile, []byte("generate"), 0644) + require.NoError(t, err) + + // Set source file to be newer than generate file + sourceTime := time.Now() + generateTime := sourceTime.Add(-1 * time.Hour) + err = os.Chtimes(sourceFile, sourceTime, sourceTime) + require.NoError(t, err) + err = os.Chtimes(generateFile, generateTime, generateTime) + require.NoError(t, err) + + return &ast.Task{ + Dir: dir, + Sources: []*ast.Glob{{Glob: "source.txt"}}, + Generates: []*ast.Glob{{Glob: "generate.txt"}}, + } + }, + expected: false, + }, + { + name: "generates newer than sources", + setup: func(t *testing.T, dir string) *ast.Task { + // Create source file + sourceFile := filepath.Join(dir, "source.txt") + err := os.WriteFile(sourceFile, []byte("source"), 0644) + require.NoError(t, err) + + // Create generate file with newer timestamp + generateFile := filepath.Join(dir, "generate.txt") + err = os.WriteFile(generateFile, []byte("generate"), 0644) + require.NoError(t, err) + + // Set generate file to be newer than source file + sourceTime := time.Now().Add(-1 * time.Hour) + generateTime := time.Now() + err = os.Chtimes(sourceFile, sourceTime, sourceTime) + require.NoError(t, err) + err = os.Chtimes(generateFile, generateTime, generateTime) + require.NoError(t, err) + + return &ast.Task{ + Dir: dir, + Sources: []*ast.Glob{{Glob: "source.txt"}}, + Generates: []*ast.Glob{{Glob: "generate.txt"}}, + } + }, + expected: true, + }, + { + name: "glob pattern directory/**/*", + setup: func(t *testing.T, dir string) *ast.Task { + // Create directory structure + subDir := filepath.Join(dir, "subdir") + nestedDir := filepath.Join(subDir, "nested") + err := os.MkdirAll(nestedDir, 0755) + require.NoError(t, err) + + // Create source files + sourceFile1 := filepath.Join(subDir, "source1.txt") + sourceFile2 := filepath.Join(nestedDir, "source2.txt") + err = os.WriteFile(sourceFile1, []byte("source1"), 0644) + require.NoError(t, err) + err = os.WriteFile(sourceFile2, []byte("source2"), 0644) + require.NoError(t, err) + + // Create generate file + generateFile := filepath.Join(dir, "generate.txt") + generateFile2 := filepath.Join(dir, "generate2.txt") + err = os.WriteFile(generateFile, []byte("generate"), 0644) + require.NoError(t, err) + err = os.WriteFile(generateFile2, []byte("generate"), 0644) + require.NoError(t, err) + + // Set source files to be newer than generate file to simulate a change + generateTime := time.Now().Add(-1 * time.Hour) + sourceTime := time.Now() + err = os.Chtimes(sourceFile1, sourceTime, sourceTime) + require.NoError(t, err) + err = os.Chtimes(sourceFile2, sourceTime, sourceTime) + require.NoError(t, err) + err = os.Chtimes(generateFile, generateTime, generateTime) + require.NoError(t, err) + err = os.Remove(generateFile2) // Also remove one generate file to simulate a change + require.NoError(t, err) + + return &ast.Task{ + Dir: dir, + Sources: []*ast.Glob{{Glob: "subdir/**/*"}}, + Generates: []*ast.Glob{{Glob: "*.txt"}}, + Method: "timestamp", + } + }, + expected: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Create temporary directory for test + tempDir, err := os.MkdirTemp("", "task-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // Create test directory + testDir := filepath.Join(tempDir, "test") + err = os.MkdirAll(testDir, 0755) + require.NoError(t, err) + + // Setup test + task := tt.setup(t, testDir) + + // Create checker + checker := NewTimestampChecker(tempDir, false) + + // Run test + result, err := checker.IsUpToDate(task) + require.NoError(t, err) + assert.Equal(t, tt.expected, result) + + // Verify timestamp file location if sources exist + if len(task.Sources) > 0 { + timestampFile := filepath.Join(tempDir, task.Dir, "timestamp", normalizeFilename(task.Task)) + if !tt.expected { + // If task is not up-to-date, the timestamp file should still exist + _, err := os.Stat(timestampFile) + require.NoError(t, err, "Timestamp file should exist at %s", timestampFile) + } + } + }) + } +} diff --git a/task_test.go b/task_test.go index 7b986662cd..20424bb243 100644 --- a/task_test.go +++ b/task_test.go @@ -411,12 +411,14 @@ func TestGenerates(t *testing.T) { func TestStatusChecksum(t *testing.T) { // nolint:paralleltest // cannot run in parallel const dir = "testdata/checksum" + pwd, _ := os.Getwd() + tests := []struct { files []string task string }{ - {[]string{"generated.txt", ".task/checksum/build"}, "build"}, - {[]string{"generated.txt", ".task/checksum/build-with-status"}, "build-with-status"}, + {[]string{"generated.txt", filepath.Join(".task", pwd, "testdata/checksum/checksum/build")}, "build"}, + {[]string{"generated.txt", filepath.Join(".task", pwd, "testdata/checksum/checksum/build-with-status")}, "build-with-status"}, } for _, test := range tests { // nolint:paralleltest // cannot run in parallel @@ -449,7 +451,7 @@ func TestStatusChecksum(t *testing.T) { // nolint:paralleltest // cannot run in // 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)) + s, err := os.Stat(filepath.Join(tempDir.Fingerprint, pwd, dir, "checksum/"+test.task)) require.NoError(t, err) time := s.ModTime() @@ -457,7 +459,7 @@ func TestStatusChecksum(t *testing.T) { // nolint:paralleltest // cannot run in require.NoError(t, e.Run(context.Background(), &task.Call{Task: test.task})) assert.Equal(t, `task: Task "`+test.task+`" is up to date`+"\n", buff.String()) - s, err = os.Stat(filepathext.SmartJoin(tempDir.Fingerprint, "checksum/"+test.task)) + s, err = os.Stat(filepath.Join(tempDir.Fingerprint, pwd, dir, "checksum/"+test.task)) require.NoError(t, err) assert.Equal(t, time, s.ModTime()) }) @@ -655,8 +657,8 @@ func TestDryChecksum(t *testing.T) { t.Parallel() const dir = "testdata/dry_checksum" - - checksumFile := filepathext.SmartJoin(dir, ".task/checksum/default") + pwd, _ := os.Getwd() + checksumFile := filepath.Join(dir, filepath.Join(".task", pwd, dir, "/checksum/default")) _ = os.Remove(checksumFile) e := task.NewExecutor(