sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/crier/controller_test.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package crier
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"reflect"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/google/go-cmp/cmp"
    28  	"github.com/sirupsen/logrus"
    29  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/types"
    31  	ctrlruntime "sigs.k8s.io/controller-runtime"
    32  	ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
    33  	fakectrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
    34  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    35  
    36  	prowv1 "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    37  )
    38  
    39  const reporterName = "fakeReporter"
    40  
    41  // Fake Reporter
    42  // Sets: Which jobs should be reported
    43  // Asserts: Which jobs are actually reported
    44  type fakeReporter struct {
    45  	reported         []string
    46  	shouldReportFunc func(pj *prowv1.ProwJob) bool
    47  	res              *reconcile.Result
    48  	err              error
    49  }
    50  
    51  func (f *fakeReporter) Report(_ context.Context, _ *logrus.Entry, pj *prowv1.ProwJob) ([]*prowv1.ProwJob, *reconcile.Result, error) {
    52  	f.reported = append(f.reported, pj.Spec.Job)
    53  	return []*prowv1.ProwJob{pj}, f.res, f.err
    54  }
    55  
    56  func (f *fakeReporter) GetName() string {
    57  	return reporterName
    58  }
    59  
    60  func (f *fakeReporter) ShouldReport(_ context.Context, _ *logrus.Entry, pj *prowv1.ProwJob) bool {
    61  	return f.shouldReportFunc(pj)
    62  }
    63  
    64  func TestReconcile(t *testing.T) {
    65  
    66  	const toReconcile = "foo"
    67  	tests := []struct {
    68  		name              string
    69  		job               *prowv1.ProwJob
    70  		enablementChecker func(org, repo string) bool
    71  		shouldReport      bool
    72  		result            *reconcile.Result
    73  		reportErr         error
    74  
    75  		expectResult  reconcile.Result
    76  		expectReport  bool
    77  		expectPatch   bool
    78  		expectedError error
    79  	}{
    80  		{
    81  			name: "reports/patches known job",
    82  			job: &prowv1.ProwJob{
    83  				Spec: prowv1.ProwJobSpec{
    84  					Job:    "foo",
    85  					Report: true,
    86  				},
    87  				Status: prowv1.ProwJobStatus{
    88  					State: prowv1.TriggeredState,
    89  				},
    90  			},
    91  			shouldReport: true,
    92  			expectReport: true,
    93  			expectPatch:  true,
    94  		},
    95  		{
    96  			name: "reports/patches job whose org/repo in refs enabled",
    97  			job: &prowv1.ProwJob{
    98  				Spec: prowv1.ProwJobSpec{
    99  					Job:    "foo",
   100  					Report: true,
   101  					Refs:   &prowv1.Refs{Org: "org", Repo: "repo"},
   102  				},
   103  				Status: prowv1.ProwJobStatus{
   104  					State: prowv1.TriggeredState,
   105  				},
   106  			},
   107  			enablementChecker: func(org, repo string) bool { return org == "org" && repo == "repo" },
   108  			shouldReport:      true,
   109  			expectReport:      true,
   110  			expectPatch:       true,
   111  		},
   112  		{
   113  			name: "reports/patches job whose org/repo in extra refs enabled",
   114  			job: &prowv1.ProwJob{
   115  				Spec: prowv1.ProwJobSpec{
   116  					Job:       "foo",
   117  					Report:    true,
   118  					ExtraRefs: []prowv1.Refs{{Org: "org", Repo: "repo"}},
   119  				},
   120  				Status: prowv1.ProwJobStatus{
   121  					State: prowv1.TriggeredState,
   122  				},
   123  			},
   124  			enablementChecker: func(org, repo string) bool { return org == "org" && repo == "repo" },
   125  			shouldReport:      true,
   126  			expectReport:      true,
   127  			expectPatch:       true,
   128  		},
   129  		{
   130  			name: "reports/patches job whose org/repo in extra refs enabled, completed",
   131  			job: &prowv1.ProwJob{
   132  				Spec: prowv1.ProwJobSpec{
   133  					Job:       "foo",
   134  					Report:    true,
   135  					ExtraRefs: []prowv1.Refs{{Org: "org", Repo: "repo"}},
   136  				},
   137  				Status: prowv1.ProwJobStatus{
   138  					State:          prowv1.SuccessState,
   139  					CompletionTime: &v1.Time{Time: time.Now()},
   140  				},
   141  			},
   142  			enablementChecker: func(org, repo string) bool { return org == "org" && repo == "repo" },
   143  			shouldReport:      true,
   144  			expectReport:      true,
   145  			expectPatch:       true,
   146  		},
   147  		{
   148  			name: "reports/patches job whose org/repo in extra refs and refs have conflicting settings",
   149  			job: &prowv1.ProwJob{
   150  				Spec: prowv1.ProwJobSpec{
   151  					Job:       "foo",
   152  					Report:    true,
   153  					Refs:      &prowv1.Refs{Org: "org", Repo: "repo"},
   154  					ExtraRefs: []prowv1.Refs{{Org: "other-org", Repo: "other-repo"}},
   155  				},
   156  				Status: prowv1.ProwJobStatus{
   157  					State: prowv1.TriggeredState,
   158  				},
   159  			},
   160  			enablementChecker: func(org, repo string) bool { return org == "org" && repo == "repo" },
   161  			shouldReport:      true,
   162  			expectReport:      true,
   163  			expectPatch:       true,
   164  		},
   165  		{
   166  			name: "doesn't reports/patches job whose org/repo is not enabled",
   167  			job: &prowv1.ProwJob{
   168  				Spec: prowv1.ProwJobSpec{
   169  					Job:    "foo",
   170  					Report: true,
   171  					Refs:   &prowv1.Refs{Org: "org", Repo: "repo"},
   172  				},
   173  				Status: prowv1.ProwJobStatus{
   174  					State: prowv1.TriggeredState,
   175  				},
   176  			},
   177  			enablementChecker: func(_, _ string) bool { return false },
   178  			shouldReport:      false,
   179  			expectReport:      false,
   180  			expectPatch:       false,
   181  		},
   182  		{
   183  			name: "doesn't report when it shouldn't",
   184  			job: &prowv1.ProwJob{
   185  				Spec: prowv1.ProwJobSpec{
   186  					Job:    "foo",
   187  					Report: true,
   188  				},
   189  				Status: prowv1.ProwJobStatus{
   190  					State: prowv1.TriggeredState,
   191  				},
   192  			},
   193  			shouldReport: false,
   194  			expectReport: false,
   195  		},
   196  		{
   197  			name:         "doesn't report nonexistant job",
   198  			shouldReport: true,
   199  			expectReport: false,
   200  		},
   201  		{
   202  			name: "doesn't report when SkipReport=true (i.e. Spec.Report=false)",
   203  			job: &prowv1.ProwJob{
   204  				Spec: prowv1.ProwJobSpec{
   205  					Job:    "foo",
   206  					Report: false,
   207  				},
   208  			},
   209  			shouldReport: true,
   210  			expectReport: false,
   211  		},
   212  		{
   213  			name:         "doesn't report empty job",
   214  			job:          &prowv1.ProwJob{},
   215  			shouldReport: true,
   216  			expectReport: false,
   217  		},
   218  		{
   219  			name: "previously-reported job isn't reported",
   220  			job: &prowv1.ProwJob{
   221  				Spec: prowv1.ProwJobSpec{
   222  					Job:    "foo",
   223  					Report: true,
   224  				},
   225  				Status: prowv1.ProwJobStatus{
   226  					State: prowv1.TriggeredState,
   227  					PrevReportStates: map[string]prowv1.ProwJobState{
   228  						reporterName: prowv1.TriggeredState,
   229  					},
   230  				},
   231  			},
   232  			shouldReport: true,
   233  			expectReport: false,
   234  		},
   235  		{
   236  			name: "error is returned",
   237  			job: &prowv1.ProwJob{
   238  				Spec: prowv1.ProwJobSpec{
   239  					Job:    "foo",
   240  					Report: true,
   241  				},
   242  				Status: prowv1.ProwJobStatus{
   243  					State: prowv1.TriggeredState,
   244  				},
   245  			},
   246  			shouldReport:  true,
   247  			reportErr:     errors.New("some-err"),
   248  			expectedError: fmt.Errorf("failed to report job: %w", errors.New("some-err")),
   249  		},
   250  		{
   251  			name: "*reconcile.Result is returned, prowjob is not updated",
   252  			job: &prowv1.ProwJob{
   253  				Spec: prowv1.ProwJobSpec{
   254  					Job:    "foo",
   255  					Report: true,
   256  				},
   257  				Status: prowv1.ProwJobStatus{
   258  					State: prowv1.TriggeredState,
   259  				},
   260  			},
   261  			shouldReport: true,
   262  			result:       &reconcile.Result{RequeueAfter: time.Minute},
   263  			expectResult: reconcile.Result{RequeueAfter: time.Minute},
   264  			expectReport: true,
   265  		},
   266  	}
   267  
   268  	for _, test := range tests {
   269  		test := test
   270  		t.Run(test.name, func(t *testing.T) {
   271  			t.Parallel()
   272  			rp := fakeReporter{
   273  				shouldReportFunc: func(*prowv1.ProwJob) bool {
   274  					return test.shouldReport
   275  				},
   276  				res: test.result,
   277  				err: test.reportErr,
   278  			}
   279  
   280  			builder := fakectrlruntimeclient.NewClientBuilder()
   281  			if test.job != nil {
   282  				test.job.Name = toReconcile
   283  				builder.WithRuntimeObjects(test.job)
   284  			}
   285  			cs := &patchTrackingClient{Client: builder.Build()}
   286  			r := &reconciler{
   287  				pjclientset:       cs,
   288  				reporter:          &rp,
   289  				enablementChecker: test.enablementChecker,
   290  			}
   291  
   292  			result, err := r.Reconcile(context.Background(), ctrlruntime.Request{NamespacedName: types.NamespacedName{Name: toReconcile}})
   293  			if !reflect.DeepEqual(err, test.expectedError) {
   294  				t.Fatalf("actual err %v differs from expected err %v", err, test.expectedError)
   295  			}
   296  			if err != nil {
   297  				return
   298  			}
   299  			if diff := cmp.Diff(result, test.expectResult); diff != "" {
   300  				t.Errorf("result differs from expected result: %s", diff)
   301  			}
   302  
   303  			var expectReports []string
   304  			if test.expectReport {
   305  				expectReports = []string{toReconcile}
   306  			}
   307  			if !reflect.DeepEqual(expectReports, rp.reported) {
   308  				t.Errorf("mismatch report: wants %v, got %v", expectReports, rp.reported)
   309  			}
   310  
   311  			if (cs.patches != 0) != test.expectPatch {
   312  				if test.expectPatch {
   313  					t.Error("expected patch, but didn't get it")
   314  				} else {
   315  					t.Error("got unexpected patch")
   316  				}
   317  			}
   318  		})
   319  	}
   320  }
   321  
   322  type patchTrackingClient struct {
   323  	ctrlruntimeclient.Client
   324  	patches int
   325  }
   326  
   327  func (c *patchTrackingClient) Patch(ctx context.Context, obj ctrlruntimeclient.Object, patch ctrlruntimeclient.Patch, opts ...ctrlruntimeclient.PatchOption) error {
   328  	c.patches++
   329  	return c.Client.Patch(ctx, obj, patch, opts...)
   330  }