1
2
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()
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()
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()
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()
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()
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()
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()
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()
870 jf := &formatter{
871 ioStreams: ioStreams,
872
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
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