mirror of
https://github.com/go-task/task.git
synced 2026-06-29 23:55:18 +00:00
refactor: decouple fingerprinting from executor (#1039)
This commit is contained in:
31
internal/env/env.go
vendored
Normal file
31
internal/env/env.go
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
func Get(t *taskfile.Task) []string {
|
||||
if t.Env == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
environ := os.Environ()
|
||||
|
||||
for k, v := range t.Env.ToCacheMap() {
|
||||
str, isString := v.(string)
|
||||
if !isString {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, alreadySet := os.LookupEnv(k); alreadySet {
|
||||
continue
|
||||
}
|
||||
|
||||
environ = append(environ, fmt.Sprintf("%s=%s", k, str))
|
||||
}
|
||||
|
||||
return environ
|
||||
}
|
||||
20
internal/fingerprint/checker.go
Normal file
20
internal/fingerprint/checker.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
// StatusCheckable defines any type that can check if the status of a task is up-to-date.
|
||||
type StatusCheckable interface {
|
||||
IsUpToDate(ctx context.Context, t *taskfile.Task) (bool, error)
|
||||
}
|
||||
|
||||
// SourcesCheckable defines any type that can check if the sources of a task are up-to-date.
|
||||
type SourcesCheckable interface {
|
||||
IsUpToDate(t *taskfile.Task) (bool, error)
|
||||
Value(t *taskfile.Task) (interface{}, error)
|
||||
OnError(t *taskfile.Task) error
|
||||
Kind() string
|
||||
}
|
||||
132
internal/fingerprint/checker_mock.go
Normal file
132
internal/fingerprint/checker_mock.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: checker.go
|
||||
|
||||
// Package fingerprint is a generated GoMock package.
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
taskfile "github.com/go-task/task/v3/taskfile"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockStatusCheckable is a mock of StatusCheckable interface.
|
||||
type MockStatusCheckable struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockStatusCheckableMockRecorder
|
||||
}
|
||||
|
||||
// MockStatusCheckableMockRecorder is the mock recorder for MockStatusCheckable.
|
||||
type MockStatusCheckableMockRecorder struct {
|
||||
mock *MockStatusCheckable
|
||||
}
|
||||
|
||||
// NewMockStatusCheckable creates a new mock instance.
|
||||
func NewMockStatusCheckable(ctrl *gomock.Controller) *MockStatusCheckable {
|
||||
mock := &MockStatusCheckable{ctrl: ctrl}
|
||||
mock.recorder = &MockStatusCheckableMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockStatusCheckable) EXPECT() *MockStatusCheckableMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// IsUpToDate mocks base method.
|
||||
func (m *MockStatusCheckable) IsUpToDate(ctx context.Context, t *taskfile.Task) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsUpToDate", ctx, t)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// IsUpToDate indicates an expected call of IsUpToDate.
|
||||
func (mr *MockStatusCheckableMockRecorder) IsUpToDate(ctx, t interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUpToDate", reflect.TypeOf((*MockStatusCheckable)(nil).IsUpToDate), ctx, t)
|
||||
}
|
||||
|
||||
// MockSourcesCheckable is a mock of SourcesCheckable interface.
|
||||
type MockSourcesCheckable struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockSourcesCheckableMockRecorder
|
||||
}
|
||||
|
||||
// MockSourcesCheckableMockRecorder is the mock recorder for MockSourcesCheckable.
|
||||
type MockSourcesCheckableMockRecorder struct {
|
||||
mock *MockSourcesCheckable
|
||||
}
|
||||
|
||||
// NewMockSourcesCheckable creates a new mock instance.
|
||||
func NewMockSourcesCheckable(ctrl *gomock.Controller) *MockSourcesCheckable {
|
||||
mock := &MockSourcesCheckable{ctrl: ctrl}
|
||||
mock.recorder = &MockSourcesCheckableMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockSourcesCheckable) EXPECT() *MockSourcesCheckableMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// IsUpToDate mocks base method.
|
||||
func (m *MockSourcesCheckable) IsUpToDate(t *taskfile.Task) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsUpToDate", t)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// IsUpToDate indicates an expected call of IsUpToDate.
|
||||
func (mr *MockSourcesCheckableMockRecorder) IsUpToDate(t interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUpToDate", reflect.TypeOf((*MockSourcesCheckable)(nil).IsUpToDate), t)
|
||||
}
|
||||
|
||||
// Kind mocks base method.
|
||||
func (m *MockSourcesCheckable) Kind() string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Kind")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Kind indicates an expected call of Kind.
|
||||
func (mr *MockSourcesCheckableMockRecorder) Kind() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Kind", reflect.TypeOf((*MockSourcesCheckable)(nil).Kind))
|
||||
}
|
||||
|
||||
// OnError mocks base method.
|
||||
func (m *MockSourcesCheckable) OnError(t *taskfile.Task) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "OnError", t)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// OnError indicates an expected call of OnError.
|
||||
func (mr *MockSourcesCheckableMockRecorder) OnError(t interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnError", reflect.TypeOf((*MockSourcesCheckable)(nil).OnError), t)
|
||||
}
|
||||
|
||||
// Value mocks base method.
|
||||
func (m *MockSourcesCheckable) Value(t *taskfile.Task) (interface{}, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Value", t)
|
||||
ret0, _ := ret[0].(interface{})
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Value indicates an expected call of Value.
|
||||
func (mr *MockSourcesCheckableMockRecorder) Value(t interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Value", reflect.TypeOf((*MockSourcesCheckable)(nil).Value), t)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package status
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"os"
|
||||
16
internal/fingerprint/sources.go
Normal file
16
internal/fingerprint/sources.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package fingerprint
|
||||
|
||||
import "fmt"
|
||||
|
||||
func NewSourcesChecker(method, tempDir string, dry bool) (SourcesCheckable, error) {
|
||||
switch method {
|
||||
case "timestamp":
|
||||
return NewTimestampChecker(tempDir, dry), nil
|
||||
case "checksum":
|
||||
return NewChecksumChecker(tempDir, dry), nil
|
||||
case "none":
|
||||
return NoneChecker{}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf(`task: invalid method "%s"`, method)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package status
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
@@ -10,51 +10,54 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
// Checksum validades if a task is up to date by calculating its source
|
||||
// ChecksumChecker validates if a task is up to date by calculating its source
|
||||
// files checksum
|
||||
type Checksum struct {
|
||||
TempDir string
|
||||
TaskDir string
|
||||
Task string
|
||||
Sources []string
|
||||
Generates []string
|
||||
Dry bool
|
||||
type ChecksumChecker struct {
|
||||
tempDir string
|
||||
dry bool
|
||||
}
|
||||
|
||||
// IsUpToDate implements the Checker interface
|
||||
func (c *Checksum) IsUpToDate() (bool, error) {
|
||||
if len(c.Sources) == 0 {
|
||||
func NewChecksumChecker(tempDir string, dry bool) *ChecksumChecker {
|
||||
return &ChecksumChecker{
|
||||
tempDir: tempDir,
|
||||
dry: dry,
|
||||
}
|
||||
}
|
||||
|
||||
func (checker *ChecksumChecker) IsUpToDate(t *taskfile.Task) (bool, error) {
|
||||
if len(t.Sources) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
checksumFile := c.checksumFilePath()
|
||||
checksumFile := checker.checksumFilePath(t)
|
||||
|
||||
data, _ := os.ReadFile(checksumFile)
|
||||
oldMd5 := strings.TrimSpace(string(data))
|
||||
|
||||
sources, err := globs(c.TaskDir, c.Sources)
|
||||
sources, err := globs(t.Dir, t.Sources)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
newMd5, err := c.checksum(sources...)
|
||||
newMd5, err := checker.checksum(sources...)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if !c.Dry {
|
||||
_ = os.MkdirAll(filepathext.SmartJoin(c.TempDir, "checksum"), 0o755)
|
||||
if !checker.dry {
|
||||
_ = os.MkdirAll(filepathext.SmartJoin(checker.tempDir, "checksum"), 0o755)
|
||||
if err = os.WriteFile(checksumFile, []byte(newMd5+"\n"), 0o644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.Generates) > 0 {
|
||||
if len(t.Generates) > 0 {
|
||||
// For each specified 'generates' field, check whether the files actually exist
|
||||
for _, g := range c.Generates {
|
||||
generates, err := Glob(c.TaskDir, g)
|
||||
for _, g := range t.Generates {
|
||||
generates, err := Glob(t.Dir, g)
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
@@ -70,7 +73,22 @@ func (c *Checksum) IsUpToDate() (bool, error) {
|
||||
return oldMd5 == newMd5, nil
|
||||
}
|
||||
|
||||
func (c *Checksum) checksum(files ...string) (string, error) {
|
||||
func (checker *ChecksumChecker) Value(t *taskfile.Task) (interface{}, error) {
|
||||
return checker.checksum()
|
||||
}
|
||||
|
||||
func (checker *ChecksumChecker) OnError(t *taskfile.Task) error {
|
||||
if len(t.Sources) == 0 {
|
||||
return nil
|
||||
}
|
||||
return os.Remove(checker.checksumFilePath(t))
|
||||
}
|
||||
|
||||
func (*ChecksumChecker) Kind() string {
|
||||
return "checksum"
|
||||
}
|
||||
|
||||
func (c *ChecksumChecker) checksum(files ...string) (string, error) {
|
||||
h := md5.New()
|
||||
|
||||
for _, f := range files {
|
||||
@@ -91,31 +109,13 @@ func (c *Checksum) checksum(files ...string) (string, error) {
|
||||
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// Value implements the Checker Interface
|
||||
func (c *Checksum) Value() (interface{}, error) {
|
||||
return c.checksum()
|
||||
}
|
||||
|
||||
// OnError implements the Checker interface
|
||||
func (c *Checksum) OnError() error {
|
||||
if len(c.Sources) == 0 {
|
||||
return nil
|
||||
}
|
||||
return os.Remove(c.checksumFilePath())
|
||||
}
|
||||
|
||||
// Kind implements the Checker Interface
|
||||
func (*Checksum) Kind() string {
|
||||
return "checksum"
|
||||
}
|
||||
|
||||
func (c *Checksum) checksumFilePath() string {
|
||||
return filepath.Join(c.TempDir, "checksum", normalizeFilename(c.Task))
|
||||
func (checker *ChecksumChecker) checksumFilePath(t *taskfile.Task) string {
|
||||
return filepath.Join(checker.tempDir, "checksum", normalizeFilename(t.Name()))
|
||||
}
|
||||
|
||||
var checksumFilenameRegexp = regexp.MustCompile("[^A-z0-9]")
|
||||
|
||||
// replaces invalid caracters on filenames with "-"
|
||||
// replaces invalid characters on filenames with "-"
|
||||
func normalizeFilename(f string) string {
|
||||
return checksumFilenameRegexp.ReplaceAllString(f, "-")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package status
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"testing"
|
||||
23
internal/fingerprint/sources_none.go
Normal file
23
internal/fingerprint/sources_none.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package fingerprint
|
||||
|
||||
import "github.com/go-task/task/v3/taskfile"
|
||||
|
||||
// NoneChecker is a no-op Checker.
|
||||
// It will always report that the task is not up-to-date.
|
||||
type NoneChecker struct{}
|
||||
|
||||
func (NoneChecker) IsUpToDate(t *taskfile.Task) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (NoneChecker) Value(t *taskfile.Task) (interface{}, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (NoneChecker) OnError(t *taskfile.Task) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (NoneChecker) Kind() string {
|
||||
return "none"
|
||||
}
|
||||
@@ -1,24 +1,29 @@
|
||||
package status
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
// Timestamp checks if any source change compared with the generated files,
|
||||
// TimestampChecker checks if any source change compared with the generated files,
|
||||
// using file modifications timestamps.
|
||||
type Timestamp struct {
|
||||
TempDir string
|
||||
Task string
|
||||
Dir string
|
||||
Sources []string
|
||||
Generates []string
|
||||
Dry bool
|
||||
type TimestampChecker struct {
|
||||
tempDir string
|
||||
dry bool
|
||||
}
|
||||
|
||||
func NewTimestampChecker(tempDir string, dry bool) *TimestampChecker {
|
||||
return &TimestampChecker{
|
||||
tempDir: tempDir,
|
||||
dry: dry,
|
||||
}
|
||||
}
|
||||
|
||||
// IsUpToDate implements the Checker interface
|
||||
func (t *Timestamp) IsUpToDate() (bool, error) {
|
||||
func (checker *TimestampChecker) IsUpToDate(t *taskfile.Task) (bool, error) {
|
||||
if len(t.Sources) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
@@ -32,7 +37,7 @@ func (t *Timestamp) IsUpToDate() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
timestampFile := t.timestampFilePath()
|
||||
timestampFile := checker.timestampFilePath(t)
|
||||
|
||||
// If the file exists, add the file path to the generates.
|
||||
// If the generate file is old, the task will be executed.
|
||||
@@ -41,7 +46,7 @@ func (t *Timestamp) IsUpToDate() (bool, error) {
|
||||
generates = append(generates, timestampFile)
|
||||
} else {
|
||||
// Create the timestamp file for the next execution when the file does not exist.
|
||||
if !t.Dry {
|
||||
if !checker.dry {
|
||||
if err := os.MkdirAll(filepath.Dir(timestampFile), 0o755); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -70,7 +75,7 @@ func (t *Timestamp) IsUpToDate() (bool, error) {
|
||||
}
|
||||
|
||||
// Modify the metadata of the file to the the current time.
|
||||
if !t.Dry {
|
||||
if !checker.dry {
|
||||
if err := os.Chtimes(timestampFile, taskTime, taskTime); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -79,12 +84,12 @@ func (t *Timestamp) IsUpToDate() (bool, error) {
|
||||
return !shouldUpdate, nil
|
||||
}
|
||||
|
||||
func (t *Timestamp) Kind() string {
|
||||
func (checker *TimestampChecker) Kind() string {
|
||||
return "timestamp"
|
||||
}
|
||||
|
||||
// Value implements the Checker Interface
|
||||
func (t *Timestamp) Value() (interface{}, error) {
|
||||
func (checker *TimestampChecker) Value(t *taskfile.Task) (interface{}, error) {
|
||||
sources, err := globs(t.Dir, t.Sources)
|
||||
if err != nil {
|
||||
return time.Now(), err
|
||||
@@ -137,10 +142,10 @@ func anyFileNewerThan(files []string, givenTime time.Time) (bool, error) {
|
||||
}
|
||||
|
||||
// OnError implements the Checker interface
|
||||
func (*Timestamp) OnError() error {
|
||||
func (*TimestampChecker) OnError(t *taskfile.Task) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Timestamp) timestampFilePath() string {
|
||||
return filepath.Join(t.TempDir, "timestamp", normalizeFilename(t.Task))
|
||||
func (checker *TimestampChecker) timestampFilePath(t *taskfile.Task) string {
|
||||
return filepath.Join(checker.tempDir, "timestamp", normalizeFilename(t.Task))
|
||||
}
|
||||
36
internal/fingerprint/status.go
Normal file
36
internal/fingerprint/status.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-task/task/v3/internal/env"
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
type StatusChecker struct {
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
func NewStatusChecker(logger *logger.Logger) StatusCheckable {
|
||||
return &StatusChecker{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (checker *StatusChecker) IsUpToDate(ctx context.Context, t *taskfile.Task) (bool, error) {
|
||||
for _, s := range t.Status {
|
||||
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
|
||||
Command: s,
|
||||
Dir: t.Dir,
|
||||
Env: env.Get(t),
|
||||
})
|
||||
if err != nil {
|
||||
checker.logger.VerboseOutf(logger.Yellow, "task: status command %s exited non-zero: %s", s, err)
|
||||
return false, nil
|
||||
}
|
||||
checker.logger.VerboseOutf(logger.Yellow, "task: status command %s exited zero", s)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
132
internal/fingerprint/task.go
Normal file
132
internal/fingerprint/task.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
type (
|
||||
CheckerOption func(*CheckerConfig)
|
||||
CheckerConfig struct {
|
||||
method string
|
||||
dry bool
|
||||
tempDir string
|
||||
logger *logger.Logger
|
||||
statusChecker StatusCheckable
|
||||
sourcesChecker SourcesCheckable
|
||||
}
|
||||
)
|
||||
|
||||
func WithMethod(method string) CheckerOption {
|
||||
return func(config *CheckerConfig) {
|
||||
config.method = method
|
||||
}
|
||||
}
|
||||
|
||||
func WithDry(dry bool) CheckerOption {
|
||||
return func(config *CheckerConfig) {
|
||||
config.dry = dry
|
||||
}
|
||||
}
|
||||
|
||||
func WithTempDir(tempDir string) CheckerOption {
|
||||
return func(config *CheckerConfig) {
|
||||
config.tempDir = tempDir
|
||||
}
|
||||
}
|
||||
|
||||
func WithLogger(logger *logger.Logger) CheckerOption {
|
||||
return func(config *CheckerConfig) {
|
||||
config.logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
func WithStatusChecker(checker StatusCheckable) CheckerOption {
|
||||
return func(config *CheckerConfig) {
|
||||
config.statusChecker = checker
|
||||
}
|
||||
}
|
||||
|
||||
func WithSourcesChecker(checker SourcesCheckable) CheckerOption {
|
||||
return func(config *CheckerConfig) {
|
||||
config.sourcesChecker = checker
|
||||
}
|
||||
}
|
||||
|
||||
func IsTaskUpToDate(
|
||||
ctx context.Context,
|
||||
t *taskfile.Task,
|
||||
opts ...CheckerOption,
|
||||
) (bool, error) {
|
||||
var statusUpToDate bool
|
||||
var sourcesUpToDate bool
|
||||
var err error
|
||||
|
||||
// Default config
|
||||
config := &CheckerConfig{
|
||||
method: "none",
|
||||
tempDir: "",
|
||||
dry: false,
|
||||
logger: nil,
|
||||
statusChecker: nil,
|
||||
sourcesChecker: nil,
|
||||
}
|
||||
|
||||
// Apply functional options
|
||||
for _, opt := range opts {
|
||||
opt(config)
|
||||
}
|
||||
|
||||
// If no status checker was given, set up the default one
|
||||
if config.statusChecker == nil {
|
||||
config.statusChecker = NewStatusChecker(config.logger)
|
||||
}
|
||||
|
||||
// If no sources checker was given, set up the default one
|
||||
if config.sourcesChecker == nil {
|
||||
config.sourcesChecker, err = NewSourcesChecker(config.method, config.tempDir, config.dry)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
statusIsSet := len(t.Status) != 0
|
||||
sourcesIsSet := len(t.Sources) != 0
|
||||
|
||||
// If status is set, check if it is up-to-date
|
||||
if statusIsSet {
|
||||
statusUpToDate, err = config.statusChecker.IsUpToDate(ctx, t)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// If sources is set, check if they are up-to-date
|
||||
if sourcesIsSet {
|
||||
sourcesUpToDate, err = config.sourcesChecker.IsUpToDate(t)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// If both status and sources are set, the task is up-to-date if both are up-to-date
|
||||
if statusIsSet && sourcesIsSet {
|
||||
return statusUpToDate && sourcesUpToDate, nil
|
||||
}
|
||||
|
||||
// If only status is set, the task is up-to-date if the status is up-to-date
|
||||
if statusIsSet {
|
||||
return statusUpToDate, nil
|
||||
}
|
||||
|
||||
// If only sources is set, the task is up-to-date if the sources are up-to-date
|
||||
if sourcesIsSet {
|
||||
return sourcesUpToDate, nil
|
||||
}
|
||||
|
||||
// If no status or sources are set, the task should always run
|
||||
// i.e. it is never considered "up-to-date"
|
||||
return false, nil
|
||||
}
|
||||
174
internal/fingerprint/task_test.go
Normal file
174
internal/fingerprint/task_test.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
// 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 |
|
||||
func TestIsTaskUpToDate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
task *taskfile.Task
|
||||
setupMockStatusChecker func(m *MockStatusCheckable)
|
||||
setupMockSourcesChecker func(m *MockSourcesCheckable)
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "expect FALSE when no status or sources are defined",
|
||||
task: &taskfile.Task{
|
||||
Status: nil,
|
||||
Sources: nil,
|
||||
},
|
||||
setupMockStatusChecker: nil,
|
||||
setupMockSourcesChecker: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expect TRUE when no status is defined and sources are up-to-date",
|
||||
task: &taskfile.Task{
|
||||
Status: nil,
|
||||
Sources: []string{"sources"},
|
||||
},
|
||||
setupMockStatusChecker: nil,
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any()).Return(true, nil)
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "expect FALSE when no status is defined and sources are NOT up-to-date",
|
||||
task: &taskfile.Task{
|
||||
Status: nil,
|
||||
Sources: []string{"sources"},
|
||||
},
|
||||
setupMockStatusChecker: nil,
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any()).Return(false, nil)
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expect TRUE when status is up-to-date and sources are not defined",
|
||||
task: &taskfile.Task{
|
||||
Status: []string{"status"},
|
||||
Sources: nil,
|
||||
},
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(true, nil)
|
||||
},
|
||||
setupMockSourcesChecker: nil,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "expect TRUE when status and sources are up-to-date",
|
||||
task: &taskfile.Task{
|
||||
Status: []string{"status"},
|
||||
Sources: []string{"sources"},
|
||||
},
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(true, nil)
|
||||
},
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any()).Return(true, nil)
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "expect FALSE when status is up-to-date, but sources are NOT up-to-date",
|
||||
task: &taskfile.Task{
|
||||
Status: []string{"status"},
|
||||
Sources: []string{"sources"},
|
||||
},
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(true, nil)
|
||||
},
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any()).Return(false, nil)
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expect FALSE when status is NOT up-to-date and sources are not defined",
|
||||
task: &taskfile.Task{
|
||||
Status: []string{"status"},
|
||||
Sources: nil,
|
||||
},
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(false, nil)
|
||||
},
|
||||
setupMockSourcesChecker: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expect FALSE when status is NOT up-to-date, but sources are up-to-date",
|
||||
task: &taskfile.Task{
|
||||
Status: []string{"status"},
|
||||
Sources: []string{"sources"},
|
||||
},
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(false, nil)
|
||||
},
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any()).Return(true, nil)
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expect FALSE when status and sources are NOT up-to-date",
|
||||
task: &taskfile.Task{
|
||||
Status: []string{"status"},
|
||||
Sources: []string{"sources"},
|
||||
},
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(false, nil)
|
||||
},
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any()).Return(false, nil)
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
mockStatusChecker := NewMockStatusCheckable(ctrl)
|
||||
if tt.setupMockStatusChecker != nil {
|
||||
tt.setupMockStatusChecker(mockStatusChecker)
|
||||
}
|
||||
|
||||
mockSourcesChecker := NewMockSourcesCheckable(ctrl)
|
||||
if tt.setupMockSourcesChecker != nil {
|
||||
tt.setupMockSourcesChecker(mockSourcesChecker)
|
||||
}
|
||||
|
||||
result, err := IsTaskUpToDate(
|
||||
context.Background(),
|
||||
tt.task,
|
||||
WithStatusChecker(mockStatusChecker),
|
||||
WithSourcesChecker(mockSourcesChecker),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package status
|
||||
|
||||
// None is a no-op Checker
|
||||
type None struct{}
|
||||
|
||||
// IsUpToDate implements the Checker interface
|
||||
func (None) IsUpToDate() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Value implements the Checker interface
|
||||
func (None) Value() (interface{}, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (None) Kind() string {
|
||||
return "none"
|
||||
}
|
||||
|
||||
// OnError implements the Checker interface
|
||||
func (None) OnError() error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package status
|
||||
|
||||
var (
|
||||
_ Checker = &Timestamp{}
|
||||
_ Checker = &Checksum{}
|
||||
_ Checker = None{}
|
||||
)
|
||||
|
||||
// Checker is an interface that checks if the status is up-to-date
|
||||
type Checker interface {
|
||||
IsUpToDate() (bool, error)
|
||||
Value() (interface{}, error)
|
||||
OnError() error
|
||||
Kind() string
|
||||
}
|
||||
Reference in New Issue
Block a user