github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/apis/trigger/trigger_test.go (about)

     1  package trigger
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"reflect"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/stretchr/testify/require"
    12  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	"k8s.io/apimachinery/pkg/runtime/schema"
    15  	"k8s.io/apimachinery/pkg/types"
    16  	ctrl "sigs.k8s.io/controller-runtime"
    17  	"sigs.k8s.io/controller-runtime/pkg/builder"
    18  	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
    19  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    20  
    21  	"github.com/tilt-dev/tilt/internal/controllers/apiset"
    22  	"github.com/tilt-dev/tilt/internal/controllers/fake"
    23  	"github.com/tilt-dev/tilt/internal/controllers/indexer"
    24  	"github.com/tilt-dev/tilt/internal/timecmp"
    25  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    26  )
    27  
    28  func TestSetupControllerRestartOn(t *testing.T) {
    29  	cfb := fake.NewControllerFixtureBuilder(t)
    30  
    31  	spec := &v1alpha1.RestartOnSpec{
    32  		UIButtons:   []string{"btn1"},
    33  		FileWatches: []string{"fw1"},
    34  	}
    35  
    36  	c := &fakeReconciler{
    37  		indexer: indexer.NewIndexer(cfb.Scheme()),
    38  		onCreateBuilder: func(b *builder.Builder, i *indexer.Indexer) {
    39  			b.For(&v1alpha1.Cmd{})
    40  			SetupControllerRestartOn(b, i, func(_ ctrlclient.Object) *v1alpha1.RestartOnSpec {
    41  				return spec
    42  			})
    43  		},
    44  	}
    45  	f := cfb.Build(c)
    46  
    47  	cmd := &v1alpha1.Cmd{ObjectMeta: metav1.ObjectMeta{Name: "cmd1"}}
    48  	f.Create(cmd)
    49  	c.indexer.OnReconcile(types.NamespacedName{Name: cmd.Name}, cmd)
    50  
    51  	ctx := context.Background()
    52  	reqs := c.indexer.Enqueue(ctx, &v1alpha1.UIButton{ObjectMeta: metav1.ObjectMeta{Name: "btn1"}})
    53  	require.Equal(t, []reconcile.Request{{NamespacedName: types.NamespacedName{Name: "cmd1"}}}, reqs)
    54  
    55  	reqs = c.indexer.Enqueue(ctx, &v1alpha1.FileWatch{ObjectMeta: metav1.ObjectMeta{Name: "fw1"}})
    56  	require.Equal(t, []reconcile.Request{{NamespacedName: types.NamespacedName{Name: "cmd1"}}}, reqs)
    57  
    58  	// fw named btn1, which doesn't exist
    59  	reqs = c.indexer.Enqueue(ctx, &v1alpha1.FileWatch{ObjectMeta: metav1.ObjectMeta{Name: "btn1"}})
    60  	require.Len(t, reqs, 0)
    61  }
    62  
    63  func TestSetupControllerStartOn(t *testing.T) {
    64  	ctx := context.Background()
    65  	cfb := fake.NewControllerFixtureBuilder(t)
    66  
    67  	spec := &v1alpha1.StartOnSpec{
    68  		UIButtons: []string{"btn1"},
    69  	}
    70  
    71  	c := &fakeReconciler{
    72  		indexer: indexer.NewIndexer(cfb.Scheme()),
    73  		onCreateBuilder: func(b *builder.Builder, i *indexer.Indexer) {
    74  			b.For(&v1alpha1.Cmd{})
    75  			SetupControllerStartOn(b, i, func(_ ctrlclient.Object) *v1alpha1.StartOnSpec {
    76  				return spec
    77  			})
    78  		},
    79  	}
    80  	f := cfb.Build(c)
    81  
    82  	cmd := &v1alpha1.Cmd{ObjectMeta: metav1.ObjectMeta{Name: "cmd1"}}
    83  	f.Create(cmd)
    84  	c.indexer.OnReconcile(types.NamespacedName{Name: cmd.Name}, cmd)
    85  
    86  	reqs := c.indexer.Enqueue(ctx, &v1alpha1.UIButton{ObjectMeta: metav1.ObjectMeta{Name: "btn1"}})
    87  	require.Equal(t, []reconcile.Request{{NamespacedName: types.NamespacedName{Name: "cmd1"}}}, reqs)
    88  
    89  	// wrong name
    90  	reqs = c.indexer.Enqueue(ctx, &v1alpha1.UIButton{ObjectMeta: metav1.ObjectMeta{Name: "btn2"}})
    91  	require.Len(t, reqs, 0)
    92  
    93  	// wrong type
    94  	reqs = c.indexer.Enqueue(ctx, &v1alpha1.FileWatch{ObjectMeta: metav1.ObjectMeta{Name: "btn1"}})
    95  	require.Len(t, reqs, 0)
    96  }
    97  
    98  func TestSetupControllerStopOn(t *testing.T) {
    99  	cfb := fake.NewControllerFixtureBuilder(t)
   100  
   101  	spec := &v1alpha1.StopOnSpec{
   102  		UIButtons: []string{"btn1"},
   103  	}
   104  
   105  	c := &fakeReconciler{
   106  		indexer: indexer.NewIndexer(cfb.Scheme()),
   107  		onCreateBuilder: func(b *builder.Builder, i *indexer.Indexer) {
   108  			b.For(&v1alpha1.Cmd{})
   109  			SetupControllerStopOn(b, i, func(_ ctrlclient.Object) *v1alpha1.StopOnSpec {
   110  				return spec
   111  			})
   112  		},
   113  	}
   114  	f := cfb.Build(c)
   115  
   116  	cmd := &v1alpha1.Cmd{ObjectMeta: metav1.ObjectMeta{Name: "cmd1"}}
   117  	f.Create(cmd)
   118  	c.indexer.OnReconcile(types.NamespacedName{Name: cmd.Name}, cmd)
   119  
   120  	ctx := context.Background()
   121  	reqs := c.indexer.Enqueue(ctx, &v1alpha1.UIButton{ObjectMeta: metav1.ObjectMeta{Name: "btn1"}})
   122  	require.Equal(t, []reconcile.Request{{NamespacedName: types.NamespacedName{Name: "cmd1"}}}, reqs)
   123  
   124  	// wrong name
   125  	reqs = c.indexer.Enqueue(ctx, &v1alpha1.UIButton{ObjectMeta: metav1.ObjectMeta{Name: "btn2"}})
   126  	require.Len(t, reqs, 0)
   127  
   128  	// wrong type
   129  	reqs = c.indexer.Enqueue(ctx, &v1alpha1.FileWatch{ObjectMeta: metav1.ObjectMeta{Name: "btn1"}})
   130  	require.Len(t, reqs, 0)
   131  }
   132  
   133  func TestLastRestartEvent(t *testing.T) {
   134  	ctx := context.Background()
   135  
   136  	objs := make(apiset.ObjectSet)
   137  	btns := []*v1alpha1.UIButton{
   138  		button("btn10", time.Unix(10, 0)),
   139  		button("btn0", time.Unix(0, 0)),
   140  		button("btn3", time.Unix(3, 0)),
   141  	}
   142  	for _, btn := range btns {
   143  		objs.Add(btn)
   144  	}
   145  
   146  	fws := []*v1alpha1.FileWatch{
   147  		filewatch("fw2", time.Unix(2, 0)),
   148  		filewatch("fw20", time.Unix(20, 0)),
   149  		filewatch("fw7", time.Unix(7, 0)),
   150  	}
   151  	for _, fw := range fws {
   152  		objs.Add(fw)
   153  	}
   154  
   155  	r := &fakeReader{objs: objs}
   156  
   157  	for _, tc := range []struct {
   158  		name           string
   159  		fws            []string
   160  		buttons        []string
   161  		expectedButton string
   162  		expectedFws    []string
   163  		expectedTime   time.Time
   164  	}{
   165  		{"no match", nil, nil, "", nil, time.Time{}},
   166  		{"one fw", []string{"fw2"}, nil, "", []string{"fw2"}, time.Unix(2, 0)},
   167  		{"one button", nil, []string{"btn3"}, "btn3", nil, time.Unix(3, 0)},
   168  		{"all buttons", nil, []string{"btn10", "btn0", "btn3"}, "btn10", nil, time.Unix(10, 0)},
   169  		{"all objects", []string{"fw2", "fw20", "fw7"}, []string{"btn10", "btn0", "btn3"}, "", []string{"fw2", "fw20", "fw7"}, time.Unix(20, 0)},
   170  	} {
   171  		t.Run(tc.name, func(t *testing.T) {
   172  
   173  			spec := &v1alpha1.RestartOnSpec{
   174  				FileWatches: tc.fws,
   175  				UIButtons:   tc.buttons,
   176  			}
   177  
   178  			ts, btn, fws, err := LastRestartEvent(ctx, r, spec)
   179  			timecmp.RequireTimeEqual(t, tc.expectedTime, ts)
   180  			buttonName := ""
   181  			if btn != nil {
   182  				buttonName = btn.Name
   183  			}
   184  			require.Equal(t, tc.expectedButton, buttonName, "button")
   185  			var fwNames []string
   186  			for _, fw := range fws {
   187  				fwNames = append(fwNames, fw.Name)
   188  			}
   189  			require.ElementsMatch(t, tc.expectedFws, fwNames, "fws")
   190  			require.NoError(t, err, "err")
   191  		})
   192  	}
   193  }
   194  
   195  func TestLastStartEvent(t *testing.T) {
   196  	ctx := context.Background()
   197  
   198  	objs := make(apiset.ObjectSet)
   199  	btns := []*v1alpha1.UIButton{
   200  		button("btn10", time.Unix(10, 0)),
   201  		button("btn0", time.Unix(0, 0)),
   202  		button("btn3", time.Unix(3, 0)),
   203  	}
   204  	for _, btn := range btns {
   205  		objs.Add(btn)
   206  	}
   207  
   208  	r := &fakeReader{objs: objs}
   209  
   210  	for _, tc := range []struct {
   211  		name           string
   212  		buttons        []string
   213  		expectedButton string
   214  		expectedTime   time.Time
   215  	}{
   216  		{"no match", nil, "", time.Time{}},
   217  		{"one button", []string{"btn3"}, "btn3", time.Unix(3, 0)},
   218  		{"all buttons", []string{"btn10", "btn0", "btn3"}, "btn10", time.Unix(10, 0)},
   219  	} {
   220  		t.Run(tc.name, func(t *testing.T) {
   221  
   222  			spec := &v1alpha1.StartOnSpec{
   223  				UIButtons: tc.buttons,
   224  			}
   225  
   226  			ts, btn, err := LastStartEvent(ctx, r, spec)
   227  			require.Equal(t, tc.expectedTime.UTC(), ts.UTC(), "timestamp")
   228  			buttonName := ""
   229  			if btn != nil {
   230  				buttonName = btn.Name
   231  			}
   232  			require.Equal(t, tc.expectedButton, buttonName, "button")
   233  			require.NoError(t, err, "err")
   234  		})
   235  	}
   236  }
   237  
   238  func TestLastStopEvent(t *testing.T) {
   239  	ctx := context.Background()
   240  
   241  	objs := make(apiset.ObjectSet)
   242  	btns := []*v1alpha1.UIButton{
   243  		button("btn10", time.Unix(10, 0)),
   244  		button("btn0", time.Unix(0, 0)),
   245  		button("btn3", time.Unix(3, 0)),
   246  	}
   247  	for _, btn := range btns {
   248  		objs.Add(btn)
   249  	}
   250  
   251  	r := &fakeReader{objs: objs}
   252  
   253  	for _, tc := range []struct {
   254  		name           string
   255  		buttons        []string
   256  		expectedButton string
   257  		expectedTime   time.Time
   258  	}{
   259  		{"no match", nil, "", time.Time{}},
   260  		{"one button", []string{"btn3"}, "btn3", time.Unix(3, 0)},
   261  		{"all buttons", []string{"btn10", "btn0", "btn3"}, "btn10", time.Unix(10, 0)},
   262  	} {
   263  		t.Run(tc.name, func(t *testing.T) {
   264  
   265  			spec := &v1alpha1.StopOnSpec{
   266  				UIButtons: tc.buttons,
   267  			}
   268  
   269  			ts, btn, err := LastStopEvent(ctx, r, spec)
   270  			timecmp.RequireTimeEqual(t, tc.expectedTime, ts)
   271  			buttonName := ""
   272  			if btn != nil {
   273  				buttonName = btn.Name
   274  			}
   275  			require.Equal(t, tc.expectedButton, buttonName, "button")
   276  			require.NoError(t, err, "err")
   277  		})
   278  	}
   279  }
   280  
   281  func TestLastRestartEventError(t *testing.T) {
   282  	expected := errors.New("oh no")
   283  	cli := &explodingReader{err: expected}
   284  	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   285  	defer cancel()
   286  	ts, btn, fw, err := LastRestartEvent(ctx, cli, &v1alpha1.RestartOnSpec{FileWatches: []string{"foo"}})
   287  	require.Equal(t, expected, err)
   288  	require.Zero(t, ts, "Timestamp was not zero value")
   289  	require.Nil(t, btn, "Button was not nil")
   290  	require.Nil(t, fw, "FileWatch was not nil")
   291  }
   292  
   293  func TestLastStartEventError(t *testing.T) {
   294  	expected := errors.New("oh no")
   295  	cli := &explodingReader{err: expected}
   296  	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   297  	defer cancel()
   298  	ts, btn, err := LastStartEvent(ctx, cli, &v1alpha1.StartOnSpec{UIButtons: []string{"foo"}})
   299  	require.Equal(t, expected, err)
   300  	require.Zero(t, ts, "Timestamp was not zero value")
   301  	require.Nil(t, btn, "Button was not nil")
   302  }
   303  
   304  func TestLastStopEventError(t *testing.T) {
   305  	expected := errors.New("oh no")
   306  	cli := &explodingReader{err: expected}
   307  	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   308  	defer cancel()
   309  	ts, btn, err := LastStopEvent(ctx, cli, &v1alpha1.StopOnSpec{UIButtons: []string{"foo"}})
   310  	require.Equal(t, expected, err)
   311  	require.Zero(t, ts, "Timestamp was not zero value")
   312  	require.Nil(t, btn, "Button was not nil")
   313  }
   314  
   315  type explodingReader struct {
   316  	err error
   317  }
   318  
   319  func (e explodingReader) Get(_ context.Context, _ ctrlclient.ObjectKey, _ ctrlclient.Object, _ ...ctrlclient.GetOption) error {
   320  	return e.err
   321  }
   322  
   323  func (e explodingReader) List(_ context.Context, _ ctrlclient.ObjectList, _ ...ctrlclient.ListOption) error {
   324  	return e.err
   325  }
   326  
   327  type fakeReader struct {
   328  	objs apiset.ObjectSet
   329  }
   330  
   331  func (f *fakeReader) Get(ctx context.Context, key ctrlclient.ObjectKey, out ctrlclient.Object, _ ...ctrlclient.GetOption) error {
   332  	if f.objs == nil {
   333  		return errors.New("fakeReader.objs uninitialized")
   334  	}
   335  
   336  	typedObjectSet := f.objs.GetSetForType(out.(apiset.Object))
   337  	obj, ok := typedObjectSet[key.Name]
   338  	if !ok {
   339  		return apierrors.NewNotFound(schema.GroupResource{}, key.Name)
   340  	}
   341  
   342  	outVal := reflect.ValueOf(out)
   343  	objVal := reflect.ValueOf(obj)
   344  	if !objVal.Type().AssignableTo(outVal.Type()) {
   345  		return fmt.Errorf("fakeReader objs[%s] is type %s, but %s was asked for", key.Name, objVal.Type(), outVal.Type())
   346  	}
   347  	reflect.Indirect(outVal).Set(reflect.Indirect(objVal))
   348  	return nil
   349  }
   350  
   351  func (f *fakeReader) List(ctx context.Context, list ctrlclient.ObjectList, opts ...ctrlclient.ListOption) error {
   352  	panic("implement me")
   353  }
   354  
   355  var _ ctrlclient.Reader = &fakeReader{}
   356  
   357  type fakeReconciler struct {
   358  	indexer         *indexer.Indexer
   359  	onCreateBuilder func(builder *builder.Builder, idxer *indexer.Indexer)
   360  }
   361  
   362  func (fr *fakeReconciler) CreateBuilder(mgr ctrl.Manager) (*builder.Builder, error) {
   363  	b := ctrl.NewControllerManagedBy(mgr)
   364  	fr.onCreateBuilder(b, fr.indexer)
   365  
   366  	return b, nil
   367  }
   368  
   369  func (fr *fakeReconciler) Reconcile(_ context.Context, _ reconcile.Request) (reconcile.Result, error) {
   370  	return reconcile.Result{}, nil
   371  }
   372  
   373  func button(name string, ts time.Time) *v1alpha1.UIButton {
   374  	return &v1alpha1.UIButton{
   375  		ObjectMeta: metav1.ObjectMeta{Name: name},
   376  		Status:     v1alpha1.UIButtonStatus{LastClickedAt: metav1.NewMicroTime(ts)},
   377  	}
   378  }
   379  
   380  func filewatch(name string, ts time.Time) *v1alpha1.FileWatch {
   381  	return &v1alpha1.FileWatch{
   382  		ObjectMeta: metav1.ObjectMeta{Name: name},
   383  		Status:     v1alpha1.FileWatchStatus{LastEventTime: metav1.NewMicroTime(ts)},
   384  	}
   385  }