2023-03-10 18:27:30 +00:00
|
|
|
package fingerprint
|
|
|
|
|
|
|
|
|
|
import (
|
2026-08-10 16:12:18 +02:00
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
2023-03-10 18:27:30 +00:00
|
|
|
"testing"
|
2026-08-10 16:12:18 +02:00
|
|
|
"time"
|
2023-03-10 18:27:30 +00:00
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
2023-04-15 21:27:14 +01:00
|
|
|
"github.com/stretchr/testify/mock"
|
2023-03-10 18:27:30 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
|
2023-12-29 20:32:03 +00:00
|
|
|
"github.com/go-task/task/v3/taskfile/ast"
|
2023-03-10 18:27:30 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// TruthTable
|
|
|
|
|
//
|
|
|
|
|
// | Status up-to-date | Sources up-to-date | Task is up-to-date |
|
|
|
|
|
// | ----------------- | ------------------ | ------------------ |
|
|
|
|
|
// | not set | not set | false |
|
|
|
|
|
// | not set | true | true |
|
|
|
|
|
// | not set | false | false |
|
|
|
|
|
// | true | not set | true |
|
|
|
|
|
// | true | true | true |
|
|
|
|
|
// | true | false | false |
|
|
|
|
|
// | false | not set | false |
|
|
|
|
|
// | false | true | false |
|
|
|
|
|
// | false | false | false |
|
2026-08-10 16:12:18 +02:00
|
|
|
func TestFingerprinterUpToDate(t *testing.T) {
|
2024-12-12 01:47:10 +01:00
|
|
|
t.Parallel()
|
|
|
|
|
|
2023-03-10 18:27:30 +00:00
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
2023-12-29 20:32:03 +00:00
|
|
|
task *ast.Task
|
2025-04-19 12:55:22 +01:00
|
|
|
setupMockStatusChecker func(m *MockStatusCheckable)
|
|
|
|
|
setupMockSourcesChecker func(m *MockSourcesCheckable)
|
2023-03-10 18:27:30 +00:00
|
|
|
expected bool
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "expect FALSE when no status or sources are defined",
|
2023-12-29 20:32:03 +00:00
|
|
|
task: &ast.Task{
|
2023-03-10 18:27:30 +00:00
|
|
|
Status: nil,
|
|
|
|
|
Sources: nil,
|
|
|
|
|
},
|
|
|
|
|
setupMockStatusChecker: nil,
|
|
|
|
|
setupMockSourcesChecker: nil,
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "expect TRUE when no status is defined and sources are up-to-date",
|
2023-12-29 20:32:03 +00:00
|
|
|
task: &ast.Task{
|
2023-03-10 18:27:30 +00:00
|
|
|
Status: nil,
|
2023-12-29 20:32:03 +00:00
|
|
|
Sources: []*ast.Glob{{Glob: "sources"}},
|
2023-03-10 18:27:30 +00:00
|
|
|
},
|
|
|
|
|
setupMockStatusChecker: nil,
|
2025-04-19 12:55:22 +01:00
|
|
|
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
2023-04-15 21:27:14 +01:00
|
|
|
m.EXPECT().IsUpToDate(mock.Anything).Return(true, nil)
|
2023-03-10 18:27:30 +00:00
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "expect FALSE when no status is defined and sources are NOT up-to-date",
|
2023-12-29 20:32:03 +00:00
|
|
|
task: &ast.Task{
|
2023-03-10 18:27:30 +00:00
|
|
|
Status: nil,
|
2023-12-29 20:32:03 +00:00
|
|
|
Sources: []*ast.Glob{{Glob: "sources"}},
|
2023-03-10 18:27:30 +00:00
|
|
|
},
|
|
|
|
|
setupMockStatusChecker: nil,
|
2025-04-19 12:55:22 +01:00
|
|
|
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
2023-04-15 21:27:14 +01:00
|
|
|
m.EXPECT().IsUpToDate(mock.Anything).Return(false, nil)
|
2023-03-10 18:27:30 +00:00
|
|
|
},
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "expect TRUE when status is up-to-date and sources are not defined",
|
2023-12-29 20:32:03 +00:00
|
|
|
task: &ast.Task{
|
2023-03-10 18:27:30 +00:00
|
|
|
Status: []string{"status"},
|
|
|
|
|
Sources: nil,
|
|
|
|
|
},
|
2025-04-19 12:55:22 +01:00
|
|
|
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
2023-04-15 21:27:14 +01:00
|
|
|
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil)
|
2023-03-10 18:27:30 +00:00
|
|
|
},
|
|
|
|
|
setupMockSourcesChecker: nil,
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "expect TRUE when status and sources are up-to-date",
|
2023-12-29 20:32:03 +00:00
|
|
|
task: &ast.Task{
|
2023-03-10 18:27:30 +00:00
|
|
|
Status: []string{"status"},
|
2023-12-29 20:32:03 +00:00
|
|
|
Sources: []*ast.Glob{{Glob: "sources"}},
|
2023-03-10 18:27:30 +00:00
|
|
|
},
|
2025-04-19 12:55:22 +01:00
|
|
|
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
2023-04-15 21:27:14 +01:00
|
|
|
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil)
|
2023-03-10 18:27:30 +00:00
|
|
|
},
|
2025-04-19 12:55:22 +01:00
|
|
|
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
2023-04-15 21:27:14 +01:00
|
|
|
m.EXPECT().IsUpToDate(mock.Anything).Return(true, nil)
|
2023-03-10 18:27:30 +00:00
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "expect FALSE when status is up-to-date, but sources are NOT up-to-date",
|
2023-12-29 20:32:03 +00:00
|
|
|
task: &ast.Task{
|
2023-03-10 18:27:30 +00:00
|
|
|
Status: []string{"status"},
|
2023-12-29 20:32:03 +00:00
|
|
|
Sources: []*ast.Glob{{Glob: "sources"}},
|
2023-03-10 18:27:30 +00:00
|
|
|
},
|
2025-04-19 12:55:22 +01:00
|
|
|
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
2023-04-15 21:27:14 +01:00
|
|
|
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil)
|
2023-03-10 18:27:30 +00:00
|
|
|
},
|
2025-04-19 12:55:22 +01:00
|
|
|
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
2023-04-15 21:27:14 +01:00
|
|
|
m.EXPECT().IsUpToDate(mock.Anything).Return(false, nil)
|
2023-03-10 18:27:30 +00:00
|
|
|
},
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "expect FALSE when status is NOT up-to-date and sources are not defined",
|
2023-12-29 20:32:03 +00:00
|
|
|
task: &ast.Task{
|
2023-03-10 18:27:30 +00:00
|
|
|
Status: []string{"status"},
|
|
|
|
|
Sources: nil,
|
|
|
|
|
},
|
2025-04-19 12:55:22 +01:00
|
|
|
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
2023-04-15 21:27:14 +01:00
|
|
|
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil)
|
2023-03-10 18:27:30 +00:00
|
|
|
},
|
|
|
|
|
setupMockSourcesChecker: nil,
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "expect FALSE when status is NOT up-to-date, but sources are up-to-date",
|
2023-12-29 20:32:03 +00:00
|
|
|
task: &ast.Task{
|
2023-03-10 18:27:30 +00:00
|
|
|
Status: []string{"status"},
|
2023-12-29 20:32:03 +00:00
|
|
|
Sources: []*ast.Glob{{Glob: "sources"}},
|
2023-03-10 18:27:30 +00:00
|
|
|
},
|
2025-04-19 12:55:22 +01:00
|
|
|
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
2023-04-15 21:27:14 +01:00
|
|
|
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil)
|
2023-03-10 18:27:30 +00:00
|
|
|
},
|
2025-04-19 12:55:22 +01:00
|
|
|
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
2023-04-15 21:27:14 +01:00
|
|
|
m.EXPECT().IsUpToDate(mock.Anything).Return(true, nil)
|
2023-03-10 18:27:30 +00:00
|
|
|
},
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "expect FALSE when status and sources are NOT up-to-date",
|
2023-12-29 20:32:03 +00:00
|
|
|
task: &ast.Task{
|
2023-03-10 18:27:30 +00:00
|
|
|
Status: []string{"status"},
|
2023-12-29 20:32:03 +00:00
|
|
|
Sources: []*ast.Glob{{Glob: "sources"}},
|
2023-03-10 18:27:30 +00:00
|
|
|
},
|
2025-04-19 12:55:22 +01:00
|
|
|
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
2023-04-15 21:27:14 +01:00
|
|
|
m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil)
|
2023-03-10 18:27:30 +00:00
|
|
|
},
|
2025-04-19 12:55:22 +01:00
|
|
|
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
2023-04-15 21:27:14 +01:00
|
|
|
m.EXPECT().IsUpToDate(mock.Anything).Return(false, nil)
|
2023-03-10 18:27:30 +00:00
|
|
|
},
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
2024-12-12 01:47:10 +01:00
|
|
|
t.Parallel()
|
|
|
|
|
|
2025-04-19 12:55:22 +01:00
|
|
|
mockStatusChecker := NewMockStatusCheckable(t)
|
2023-03-10 18:27:30 +00:00
|
|
|
if tt.setupMockStatusChecker != nil {
|
|
|
|
|
tt.setupMockStatusChecker(mockStatusChecker)
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-19 12:55:22 +01:00
|
|
|
mockSourcesChecker := NewMockSourcesCheckable(t)
|
2023-03-10 18:27:30 +00:00
|
|
|
if tt.setupMockSourcesChecker != nil {
|
|
|
|
|
tt.setupMockSourcesChecker(mockSourcesChecker)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-10 16:12:18 +02:00
|
|
|
f := NewFingerprinter("checksum", "", false, nil,
|
2023-03-10 18:27:30 +00:00
|
|
|
WithStatusChecker(mockStatusChecker),
|
|
|
|
|
WithSourcesChecker(mockSourcesChecker),
|
|
|
|
|
)
|
2026-08-10 16:12:18 +02:00
|
|
|
result, err := f.UpToDate(t.Context(), tt.task)
|
2023-03-10 18:27:30 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Equal(t, tt.expected, result)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-10 16:12:18 +02:00
|
|
|
|
|
|
|
|
// The task's own method wins over the Taskfile default, for the injected
|
|
|
|
|
// variable as much as for the up-to-date check.
|
|
|
|
|
func TestFingerprinterMethodResolution(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
defaultMethod string
|
|
|
|
|
method string
|
|
|
|
|
expectedKind string
|
|
|
|
|
expectedValue any
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "task method wins over the default",
|
|
|
|
|
defaultMethod: "checksum",
|
|
|
|
|
method: "timestamp",
|
|
|
|
|
expectedKind: "timestamp",
|
|
|
|
|
expectedValue: time.Time{},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "default method is inherited when the task declares none",
|
|
|
|
|
defaultMethod: "timestamp",
|
|
|
|
|
expectedKind: "timestamp",
|
|
|
|
|
expectedValue: time.Time{},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "checksum is inherited too",
|
|
|
|
|
defaultMethod: "checksum",
|
|
|
|
|
expectedKind: "checksum",
|
|
|
|
|
expectedValue: "",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "none is inherited too",
|
|
|
|
|
defaultMethod: "none",
|
|
|
|
|
expectedKind: "none",
|
|
|
|
|
expectedValue: "",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "source.txt"), []byte("content"), 0o644))
|
|
|
|
|
task := &ast.Task{
|
|
|
|
|
Dir: dir,
|
|
|
|
|
Method: tt.method,
|
|
|
|
|
Sources: []*ast.Glob{{Glob: "source.txt"}},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f := NewFingerprinter(tt.defaultMethod, t.TempDir(), true, nil)
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, tt.expectedKind, f.Kind(task))
|
|
|
|
|
|
|
|
|
|
// A timestamp checker yields a time, the other two a string.
|
|
|
|
|
value, err := f.SourceValue(task)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.IsType(t, tt.expectedValue, value)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only the entry points that need a checker reject an invalid method; Kind
|
|
|
|
|
// tolerates it, so that --force runs still compile.
|
|
|
|
|
func TestFingerprinterInvalidMethod(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
const wantErr = `task: invalid method "Checksum"`
|
|
|
|
|
task := &ast.Task{Sources: []*ast.Glob{{Glob: "source.txt"}}}
|
|
|
|
|
f := NewFingerprinter("Checksum", t.TempDir(), true, nil)
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, "checksum", f.Kind(task))
|
|
|
|
|
|
|
|
|
|
_, err := f.SourceValue(task)
|
|
|
|
|
require.ErrorIs(t, err, ErrInvalidMethod)
|
|
|
|
|
require.EqualError(t, err, wantErr)
|
|
|
|
|
_, err = f.UpToDate(t.Context(), task)
|
|
|
|
|
require.EqualError(t, err, wantErr)
|
|
|
|
|
require.EqualError(t, f.OnError(task), wantErr)
|
|
|
|
|
}
|