1/*
2Copyright 2018 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package typed_test
18
19import (
20	"fmt"
21	"testing"
22
23	"sigs.k8s.io/structured-merge-diff/v3/fieldpath"
24	"sigs.k8s.io/structured-merge-diff/v3/typed"
25)
26
27type symdiffTestCase struct {
28	name         string
29	rootTypeName string
30	schema       typed.YAMLObject
31	quints       []symdiffQuint
32}
33
34type symdiffQuint struct {
35	lhs typed.YAMLObject
36	rhs typed.YAMLObject
37
38	// Please note that everything is tested both ways--removed and added
39	// are symmetric. So if a test case is covered for one of them, it
40	// covers both.
41	removed  *fieldpath.Set
42	modified *fieldpath.Set
43	added    *fieldpath.Set
44}
45
46var symdiffCases = []symdiffTestCase{{
47	name:         "simple pair",
48	rootTypeName: "stringPair",
49	schema: `types:
50- name: stringPair
51  map:
52    fields:
53    - name: key
54      type:
55        scalar: string
56    - name: value
57      type:
58        namedType: __untyped_atomic_
59- name: __untyped_atomic_
60  scalar: untyped
61  list:
62    elementType:
63      namedType: __untyped_atomic_
64    elementRelationship: atomic
65  map:
66    elementType:
67      namedType: __untyped_atomic_
68    elementRelationship: atomic
69`,
70	quints: []symdiffQuint{{
71		lhs:      `{"key":"foo","value":1}`,
72		rhs:      `{"key":"foo","value":1}`,
73		removed:  _NS(),
74		modified: _NS(),
75		added:    _NS(),
76	}, {
77		lhs:      `{"key":"foo","value":{}}`,
78		rhs:      `{"key":"foo","value":1}`,
79		removed:  _NS(),
80		modified: _NS(_P("value")),
81		added:    _NS(),
82	}, {
83		lhs:      `{"key":"foo","value":1}`,
84		rhs:      `{"key":"foo","value":{}}`,
85		removed:  _NS(),
86		modified: _NS(_P("value")),
87		added:    _NS(),
88	}, {
89		lhs:      `{"key":"foo","value":1}`,
90		rhs:      `{"key":"foo","value":{"doesn't matter":"what's here","or":{"how":"nested"}}}`,
91		removed:  _NS(),
92		modified: _NS(_P("value")),
93		added:    _NS(),
94	}, {
95		lhs:      `{"key":"foo","value":null}`,
96		rhs:      `{"key":"foo","value":{}}`,
97		removed:  _NS(),
98		modified: _NS(_P("value")),
99		added:    _NS(),
100	}, {
101		lhs:      `{"key":"foo"}`,
102		rhs:      `{"value":true}`,
103		removed:  _NS(_P("key")),
104		modified: _NS(),
105		added:    _NS(_P("value")),
106	}, {
107		lhs:      `{"key":"foot"}`,
108		rhs:      `{"key":"foo","value":true}`,
109		removed:  _NS(),
110		modified: _NS(_P("key")),
111		added:    _NS(_P("value")),
112	}},
113}, {
114	name:         "null/empty map",
115	rootTypeName: "nestedMap",
116	schema: `types:
117- name: nestedMap
118  map:
119    fields:
120    - name: inner
121      type:
122        map:
123          elementType:
124            namedType: __untyped_atomic_
125- name: __untyped_atomic_
126  scalar: untyped
127  list:
128    elementType:
129      namedType: __untyped_atomic_
130    elementRelationship: atomic
131  map:
132    elementType:
133      namedType: __untyped_atomic_
134    elementRelationship: atomic
135`,
136	quints: []symdiffQuint{{
137		lhs:      `{}`,
138		rhs:      `{"inner":{}}`,
139		removed:  _NS(),
140		modified: _NS(),
141		added:    _NS(_P("inner")),
142	}, {
143		lhs:      `{}`,
144		rhs:      `{"inner":null}`,
145		removed:  _NS(),
146		modified: _NS(),
147		added:    _NS(_P("inner")),
148	}, {
149		lhs:      `{"inner":null}`,
150		rhs:      `{"inner":{}}`,
151		removed:  _NS(),
152		modified: _NS(_P("inner")),
153		added:    _NS(),
154	}, {
155		lhs:      `{"inner":{}}`,
156		rhs:      `{"inner":null}`,
157		removed:  _NS(),
158		modified: _NS(_P("inner")),
159		added:    _NS(),
160	}, {
161		lhs:      `{"inner":{}}`,
162		rhs:      `{"inner":{}}`,
163		removed:  _NS(),
164		modified: _NS(),
165		added:    _NS(),
166	}},
167}, {
168	name:         "null/empty struct",
169	rootTypeName: "nestedStruct",
170	schema: `types:
171- name: nestedStruct
172  map:
173    fields:
174    - name: inner
175      type:
176        map:
177          fields:
178          - name: value
179            type:
180              namedType: __untyped_atomic_
181`,
182	quints: []symdiffQuint{{
183		lhs:      `{}`,
184		rhs:      `{"inner":{}}`,
185		removed:  _NS(),
186		modified: _NS(),
187		added:    _NS(_P("inner")),
188	}, {
189		lhs:      `{}`,
190		rhs:      `{"inner":null}`,
191		removed:  _NS(),
192		modified: _NS(),
193		added:    _NS(_P("inner")),
194	}, {
195		lhs:      `{"inner":null}`,
196		rhs:      `{"inner":{}}`,
197		removed:  _NS(),
198		modified: _NS(_P("inner")),
199		added:    _NS(),
200	}, {
201		lhs:      `{"inner":{}}`,
202		rhs:      `{"inner":null}`,
203		removed:  _NS(),
204		modified: _NS(_P("inner")),
205		added:    _NS(),
206	}, {
207		lhs:      `{"inner":{}}`,
208		rhs:      `{"inner":{}}`,
209		removed:  _NS(),
210		modified: _NS(),
211		added:    _NS(),
212	}},
213}, {
214	name:         "null/empty list",
215	rootTypeName: "nestedList",
216	schema: `types:
217- name: nestedList
218  map:
219    fields:
220    - name: inner
221      type:
222        list:
223          elementType:
224            namedType: __untyped_atomic_
225          elementRelationship: atomic
226- name: __untyped_atomic_
227  scalar: untyped
228  list:
229    elementType:
230      namedType: __untyped_atomic_
231    elementRelationship: atomic
232  map:
233    elementType:
234      namedType: __untyped_atomic_
235    elementRelationship: atomic
236`,
237	quints: []symdiffQuint{{
238		lhs:      `{}`,
239		rhs:      `{"inner":[]}`,
240		removed:  _NS(),
241		modified: _NS(),
242		added:    _NS(_P("inner")),
243	}, {
244		lhs:      `{}`,
245		rhs:      `{"inner":null}`,
246		removed:  _NS(),
247		modified: _NS(),
248		added:    _NS(_P("inner")),
249	}, {
250		lhs:      `{"inner":null}`,
251		rhs:      `{"inner":[]}`,
252		removed:  _NS(),
253		modified: _NS(_P("inner")),
254		added:    _NS(),
255	}, {
256		lhs:      `{"inner":[]}`,
257		rhs:      `{"inner":null}`,
258		removed:  _NS(),
259		modified: _NS(_P("inner")),
260		added:    _NS(),
261	}, {
262		lhs:      `{"inner":[]}`,
263		rhs:      `{"inner":[]}`,
264		removed:  _NS(),
265		modified: _NS(),
266		added:    _NS(),
267	}},
268}, {
269	name:         "map merge",
270	rootTypeName: "nestedMap",
271	schema: `types:
272- name: nestedMap
273  map:
274    elementType:
275      namedType: nestedMap
276`,
277	quints: []symdiffQuint{{
278		lhs:      `{"a":{},"b":{}}`,
279		rhs:      `{"a":{},"b":{}}`,
280		removed:  _NS(),
281		modified: _NS(),
282		added:    _NS(),
283	}, {
284		lhs:      `{"a":{}}`,
285		rhs:      `{"b":{}}`,
286		removed:  _NS(_P("a")),
287		modified: _NS(),
288		added:    _NS(_P("b")),
289	}, {
290		lhs:      `{"a":{"b":{"c":{}}}}`,
291		rhs:      `{"a":{"b":{}}}`,
292		removed:  _NS(_P("a", "b", "c")),
293		modified: _NS(),
294		added:    _NS(),
295	}, {
296		lhs:      `{"a":{}}`,
297		rhs:      `{"a":{"b":{}}}`,
298		removed:  _NS(),
299		modified: _NS(),
300		added:    _NS(_P("a", "b")),
301	}},
302}, {
303	name:         "untyped deduced",
304	rootTypeName: "__untyped_deduced_",
305	schema: `types:
306- name: __untyped_atomic_
307  scalar: untyped
308  list:
309    elementType:
310      namedType: __untyped_atomic_
311    elementRelationship: atomic
312  map:
313    elementType:
314      namedType: __untyped_atomic_
315    elementRelationship: atomic
316- name: __untyped_deduced_
317  scalar: untyped
318  list:
319    elementType:
320      namedType: __untyped_atomic_
321    elementRelationship: atomic
322  map:
323    elementType:
324      namedType: __untyped_deduced_
325    elementRelationship: separable
326`,
327	quints: []symdiffQuint{{
328		lhs:      `{"a":{}}}`,
329		rhs:      `{"a":{"b":{}}}`,
330		removed:  _NS(),
331		modified: _NS(),
332		added:    _NS(_P("a", "b")),
333	}, {
334		lhs:      `{"a":null}`,
335		rhs:      `{"a":{"b":{}}}`,
336		removed:  _NS(),
337		modified: _NS(),
338		added:    _NS(_P("a", "b")),
339	}, {
340		lhs:      `{"a":{"b":{}}}`,
341		rhs:      `{"a":{}}}`,
342		removed:  _NS(_P("a", "b")),
343		modified: _NS(),
344		added:    _NS(),
345	}, {
346		lhs:      `{"a":{"b":{}}}`,
347		rhs:      `{"a":null}`,
348		removed:  _NS(_P("a", "b")),
349		modified: _NS(),
350		added:    _NS(),
351	}, {
352		lhs:      `{"a":[]}`,
353		rhs:      `{"a":["b"]}`,
354		removed:  _NS(),
355		modified: _NS(_P("a")),
356		added:    _NS(),
357	}, {
358		lhs:      `{"a":null}`,
359		rhs:      `{"a":["b"]}`,
360		removed:  _NS(),
361		modified: _NS(_P("a")),
362		added:    _NS(),
363	}, {
364		lhs:      `{"a":["b"]}`,
365		rhs:      `{"a":[]}`,
366		removed:  _NS(),
367		modified: _NS(_P("a")),
368		added:    _NS(),
369	}, {
370		lhs:      `{"a":["b"]}`,
371		rhs:      `{"a":null}`,
372		removed:  _NS(),
373		modified: _NS(_P("a")),
374		added:    _NS(),
375	}, {
376		lhs:      `{"a":null}`,
377		rhs:      `{"a":"b"}`,
378		removed:  _NS(),
379		modified: _NS(_P("a")),
380		added:    _NS(),
381	}, {
382		lhs:      `{"a":"b"}`,
383		rhs:      `{"a":null}`,
384		removed:  _NS(),
385		modified: _NS(_P("a")),
386		added:    _NS(),
387	}, {
388		lhs:      `{"a":{"b":{}}}`,
389		rhs:      `{"a":["b"]}}`,
390		removed:  _NS(_P("a", "b")),
391		modified: _NS(_P("a")),
392		added:    _NS(),
393	}, {
394		lhs:      `{"a":["b"]}}`,
395		rhs:      `{"a":{"b":{}}}`,
396		removed:  _NS(),
397		modified: _NS(_P("a")),
398		added:    _NS(_P("a", "b")),
399	}, {
400		lhs:      `{"a":{"b":{}}}`,
401		rhs:      `{"a":"b"}`,
402		removed:  _NS(_P("a", "b")),
403		modified: _NS(_P("a")),
404		added:    _NS(),
405	}, {
406		lhs:      `{"a":"b"}`,
407		rhs:      `{"a":{"b":{}}}`,
408		removed:  _NS(),
409		modified: _NS(_P("a")),
410		added:    _NS(_P("a", "b")),
411	}, {
412		lhs:      `{"a":["b"]}}`,
413		rhs:      `{"a":"b"}`,
414		removed:  _NS(),
415		modified: _NS(_P("a")),
416		added:    _NS(),
417	}, {
418		lhs:      `{"a":"b"}`,
419		rhs:      `{"a":["b"]}}`,
420		removed:  _NS(),
421		modified: _NS(_P("a")),
422		added:    _NS(),
423	}},
424}, {
425	name:         "untyped separable",
426	rootTypeName: "__untyped_separable_",
427	schema: `types:
428- name: __untyped_separable_
429  scalar: untyped
430  list:
431    elementType:
432      namedType: __untyped_separable_
433    elementRelationship: associative
434  map:
435    elementType:
436      namedType: __untyped_separable_
437    elementRelationship: separable
438`,
439	quints: []symdiffQuint{{
440		lhs:      `{"a":{}}}`,
441		rhs:      `{"a":{"b":{}}}`,
442		removed:  _NS(),
443		modified: _NS(),
444		added:    _NS(_P("a", "b")),
445	}, {
446		lhs:      `{"a":null}`,
447		rhs:      `{"a":{"b":{}}}`,
448		removed:  _NS(),
449		modified: _NS(),
450		added:    _NS(_P("a", "b")),
451	}, {
452		lhs:      `{"a":{"b":{}}}`,
453		rhs:      `{"a":{}}}`,
454		removed:  _NS(_P("a", "b")),
455		modified: _NS(),
456		added:    _NS(),
457	}, {
458		lhs:      `{"a":{"b":{}}}`,
459		rhs:      `{"a":null}`,
460		removed:  _NS(_P("a", "b")),
461		modified: _NS(),
462		added:    _NS(),
463	}, {
464		lhs:      `{"a":[]}`,
465		rhs:      `{"a":["b"]}`,
466		removed:  _NS(),
467		modified: _NS(),
468		added:    _NS(_P("a", _V("b"))),
469	}, {
470		lhs:     `{"a":null}`,
471		rhs:     `{"a":["b"]}`,
472		removed: _NS(),
473		// TODO: result should be the same as the previous case
474		// nothing shoule be modified here.
475		modified: _NS(_P("a")),
476		added:    _NS(_P("a", _V("b"))),
477	}, {
478		lhs:      `{"a":["b"]}`,
479		rhs:      `{"a":[]}`,
480		removed:  _NS(_P("a", _V("b"))),
481		modified: _NS(),
482		added:    _NS(),
483	}, {
484		lhs:     `{"a":["b"]}`,
485		rhs:     `{"a":null}`,
486		removed: _NS(_P("a", _V("b"))),
487		// TODO: result should be the same as the previous case
488		// nothing shoule be modified here.
489		modified: _NS(_P("a")),
490		added:    _NS(),
491	}, {
492		lhs:      `{"a":null}`,
493		rhs:      `{"a":"b"}`,
494		removed:  _NS(),
495		modified: _NS(_P("a")),
496		added:    _NS(),
497	}, {
498		lhs:      `{"a":"b"}`,
499		rhs:      `{"a":null}`,
500		removed:  _NS(),
501		modified: _NS(_P("a")),
502		added:    _NS(),
503	}, {
504		lhs:      `{"a":{"b":{}}}`,
505		rhs:      `{"a":["b"]}}`,
506		removed:  _NS(_P("a", "b")),
507		modified: _NS(),
508		added:    _NS(_P("a", _V("b"))),
509	}, {
510		lhs:      `{"a":["b"]}}`,
511		rhs:      `{"a":{"b":{}}}`,
512		removed:  _NS(_P("a", _V("b"))),
513		modified: _NS(),
514		added:    _NS(_P("a", "b")),
515	}, {
516		lhs:      `{"a":{"b":{}}}`,
517		rhs:      `{"a":"b"}`,
518		removed:  _NS(_P("a", "b")),
519		modified: _NS(_P("a")),
520		added:    _NS(),
521	}, {
522		lhs:      `{"a":"b"}`,
523		rhs:      `{"a":{"b":{}}}`,
524		removed:  _NS(),
525		modified: _NS(_P("a")),
526		added:    _NS(_P("a", "b")),
527	}, {
528		lhs:      `{"a":["b"]}}`,
529		rhs:      `{"a":"b"}`,
530		removed:  _NS(_P("a", _V("b"))),
531		modified: _NS(_P("a")),
532		added:    _NS(),
533	}, {
534		lhs:      `{"a":"b"}`,
535		rhs:      `{"a":["b"]}}`,
536		removed:  _NS(),
537		modified: _NS(_P("a")),
538		added:    _NS(_P("a", _V("b"))),
539	}},
540}, {
541	name:         "struct grab bag",
542	rootTypeName: "myStruct",
543	schema: `types:
544- name: myStruct
545  map:
546    fields:
547    - name: numeric
548      type:
549        scalar: numeric
550    - name: string
551      type:
552        scalar: string
553    - name: bool
554      type:
555        scalar: boolean
556    - name: setStr
557      type:
558        list:
559          elementType:
560            scalar: string
561          elementRelationship: associative
562    - name: setBool
563      type:
564        list:
565          elementType:
566            scalar: boolean
567          elementRelationship: associative
568    - name: setNumeric
569      type:
570        list:
571          elementType:
572            scalar: numeric
573          elementRelationship: associative
574`,
575	quints: []symdiffQuint{{
576		lhs:      `{"numeric":1}`,
577		rhs:      `{"numeric":3.14159}`,
578		removed:  _NS(),
579		modified: _NS(_P("numeric")),
580		added:    _NS(),
581	}, {
582		lhs:      `{"numeric":3.14159}`,
583		rhs:      `{"numeric":1}`,
584		removed:  _NS(),
585		modified: _NS(_P("numeric")),
586		added:    _NS(),
587	}, {
588		lhs:      `{"string":"aoeu"}`,
589		rhs:      `{"bool":true}`,
590		removed:  _NS(_P("string")),
591		modified: _NS(),
592		added:    _NS(_P("bool")),
593	}, {
594		lhs:      `{"setStr":["a","b"]}`,
595		rhs:      `{"setStr":["a","b","c"]}`,
596		removed:  _NS(),
597		modified: _NS(),
598		added:    _NS(_P("setStr", _V("c"))),
599	}, {
600		lhs: `{"setStr":["a","b","c"]}`,
601		rhs: `{"setStr":[]}`,
602		removed: _NS(
603			_P("setStr", _V("a")),
604			_P("setStr", _V("b")),
605			_P("setStr", _V("c")),
606		),
607		modified: _NS(),
608		added:    _NS(),
609	}, {
610		lhs:      `{"setBool":[true]}`,
611		rhs:      `{"setBool":[false]}`,
612		removed:  _NS(_P("setBool", _V(true))),
613		modified: _NS(),
614		added:    _NS(_P("setBool", _V(false))),
615	}, {
616		lhs:      `{"setNumeric":[1,2,3.14159]}`,
617		rhs:      `{"setNumeric":[1,2,3]}`,
618		removed:  _NS(_P("setNumeric", _V(3.14159))),
619		modified: _NS(),
620		added:    _NS(_P("setNumeric", _V(3))),
621	}},
622}, {
623	name:         "associative list",
624	rootTypeName: "myRoot",
625	schema: `types:
626- name: myRoot
627  map:
628    fields:
629    - name: list
630      type:
631        namedType: myList
632    - name: atomicList
633      type:
634        namedType: mySequence
635- name: myList
636  list:
637    elementType:
638      namedType: myElement
639    elementRelationship: associative
640    keys:
641    - key
642    - id
643- name: mySequence
644  list:
645    elementType:
646      scalar: string
647    elementRelationship: atomic
648- name: myElement
649  map:
650    fields:
651    - name: key
652      type:
653        scalar: string
654    - name: id
655      type:
656        scalar: numeric
657    - name: value
658      type:
659        namedType: myValue
660    - name: bv
661      type:
662        scalar: boolean
663    - name: nv
664      type:
665        scalar: numeric
666- name: myValue
667  map:
668    elementType:
669      scalar: string
670`,
671	quints: []symdiffQuint{{
672		lhs:      `{}`,
673		rhs:      `{"list":[{"key":"a","id":1,"value":{"a":"a"}}]}`,
674		removed:  _NS(),
675		modified: _NS(),
676		added: _NS(
677			_P("list"),
678			_P("list", _KBF("key", "a", "id", 1)),
679			_P("list", _KBF("key", "a", "id", 1), "key"),
680			_P("list", _KBF("key", "a", "id", 1), "id"),
681			_P("list", _KBF("key", "a", "id", 1), "value"),
682			_P("list", _KBF("key", "a", "id", 1), "value", "a"),
683		),
684	}, {
685		lhs:      `{"list":[{"key":"a","id":1,"value":{"a":"a"}}]}`,
686		rhs:      `{"list":[{"key":"a","id":1,"value":{"a":"a"}}]}`,
687		removed:  _NS(),
688		modified: _NS(),
689		added:    _NS(),
690	}, {
691		lhs:      `{"list":[{"key":"a","id":1,"value":{"a":"a"}}]}`,
692		rhs:      `{"list":[{"key":"a","id":1,"value":{"a":"b"}}]}`,
693		removed:  _NS(),
694		modified: _NS(_P("list", _KBF("key", "a", "id", 1), "value", "a")),
695		added:    _NS(),
696	}, {
697		lhs: `{"list":[{"key":"a","id":1,"value":{"a":"a"}}]}`,
698		rhs: `{"list":[{"key":"a","id":2,"value":{"a":"a"}}]}`,
699		removed: _NS(
700			_P("list", _KBF("key", "a", "id", 1)),
701			_P("list", _KBF("key", "a", "id", 1), "key"),
702			_P("list", _KBF("key", "a", "id", 1), "id"),
703			_P("list", _KBF("key", "a", "id", 1), "value"),
704			_P("list", _KBF("key", "a", "id", 1), "value", "a"),
705		),
706		modified: _NS(),
707		added: _NS(
708			_P("list", _KBF("key", "a", "id", 2)),
709			_P("list", _KBF("key", "a", "id", 2), "key"),
710			_P("list", _KBF("key", "a", "id", 2), "id"),
711			_P("list", _KBF("key", "a", "id", 2), "value"),
712			_P("list", _KBF("key", "a", "id", 2), "value", "a"),
713		),
714	}, {
715		lhs: `{"list":[{"key":"a","id":1},{"key":"b","id":1}]}`,
716		rhs: `{"list":[{"key":"a","id":1},{"key":"a","id":2}]}`,
717		removed: _NS(
718			_P("list", _KBF("key", "b", "id", 1)),
719			_P("list", _KBF("key", "b", "id", 1), "key"),
720			_P("list", _KBF("key", "b", "id", 1), "id"),
721		),
722		modified: _NS(),
723		added: _NS(
724			_P("list", _KBF("key", "a", "id", 2)),
725			_P("list", _KBF("key", "a", "id", 2), "key"),
726			_P("list", _KBF("key", "a", "id", 2), "id"),
727		),
728	}, {
729		lhs:      `{"atomicList":["a","a","a"]}`,
730		rhs:      `{"atomicList":null}`,
731		removed:  _NS(),
732		modified: _NS(_P("atomicList")),
733		added:    _NS(),
734	}, {
735		lhs:      `{"atomicList":["a","b","c"]}`,
736		rhs:      `{"atomicList":[]}`,
737		removed:  _NS(),
738		modified: _NS(_P("atomicList")),
739		added:    _NS(),
740	}, {
741		lhs:      `{"atomicList":["a","a","a"]}`,
742		rhs:      `{"atomicList":["a","a"]}`,
743		removed:  _NS(),
744		modified: _NS(_P("atomicList")),
745		added:    _NS(),
746	}},
747}}
748
749func (tt symdiffTestCase) test(t *testing.T) {
750	parser, err := typed.NewParser(tt.schema)
751	if err != nil {
752		t.Fatalf("failed to create schema: %v", err)
753	}
754	for i, quint := range tt.quints {
755		quint := quint
756		t.Run(fmt.Sprintf("%v-valid-%v", tt.name, i), func(t *testing.T) {
757			t.Parallel()
758			pt := parser.Type(tt.rootTypeName)
759
760			tvLHS, err := pt.FromYAML(quint.lhs)
761			if err != nil {
762				t.Fatalf("failed to parse lhs: %v", err)
763			}
764			tvRHS, err := pt.FromYAML(quint.rhs)
765			if err != nil {
766				t.Fatalf("failed to parse rhs: %v", err)
767			}
768			got, err := tvLHS.Compare(tvRHS)
769			if err != nil {
770				t.Fatalf("got validation errors: %v", err)
771			}
772			t.Logf("got added:\n%s\n", got.Added)
773			if !got.Added.Equals(quint.added) {
774				t.Errorf("Expected added:\n%s\n", quint.added)
775			}
776			t.Logf("got modified:\n%s", got.Modified)
777			if !got.Modified.Equals(quint.modified) {
778				t.Errorf("Expected modified:\n%s\n", quint.modified)
779			}
780			t.Logf("got removed:\n%s", got.Removed)
781			if !got.Removed.Equals(quint.removed) {
782				t.Errorf("Expected removed:\n%s\n", quint.removed)
783			}
784
785			// Do the reverse operation and sanity check.
786			gotR, err := tvRHS.Compare(tvLHS)
787			if err != nil {
788				t.Fatalf("(reverse) got validation errors: %v", err)
789			}
790			if !gotR.Modified.Equals(got.Modified) {
791				t.Errorf("reverse operation gave different modified list:\n%s", gotR.Modified)
792			}
793			if !gotR.Removed.Equals(got.Added) {
794				t.Errorf("reverse removed gave different result than added:\n%s", gotR.Removed)
795			}
796			if !gotR.Added.Equals(got.Removed) {
797				t.Errorf("reverse added gave different result than removed:\n%s", gotR.Added)
798			}
799
800		})
801	}
802}
803
804func TestSymdiff(t *testing.T) {
805	for _, tt := range symdiffCases {
806		tt := tt
807		t.Run(tt.name, func(t *testing.T) {
808			t.Parallel()
809			tt.test(t)
810		})
811	}
812}
813