...

Source file src/sigs.k8s.io/cli-utils/pkg/printers/json/formatter_test.go

Documentation: sigs.k8s.io/cli-utils/pkg/printers/json

     1  // Copyright 2020 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package json
     5  
     6  import (
     7  	"encoding/json"
     8  	"errors"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  	"k8s.io/apimachinery/pkg/runtime/schema"
    16  	"k8s.io/apimachinery/pkg/util/validation/field"
    17  	"k8s.io/cli-runtime/pkg/genericclioptions"
    18  	"sigs.k8s.io/cli-utils/pkg/apply/event"
    19  	"sigs.k8s.io/cli-utils/pkg/common"
    20  	pollevent "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
    21  	"sigs.k8s.io/cli-utils/pkg/kstatus/status"
    22  	"sigs.k8s.io/cli-utils/pkg/object"
    23  	"sigs.k8s.io/cli-utils/pkg/object/graph"
    24  	"sigs.k8s.io/cli-utils/pkg/object/validation"
    25  	"sigs.k8s.io/cli-utils/pkg/print/list"
    26  	"sigs.k8s.io/cli-utils/pkg/print/stats"
    27  	"sigs.k8s.io/cli-utils/pkg/testutil"
    28  )
    29  
    30  func TestFormatter_FormatApplyEvent(t *testing.T) {
    31  	testCases := map[string]struct {
    32  		previewStrategy common.DryRunStrategy
    33  		event           event.ApplyEvent
    34  		expected        []map[string]interface{}
    35  	}{
    36  		"resource created without dryrun": {
    37  			previewStrategy: common.DryRunNone,
    38  			event: event.ApplyEvent{
    39  				Status:     event.ApplySuccessful,
    40  				Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
    41  			},
    42  			expected: []map[string]interface{}{
    43  				{
    44  					"group":     "apps",
    45  					"kind":      "Deployment",
    46  					"name":      "my-dep",
    47  					"namespace": "default",
    48  					"status":    "Successful",
    49  					"timestamp": "",
    50  					"type":      "apply",
    51  				},
    52  			},
    53  		},
    54  		"resource updated with client dryrun": {
    55  			previewStrategy: common.DryRunClient,
    56  			event: event.ApplyEvent{
    57  				Status:     event.ApplySuccessful,
    58  				Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
    59  			},
    60  			expected: []map[string]interface{}{
    61  				{
    62  					"group":     "apps",
    63  					"kind":      "Deployment",
    64  					"name":      "my-dep",
    65  					"namespace": "",
    66  					"status":    "Successful",
    67  					"timestamp": "",
    68  					"type":      "apply",
    69  				},
    70  			},
    71  		},
    72  		"resource updated with server dryrun": {
    73  			previewStrategy: common.DryRunServer,
    74  			event: event.ApplyEvent{
    75  				Status:     event.ApplySuccessful,
    76  				Identifier: createIdentifier("batch", "CronJob", "foo", "my-cron"),
    77  			},
    78  			expected: []map[string]interface{}{
    79  				{
    80  					"group":     "batch",
    81  					"kind":      "CronJob",
    82  					"name":      "my-cron",
    83  					"namespace": "foo",
    84  					"status":    "Successful",
    85  					"timestamp": "",
    86  					"type":      "apply",
    87  				},
    88  			},
    89  		},
    90  		"resource apply failed": {
    91  			previewStrategy: common.DryRunNone,
    92  			event: event.ApplyEvent{
    93  				Status:     event.ApplyFailed,
    94  				Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
    95  				Error:      errors.New("example error"),
    96  			},
    97  			expected: []map[string]interface{}{
    98  				{
    99  					"group":     "apps",
   100  					"kind":      "Deployment",
   101  					"name":      "my-dep",
   102  					"namespace": "",
   103  					"status":    "Failed",
   104  					"timestamp": "",
   105  					"type":      "apply",
   106  					"error":     "example error",
   107  				},
   108  			},
   109  		},
   110  		"resource apply skip error": {
   111  			previewStrategy: common.DryRunNone,
   112  			event: event.ApplyEvent{
   113  				Status:     event.ApplySkipped,
   114  				Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
   115  				Error:      errors.New("example error"),
   116  			},
   117  			expected: []map[string]interface{}{
   118  				{
   119  					"group":     "apps",
   120  					"kind":      "Deployment",
   121  					"name":      "my-dep",
   122  					"namespace": "",
   123  					"status":    "Skipped",
   124  					"timestamp": "",
   125  					"type":      "apply",
   126  					"error":     "example error",
   127  				},
   128  			},
   129  		},
   130  	}
   131  
   132  	for tn, tc := range testCases {
   133  		t.Run(tn, func(t *testing.T) {
   134  			ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
   135  			formatter := NewFormatter(ioStreams, tc.previewStrategy)
   136  			err := formatter.FormatApplyEvent(tc.event)
   137  			assert.NoError(t, err)
   138  
   139  			objects := strings.Split(strings.TrimSpace(out.String()), "\n")
   140  
   141  			if !assert.Equal(t, len(tc.expected), len(objects)) {
   142  				t.FailNow()
   143  			}
   144  			for i := range tc.expected {
   145  				assertOutput(t, tc.expected[i], objects[i])
   146  			}
   147  		})
   148  	}
   149  }
   150  
   151  func TestFormatter_FormatStatusEvent(t *testing.T) {
   152  	testCases := map[string]struct {
   153  		previewStrategy common.DryRunStrategy
   154  		event           event.StatusEvent
   155  		expected        map[string]interface{}
   156  	}{
   157  		"resource update with Current status": {
   158  			previewStrategy: common.DryRunNone,
   159  			event: event.StatusEvent{
   160  				Identifier: object.ObjMetadata{
   161  					GroupKind: schema.GroupKind{
   162  						Group: "apps",
   163  						Kind:  "Deployment",
   164  					},
   165  					Namespace: "foo",
   166  					Name:      "bar",
   167  				},
   168  				PollResourceInfo: &pollevent.ResourceStatus{
   169  					Identifier: object.ObjMetadata{
   170  						GroupKind: schema.GroupKind{
   171  							Group: "apps",
   172  							Kind:  "Deployment",
   173  						},
   174  						Namespace: "foo",
   175  						Name:      "bar",
   176  					},
   177  					Status:  status.CurrentStatus,
   178  					Message: "Resource is Current",
   179  				},
   180  			},
   181  			expected: map[string]interface{}{
   182  				"group":     "apps",
   183  				"kind":      "Deployment",
   184  				"message":   "Resource is Current",
   185  				"name":      "bar",
   186  				"namespace": "foo",
   187  				"status":    "Current",
   188  				"timestamp": "",
   189  				"type":      "status",
   190  			},
   191  		},
   192  	}
   193  
   194  	for tn, tc := range testCases {
   195  		t.Run(tn, func(t *testing.T) {
   196  			ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
   197  			formatter := NewFormatter(ioStreams, tc.previewStrategy)
   198  			err := formatter.FormatStatusEvent(tc.event)
   199  			assert.NoError(t, err)
   200  
   201  			assertOutput(t, tc.expected, out.String())
   202  		})
   203  	}
   204  }
   205  
   206  func TestFormatter_FormatPruneEvent(t *testing.T) {
   207  	testCases := map[string]struct {
   208  		previewStrategy common.DryRunStrategy
   209  		event           event.PruneEvent
   210  		expected        map[string]interface{}
   211  	}{
   212  		"resource pruned without dryrun": {
   213  			previewStrategy: common.DryRunNone,
   214  			event: event.PruneEvent{
   215  				Status:     event.PruneSuccessful,
   216  				Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
   217  			},
   218  			expected: map[string]interface{}{
   219  				"group":     "apps",
   220  				"kind":      "Deployment",
   221  				"name":      "my-dep",
   222  				"namespace": "default",
   223  				"status":    "Successful",
   224  				"timestamp": "",
   225  				"type":      "prune",
   226  			},
   227  		},
   228  		"resource skipped with client dryrun": {
   229  			previewStrategy: common.DryRunClient,
   230  			event: event.PruneEvent{
   231  				Status:     event.PruneSkipped,
   232  				Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
   233  			},
   234  			expected: map[string]interface{}{
   235  				"group":     "apps",
   236  				"kind":      "Deployment",
   237  				"name":      "my-dep",
   238  				"namespace": "",
   239  				"status":    "Skipped",
   240  				"timestamp": "",
   241  				"type":      "prune",
   242  			},
   243  		},
   244  		"resource prune failed": {
   245  			previewStrategy: common.DryRunNone,
   246  			event: event.PruneEvent{
   247  				Status:     event.PruneFailed,
   248  				Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
   249  				Error:      errors.New("example error"),
   250  			},
   251  			expected: map[string]interface{}{
   252  				"group":     "apps",
   253  				"kind":      "Deployment",
   254  				"name":      "my-dep",
   255  				"namespace": "",
   256  				"status":    "Failed",
   257  				"timestamp": "",
   258  				"type":      "prune",
   259  				"error":     "example error",
   260  			},
   261  		},
   262  		"resource prune skip error": {
   263  			previewStrategy: common.DryRunNone,
   264  			event: event.PruneEvent{
   265  				Status:     event.PruneSkipped,
   266  				Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
   267  				Error:      errors.New("example error"),
   268  			},
   269  			expected: map[string]interface{}{
   270  				"group":     "apps",
   271  				"kind":      "Deployment",
   272  				"name":      "my-dep",
   273  				"namespace": "",
   274  				"status":    "Skipped",
   275  				"timestamp": "",
   276  				"type":      "prune",
   277  				"error":     "example error",
   278  			},
   279  		},
   280  	}
   281  
   282  	for tn, tc := range testCases {
   283  		t.Run(tn, func(t *testing.T) {
   284  			ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
   285  			formatter := NewFormatter(ioStreams, tc.previewStrategy)
   286  			err := formatter.FormatPruneEvent(tc.event)
   287  			assert.NoError(t, err)
   288  
   289  			assertOutput(t, tc.expected, out.String())
   290  		})
   291  	}
   292  }
   293  
   294  func TestFormatter_FormatDeleteEvent(t *testing.T) {
   295  	testCases := map[string]struct {
   296  		previewStrategy common.DryRunStrategy
   297  		event           event.DeleteEvent
   298  		statusCollector list.Collector
   299  		expected        map[string]interface{}
   300  	}{
   301  		"resource deleted without no dryrun": {
   302  			previewStrategy: common.DryRunNone,
   303  			event: event.DeleteEvent{
   304  				Status:     event.DeleteSuccessful,
   305  				Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
   306  			},
   307  			expected: map[string]interface{}{
   308  				"group":     "apps",
   309  				"kind":      "Deployment",
   310  				"name":      "my-dep",
   311  				"namespace": "default",
   312  				"status":    "Successful",
   313  				"timestamp": "",
   314  				"type":      "delete",
   315  			},
   316  		},
   317  		"resource skipped with client dryrun": {
   318  			previewStrategy: common.DryRunClient,
   319  			event: event.DeleteEvent{
   320  				Status:     event.DeleteSkipped,
   321  				Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
   322  			},
   323  			expected: map[string]interface{}{
   324  				"group":     "apps",
   325  				"kind":      "Deployment",
   326  				"name":      "my-dep",
   327  				"namespace": "",
   328  				"status":    "Skipped",
   329  				"timestamp": "",
   330  				"type":      "delete",
   331  			},
   332  		},
   333  		"resource delete failed": {
   334  			previewStrategy: common.DryRunNone,
   335  			event: event.DeleteEvent{
   336  				Status:     event.DeleteFailed,
   337  				Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
   338  				Error:      errors.New("example error"),
   339  			},
   340  			expected: map[string]interface{}{
   341  				"group":     "apps",
   342  				"kind":      "Deployment",
   343  				"name":      "my-dep",
   344  				"namespace": "default",
   345  				"status":    "Failed",
   346  				"timestamp": "",
   347  				"type":      "delete",
   348  				"error":     "example error",
   349  			},
   350  		},
   351  		"resource delete skip error": {
   352  			previewStrategy: common.DryRunNone,
   353  			event: event.DeleteEvent{
   354  				Status:     event.DeleteSkipped,
   355  				Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
   356  				Error:      errors.New("example error"),
   357  			},
   358  			expected: map[string]interface{}{
   359  				"group":     "apps",
   360  				"kind":      "Deployment",
   361  				"name":      "my-dep",
   362  				"namespace": "default",
   363  				"status":    "Skipped",
   364  				"timestamp": "",
   365  				"type":      "delete",
   366  				"error":     "example error",
   367  			},
   368  		},
   369  	}
   370  
   371  	for tn, tc := range testCases {
   372  		t.Run(tn, func(t *testing.T) {
   373  			ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
   374  			formatter := NewFormatter(ioStreams, tc.previewStrategy)
   375  			err := formatter.FormatDeleteEvent(tc.event)
   376  			assert.NoError(t, err)
   377  
   378  			assertOutput(t, tc.expected, out.String())
   379  		})
   380  	}
   381  }
   382  
   383  func TestFormatter_FormatWaitEvent(t *testing.T) {
   384  	testCases := map[string]struct {
   385  		previewStrategy common.DryRunStrategy
   386  		event           event.WaitEvent
   387  		statusCollector list.Collector
   388  		expected        map[string]interface{}
   389  	}{
   390  		"resource reconciled": {
   391  			previewStrategy: common.DryRunNone,
   392  			event: event.WaitEvent{
   393  				GroupName:  "wait-1",
   394  				Status:     event.ReconcileSuccessful,
   395  				Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
   396  			},
   397  			expected: map[string]interface{}{
   398  				"group":     "apps",
   399  				"kind":      "Deployment",
   400  				"name":      "my-dep",
   401  				"namespace": "default",
   402  				"status":    "Successful",
   403  				"timestamp": "",
   404  				"type":      "wait",
   405  			},
   406  		},
   407  		"resource reconciled (client-side dry-run)": {
   408  			previewStrategy: common.DryRunClient,
   409  			event: event.WaitEvent{
   410  				GroupName:  "wait-1",
   411  				Status:     event.ReconcileSuccessful,
   412  				Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
   413  			},
   414  			expected: map[string]interface{}{
   415  				"group":     "apps",
   416  				"kind":      "Deployment",
   417  				"name":      "my-dep",
   418  				"namespace": "default",
   419  				"status":    "Successful",
   420  				"timestamp": "",
   421  				"type":      "wait",
   422  			},
   423  		},
   424  		"resource reconciled (server-side dry-run)": {
   425  			previewStrategy: common.DryRunServer,
   426  			event: event.WaitEvent{
   427  				GroupName:  "wait-1",
   428  				Status:     event.ReconcileSuccessful,
   429  				Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
   430  			},
   431  			expected: map[string]interface{}{
   432  				"group":     "apps",
   433  				"kind":      "Deployment",
   434  				"name":      "my-dep",
   435  				"namespace": "default",
   436  				"status":    "Successful",
   437  				"timestamp": "",
   438  				"type":      "wait",
   439  			},
   440  		},
   441  		"resource reconcile pending": {
   442  			previewStrategy: common.DryRunServer,
   443  			event: event.WaitEvent{
   444  				GroupName:  "wait-1",
   445  				Status:     event.ReconcilePending,
   446  				Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
   447  			},
   448  			expected: map[string]interface{}{
   449  				"group":     "apps",
   450  				"kind":      "Deployment",
   451  				"name":      "my-dep",
   452  				"namespace": "default",
   453  				"status":    "Pending",
   454  				"timestamp": "",
   455  				"type":      "wait",
   456  			},
   457  		},
   458  		"resource reconcile skipped": {
   459  			previewStrategy: common.DryRunServer,
   460  			event: event.WaitEvent{
   461  				GroupName:  "wait-1",
   462  				Status:     event.ReconcileSkipped,
   463  				Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
   464  			},
   465  			expected: map[string]interface{}{
   466  				"group":     "apps",
   467  				"kind":      "Deployment",
   468  				"name":      "my-dep",
   469  				"namespace": "default",
   470  				"status":    "Skipped",
   471  				"timestamp": "",
   472  				"type":      "wait",
   473  			},
   474  		},
   475  		"resource reconcile timeout": {
   476  			previewStrategy: common.DryRunServer,
   477  			event: event.WaitEvent{
   478  				GroupName:  "wait-1",
   479  				Status:     event.ReconcileTimeout,
   480  				Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
   481  			},
   482  			expected: map[string]interface{}{
   483  				"group":     "apps",
   484  				"kind":      "Deployment",
   485  				"name":      "my-dep",
   486  				"namespace": "default",
   487  				"status":    "Timeout",
   488  				"timestamp": "",
   489  				"type":      "wait",
   490  			},
   491  		},
   492  		"resource reconcile failed": {
   493  			previewStrategy: common.DryRunNone,
   494  			event: event.WaitEvent{
   495  				GroupName:  "wait-1",
   496  				Status:     event.ReconcileFailed,
   497  				Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
   498  			},
   499  			expected: map[string]interface{}{
   500  				"group":     "apps",
   501  				"kind":      "Deployment",
   502  				"name":      "my-dep",
   503  				"namespace": "default",
   504  				"status":    "Failed",
   505  				"timestamp": "",
   506  				"type":      "wait",
   507  			},
   508  		},
   509  	}
   510  
   511  	for tn, tc := range testCases {
   512  		t.Run(tn, func(t *testing.T) {
   513  			ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
   514  			formatter := NewFormatter(ioStreams, tc.previewStrategy)
   515  			err := formatter.FormatWaitEvent(tc.event)
   516  			assert.NoError(t, err)
   517  
   518  			assertOutput(t, tc.expected, out.String())
   519  		})
   520  	}
   521  }
   522  
   523  func TestFormatter_FormatActionGroupEvent(t *testing.T) {
   524  	testCases := map[string]struct {
   525  		previewStrategy common.DryRunStrategy
   526  		event           event.ActionGroupEvent
   527  		actionGroups    []event.ActionGroup
   528  		statsCollector  stats.Stats
   529  		statusCollector list.Collector
   530  		expected        map[string]interface{}
   531  	}{
   532  		"not the last apply action group finished": {
   533  			previewStrategy: common.DryRunNone,
   534  			event: event.ActionGroupEvent{
   535  				GroupName: "age-1",
   536  				Action:    event.ApplyAction,
   537  				Status:    event.Finished,
   538  			},
   539  			actionGroups: []event.ActionGroup{
   540  				{
   541  					Name:   "age-1",
   542  					Action: event.ApplyAction,
   543  				},
   544  				{
   545  					Name:   "age-2",
   546  					Action: event.ApplyAction,
   547  				},
   548  			},
   549  			statsCollector: stats.Stats{
   550  				ApplyStats: stats.ApplyStats{},
   551  			},
   552  			expected: map[string]interface{}{
   553  				"action":     "Apply",
   554  				"count":      0,
   555  				"failed":     0,
   556  				"skipped":    0,
   557  				"status":     "Finished",
   558  				"successful": 0,
   559  				"timestamp":  "2022-03-24T01:35:04Z",
   560  				"type":       "group",
   561  			},
   562  		},
   563  		"the last apply action group finished": {
   564  			previewStrategy: common.DryRunNone,
   565  			event: event.ActionGroupEvent{
   566  				GroupName: "age-2",
   567  				Action:    event.ApplyAction,
   568  				Status:    event.Finished,
   569  			},
   570  			actionGroups: []event.ActionGroup{
   571  				{
   572  					Name:   "age-1",
   573  					Action: event.ApplyAction,
   574  				},
   575  				{
   576  					Name:   "age-2",
   577  					Action: event.ApplyAction,
   578  				},
   579  			},
   580  			statsCollector: stats.Stats{
   581  				ApplyStats: stats.ApplyStats{
   582  					Successful: 42,
   583  				},
   584  			},
   585  			expected: map[string]interface{}{
   586  				"action":     "Apply",
   587  				"count":      42,
   588  				"failed":     0,
   589  				"skipped":    0,
   590  				"status":     "Finished",
   591  				"successful": 42,
   592  				"timestamp":  "2022-03-24T01:35:04Z",
   593  				"type":       "group",
   594  			},
   595  		},
   596  		"last prune action group started": {
   597  			previewStrategy: common.DryRunNone,
   598  			event: event.ActionGroupEvent{
   599  				GroupName: "age-2",
   600  				Action:    event.PruneAction,
   601  				Status:    event.Started,
   602  			},
   603  			actionGroups: []event.ActionGroup{
   604  				{
   605  					Name:   "age-1",
   606  					Action: event.PruneAction,
   607  				},
   608  				{
   609  					Name:   "age-2",
   610  					Action: event.PruneAction,
   611  				},
   612  			},
   613  			expected: map[string]interface{}{
   614  				"action":    "Prune",
   615  				"status":    "Started",
   616  				"timestamp": "2022-03-24T01:51:36Z",
   617  				"type":      "group",
   618  			},
   619  		},
   620  	}
   621  
   622  	for tn, tc := range testCases {
   623  		t.Run(tn, func(t *testing.T) {
   624  			ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
   625  			formatter := NewFormatter(ioStreams, tc.previewStrategy)
   626  			err := formatter.FormatActionGroupEvent(tc.event, tc.actionGroups, tc.statsCollector, tc.statusCollector)
   627  			assert.NoError(t, err)
   628  
   629  			assertOutput(t, tc.expected, out.String())
   630  		})
   631  	}
   632  }
   633  
   634  func TestFormatter_FormatValidationEvent(t *testing.T) {
   635  	testCases := map[string]struct {
   636  		previewStrategy common.DryRunStrategy
   637  		event           event.ValidationEvent
   638  		expected        map[string]interface{}
   639  		expectedError   error
   640  	}{
   641  		"zero objects, return error": {
   642  			previewStrategy: common.DryRunNone,
   643  			event: event.ValidationEvent{
   644  				Identifiers: object.ObjMetadataSet{},
   645  				Error:       errors.New("unexpected"),
   646  			},
   647  			expectedError: errors.New("invalid validation event: no identifiers: unexpected"),
   648  		},
   649  		"one object, missing namespace": {
   650  			previewStrategy: common.DryRunNone,
   651  			event: event.ValidationEvent{
   652  				Identifiers: object.ObjMetadataSet{
   653  					{
   654  						GroupKind: schema.GroupKind{
   655  							Group: "apps",
   656  							Kind:  "Deployment",
   657  						},
   658  						Namespace: "foo",
   659  						Name:      "bar",
   660  					},
   661  				},
   662  				Error: validation.NewError(
   663  					field.Required(field.NewPath("metadata", "namespace"), "namespace is required"),
   664  					object.ObjMetadata{
   665  						GroupKind: schema.GroupKind{
   666  							Group: "apps",
   667  							Kind:  "Deployment",
   668  						},
   669  						Namespace: "foo",
   670  						Name:      "bar",
   671  					},
   672  				),
   673  			},
   674  			expected: map[string]interface{}{
   675  				"type":      "validation",
   676  				"timestamp": "",
   677  				"objects": []interface{}{
   678  					map[string]interface{}{
   679  						"group":     "apps",
   680  						"kind":      "Deployment",
   681  						"name":      "bar",
   682  						"namespace": "foo",
   683  					},
   684  				},
   685  				"error": "metadata.namespace: Required value: namespace is required",
   686  			},
   687  		},
   688  		"two objects, cyclic dependency": {
   689  			previewStrategy: common.DryRunNone,
   690  			event: event.ValidationEvent{
   691  				Identifiers: object.ObjMetadataSet{
   692  					{
   693  						GroupKind: schema.GroupKind{
   694  							Group: "apps",
   695  							Kind:  "Deployment",
   696  						},
   697  						Namespace: "default",
   698  						Name:      "bar",
   699  					},
   700  					{
   701  						GroupKind: schema.GroupKind{
   702  							Group: "apps",
   703  							Kind:  "Deployment",
   704  						},
   705  						Namespace: "default",
   706  						Name:      "foo",
   707  					},
   708  				},
   709  				Error: validation.NewError(
   710  					graph.CyclicDependencyError{
   711  						Edges: []graph.Edge{
   712  							{
   713  								From: object.ObjMetadata{
   714  									GroupKind: schema.GroupKind{
   715  										Group: "apps",
   716  										Kind:  "Deployment",
   717  									},
   718  									Namespace: "default",
   719  									Name:      "bar",
   720  								},
   721  								To: object.ObjMetadata{
   722  									GroupKind: schema.GroupKind{
   723  										Group: "apps",
   724  										Kind:  "Deployment",
   725  									},
   726  									Namespace: "default",
   727  									Name:      "foo",
   728  								},
   729  							},
   730  							{
   731  								From: object.ObjMetadata{
   732  									GroupKind: schema.GroupKind{
   733  										Group: "apps",
   734  										Kind:  "Deployment",
   735  									},
   736  									Namespace: "default",
   737  									Name:      "foo",
   738  								},
   739  								To: object.ObjMetadata{
   740  									GroupKind: schema.GroupKind{
   741  										Group: "apps",
   742  										Kind:  "Deployment",
   743  									},
   744  									Namespace: "default",
   745  									Name:      "bar",
   746  								},
   747  							},
   748  						},
   749  					},
   750  					object.ObjMetadata{
   751  						GroupKind: schema.GroupKind{
   752  							Group: "apps",
   753  							Kind:  "Deployment",
   754  						},
   755  						Namespace: "default",
   756  						Name:      "bar",
   757  					},
   758  					object.ObjMetadata{
   759  						GroupKind: schema.GroupKind{
   760  							Group: "apps",
   761  							Kind:  "Deployment",
   762  						},
   763  						Namespace: "default",
   764  						Name:      "foo",
   765  					},
   766  				),
   767  			},
   768  			expected: map[string]interface{}{
   769  				"type":      "validation",
   770  				"timestamp": "",
   771  				"objects": []interface{}{
   772  					map[string]interface{}{
   773  						"group":     "apps",
   774  						"kind":      "Deployment",
   775  						"name":      "bar",
   776  						"namespace": "default",
   777  					},
   778  					map[string]interface{}{
   779  						"group":     "apps",
   780  						"kind":      "Deployment",
   781  						"name":      "foo",
   782  						"namespace": "default",
   783  					},
   784  				},
   785  				"error": `cyclic dependency:
   786  - apps/namespaces/default/Deployment/bar -> apps/namespaces/default/Deployment/foo
   787  - apps/namespaces/default/Deployment/foo -> apps/namespaces/default/Deployment/bar`,
   788  			},
   789  		},
   790  	}
   791  
   792  	for tn, tc := range testCases {
   793  		t.Run(tn, func(t *testing.T) {
   794  			ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
   795  			formatter := NewFormatter(ioStreams, tc.previewStrategy)
   796  			err := formatter.FormatValidationEvent(tc.event)
   797  			if tc.expectedError != nil {
   798  				assert.EqualError(t, err, tc.expectedError.Error())
   799  				return
   800  			}
   801  			assert.NoError(t, err)
   802  			assertOutput(t, tc.expected, out.String())
   803  		})
   804  	}
   805  }
   806  
   807  func TestFormatter_FormatSummary(t *testing.T) {
   808  	now := time.Now()
   809  	nowStr := now.UTC().Format(time.RFC3339)
   810  
   811  	testCases := map[string]struct {
   812  		statsCollector stats.Stats
   813  		expected       []map[string]interface{}
   814  	}{
   815  		"apply prune wait": {
   816  			statsCollector: stats.Stats{
   817  				ApplyStats: stats.ApplyStats{
   818  					Successful: 1,
   819  					Skipped:    2,
   820  					Failed:     3,
   821  				},
   822  				PruneStats: stats.PruneStats{
   823  					Successful: 3,
   824  					Skipped:    2,
   825  					Failed:     1,
   826  				},
   827  				WaitStats: stats.WaitStats{
   828  					Successful: 4,
   829  					Skipped:    6,
   830  					Failed:     1,
   831  					Timeout:    1,
   832  				},
   833  			},
   834  			expected: []map[string]interface{}{
   835  				{
   836  					"action":     "Apply",
   837  					"count":      float64(6),
   838  					"successful": float64(1),
   839  					"skipped":    float64(2),
   840  					"failed":     float64(3),
   841  					"timestamp":  nowStr,
   842  					"type":       "summary",
   843  				},
   844  				{
   845  					"action":     "Prune",
   846  					"count":      float64(6),
   847  					"successful": float64(3),
   848  					"skipped":    float64(2),
   849  					"failed":     float64(1),
   850  					"timestamp":  nowStr,
   851  					"type":       "summary",
   852  				},
   853  				{
   854  					"action":     "Wait",
   855  					"count":      float64(12),
   856  					"successful": float64(4),
   857  					"skipped":    float64(6),
   858  					"failed":     float64(1),
   859  					"timeout":    float64(1),
   860  					"timestamp":  nowStr,
   861  					"type":       "summary",
   862  				},
   863  			},
   864  		},
   865  	}
   866  
   867  	for tn, tc := range testCases {
   868  		t.Run(tn, func(t *testing.T) {
   869  			ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
   870  			jf := &formatter{
   871  				ioStreams: ioStreams,
   872  				// fake time func
   873  				now: func() time.Time { return now },
   874  			}
   875  			err := jf.FormatSummary(tc.statsCollector)
   876  			assert.NoError(t, err)
   877  
   878  			assertOutputLines(t, tc.expected, out.String())
   879  		})
   880  	}
   881  }
   882  
   883  func assertOutputLines(t *testing.T, expectedMaps []map[string]interface{}, actual string) {
   884  	actual = strings.TrimRight(actual, "\n")
   885  	lines := strings.Split(actual, "\n")
   886  	actualMaps := make([]map[string]interface{}, len(lines))
   887  	for i, line := range lines {
   888  		err := json.Unmarshal([]byte(line), &actualMaps[i])
   889  		require.NoError(t, err)
   890  	}
   891  	testutil.AssertEqual(t, expectedMaps, actualMaps)
   892  }
   893  
   894  // nolint:unparam
   895  func assertOutput(t *testing.T, expectedMap map[string]interface{}, actual string) bool {
   896  	if len(expectedMap) == 0 {
   897  		return assert.Empty(t, actual)
   898  	}
   899  
   900  	var m map[string]interface{}
   901  	err := json.Unmarshal([]byte(actual), &m)
   902  	if !assert.NoError(t, err) {
   903  		return false
   904  	}
   905  
   906  	if _, found := expectedMap["timestamp"]; found {
   907  		if _, ok := m["timestamp"]; ok {
   908  			delete(expectedMap, "timestamp")
   909  			delete(m, "timestamp")
   910  		} else {
   911  			t.Error("expected to find key 'timestamp', but didn't")
   912  			return false
   913  		}
   914  	}
   915  
   916  	for key, val := range m {
   917  		if floatVal, ok := val.(float64); ok {
   918  			m[key] = int(floatVal)
   919  		}
   920  	}
   921  
   922  	return assert.Equal(t, expectedMap, m)
   923  }
   924  
   925  func createIdentifier(group, kind, namespace, name string) object.ObjMetadata {
   926  	return object.ObjMetadata{
   927  		Namespace: namespace,
   928  		Name:      name,
   929  		GroupKind: schema.GroupKind{
   930  			Group: group,
   931  			Kind:  kind,
   932  		},
   933  	}
   934  }
   935  

View as plain text