diff --git a/internal/fingerprint/sources_checksum.go b/internal/fingerprint/sources_checksum.go index 16a98c1640..f1108e111c 100644 --- a/internal/fingerprint/sources_checksum.go +++ b/internal/fingerprint/sources_checksum.go @@ -53,6 +53,7 @@ func (checker *ChecksumChecker) IsUpToDate(t *ast.Task) (bool, error) { if len(t.Generates) > 0 { // For each specified 'generates' field, check whether the files actually exist for _, g := range t.Generates { + // Exclusion patterns don't represent output files; skip them. if g.Negate { continue } diff --git a/internal/fingerprint/sources_timestamp.go b/internal/fingerprint/sources_timestamp.go index b1a6f299d5..258d9386d9 100644 --- a/internal/fingerprint/sources_timestamp.go +++ b/internal/fingerprint/sources_timestamp.go @@ -32,6 +32,28 @@ func (checker *TimestampChecker) IsUpToDate(t *ast.Task) (bool, error) { if err != nil { return false, nil } + + // If generates are declared, ensure they all exist. A missing generated + // file means the task must run regardless of timestamps. + if len(t.Generates) > 0 { + for _, g := range t.Generates { + // Exclusion patterns don't represent output files; skip them. + if g.Negate { + continue + } + files, err := glob(t.Dir, g.Glob) + if os.IsNotExist(err) { + return false, nil + } + if err != nil { + return false, err + } + if len(files) == 0 { + return false, nil + } + } + } + generates, err := Globs(t.Dir, t.Generates) if err != nil { return false, nil diff --git a/task_test.go b/task_test.go index 9d54af9740..3b85f764ec 100644 --- a/task_test.go +++ b/task_test.go @@ -487,6 +487,104 @@ func TestStatusChecksum(t *testing.T) { // nolint:paralleltest // cannot run in } } +// TestStatusTimestamp is a regression test for https://github.com/go-task/task/issues/1230. +// When using method: timestamp, deleting a generated file should cause the task to re-run, +// not be skipped because the timestamp file is still present. +func TestStatusTimestamp(t *testing.T) { // nolint:paralleltest // cannot run in parallel + const dir = "testdata/timestamp" + + generatedFile := filepathext.SmartJoin(dir, "generated.txt") + tempDir := task.TempDir{ + Remote: filepathext.SmartJoin(dir, ".task"), + Fingerprint: filepathext.SmartJoin(dir, ".task"), + } + + // Clean up any state from previous runs. + _ = os.Remove(generatedFile) + _ = os.RemoveAll(filepathext.SmartJoin(dir, ".task")) + + var buff bytes.Buffer + e := task.NewExecutor( + task.WithDir(dir), + task.WithStdout(&buff), + task.WithStderr(&buff), + task.WithTempDir(tempDir), + ) + require.NoError(t, e.Setup()) + + // First run: task should execute and create generated.txt. + require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"})) + _, err := os.Stat(generatedFile) + require.NoError(t, err, "generated.txt should exist after first run") + buff.Reset() + + // Second run: task should be up to date. + require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"})) + assert.Equal(t, `task: Task "build" is up to date`+"\n", buff.String()) + buff.Reset() + + // Delete the generated file (simulate a clean), but leave the timestamp file. + require.NoError(t, os.Remove(generatedFile)) + _, err = os.Stat(generatedFile) + require.Error(t, err, "generated.txt should be gone") + + // Third run: task MUST re-run because generated.txt is missing. + // This is the regression: previously the task was incorrectly skipped. + require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"})) + assert.NotContains(t, buff.String(), "is up to date", "task should re-run when generated file is missing") + _, err = os.Stat(generatedFile) + require.NoError(t, err, "generated.txt should be recreated after third run") +} + +// TestStatusChecksumMissingGenerated is a regression test for https://github.com/go-task/task/issues/1230. +// When using method: checksum, deleting a generated file should cause the task to re-run, +// not be skipped because the checksum file still matches. +func TestStatusChecksumMissingGenerated(t *testing.T) { // nolint:paralleltest // cannot run in parallel + const dir = "testdata/checksum" + + generatedFile := filepathext.SmartJoin(dir, "generated.txt") + tempDir := task.TempDir{ + Remote: filepathext.SmartJoin(dir, ".task"), + Fingerprint: filepathext.SmartJoin(dir, ".task"), + } + + // Clean up any state from previous runs. + _ = os.Remove(generatedFile) + _ = os.RemoveAll(filepathext.SmartJoin(dir, ".task")) + + var buff bytes.Buffer + e := task.NewExecutor( + task.WithDir(dir), + task.WithStdout(&buff), + task.WithStderr(&buff), + task.WithTempDir(tempDir), + ) + require.NoError(t, e.Setup()) + + // First run: task should execute and create generated.txt. + require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"})) + _, err := os.Stat(generatedFile) + require.NoError(t, err, "generated.txt should exist after first run") + buff.Reset() + + // Second run: task should be up to date. + require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"})) + assert.Equal(t, `task: Task "build" is up to date`+"\n", buff.String()) + buff.Reset() + + // Delete the generated file (simulate a clean), but leave the checksum file. + require.NoError(t, os.Remove(generatedFile)) + _, err = os.Stat(generatedFile) + require.Error(t, err, "generated.txt should be gone") + + // Third run: task MUST re-run because generated.txt is missing. + // This is the regression: previously the task was incorrectly skipped. + require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"})) + assert.NotContains(t, buff.String(), "is up to date", "task should re-run when generated file is missing") + _, err = os.Stat(generatedFile) + require.NoError(t, err, "generated.txt should be recreated after third run") +} + func TestStatusVariables(t *testing.T) { t.Parallel() diff --git a/testdata/timestamp/.gitignore b/testdata/timestamp/.gitignore new file mode 100644 index 0000000000..8443a36988 --- /dev/null +++ b/testdata/timestamp/.gitignore @@ -0,0 +1,2 @@ +.task +generated.txt diff --git a/testdata/timestamp/Taskfile.yml b/testdata/timestamp/Taskfile.yml new file mode 100644 index 0000000000..450ed56021 --- /dev/null +++ b/testdata/timestamp/Taskfile.yml @@ -0,0 +1,11 @@ +version: '3' + +tasks: + build: + cmds: + - cp ./source.txt ./generated.txt + sources: + - ./source.txt + generates: + - ./generated.txt + method: timestamp diff --git a/testdata/timestamp/source.txt b/testdata/timestamp/source.txt new file mode 100644 index 0000000000..3a4e1f3cac --- /dev/null +++ b/testdata/timestamp/source.txt @@ -0,0 +1 @@ +hello from source