Skip to content

Commit 1ce4a77

Browse files
committed
Add field unsetting tests
1 parent ca99bda commit 1ce4a77

File tree

2 files changed

+347
-0
lines changed

2 files changed

+347
-0
lines changed

merge/multiple_appliers_test.go

Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
. "sigs.k8s.io/structured-merge-diff/v3/internal/fixture"
2828
"sigs.k8s.io/structured-merge-diff/v3/merge"
2929
"sigs.k8s.io/structured-merge-diff/v3/typed"
30+
"sigs.k8s.io/structured-merge-diff/v3/value"
3031
)
3132

3233
func TestMultipleAppliersSet(t *testing.T) {
@@ -243,6 +244,300 @@ func TestMultipleAppliersSet(t *testing.T) {
243244
}
244245
}
245246

247+
var structMultiversionParser = func() Parser {
248+
parser, err := typed.NewParser(`types:
249+
- name: v1
250+
map:
251+
fields:
252+
- name: struct
253+
type:
254+
namedType: struct
255+
- name: version
256+
type:
257+
scalar: string
258+
- name: struct
259+
map:
260+
fields:
261+
- name: name
262+
type:
263+
scalar: string
264+
- name: scalarField_v1
265+
type:
266+
scalar: string
267+
- name: complexField_v1
268+
type:
269+
namedType: complex
270+
- name: complex
271+
map:
272+
fields:
273+
- name: name
274+
type:
275+
scalar: string
276+
- name: v2
277+
map:
278+
fields:
279+
- name: struct
280+
type:
281+
namedType: struct_v2
282+
- name: version
283+
type:
284+
scalar: string
285+
- name: struct_v2
286+
map:
287+
fields:
288+
- name: name
289+
type:
290+
scalar: string
291+
- name: scalarField_v2
292+
type:
293+
scalar: string
294+
- name: complexField_v2
295+
type:
296+
namedType: complex_v2
297+
- name: complex_v2
298+
map:
299+
fields:
300+
- name: name
301+
type:
302+
scalar: string
303+
- name: v3
304+
map:
305+
fields:
306+
- name: struct
307+
type:
308+
namedType: struct_v3
309+
- name: version
310+
type:
311+
scalar: string
312+
- name: struct_v3
313+
map:
314+
fields:
315+
- name: name
316+
type:
317+
scalar: string
318+
- name: scalarField_v3
319+
type:
320+
scalar: string
321+
- name: complexField_v3
322+
type:
323+
namedType: complex_v3
324+
- name: complex_v3
325+
map:
326+
fields:
327+
- name: name
328+
type:
329+
scalar: string
330+
`)
331+
if err != nil {
332+
panic(err)
333+
}
334+
return parser
335+
}()
336+
337+
func TestMultipleAppliersFieldUnsetting(t *testing.T) {
338+
versions := []fieldpath.APIVersion{"v1", "v2", "v3"}
339+
for _, v1 := range versions {
340+
for _, v2 := range versions {
341+
for _, v3 := range versions {
342+
t.Run(fmt.Sprintf("%s-%s-%s", v1, v2, v3), func(t *testing.T) {
343+
testMultipleAppliersFieldUnsetting(t, v1, v2, v3)
344+
})
345+
}
346+
}
347+
}
348+
}
349+
350+
func testMultipleAppliersFieldUnsetting(t *testing.T, v1, v2, v3 fieldpath.APIVersion) {
351+
tests := map[string]TestCase{
352+
"unset_scalar_sole_owner": {
353+
Ops: []Operation{
354+
Apply{
355+
Manager: "apply-one",
356+
APIVersion: v1,
357+
Object: typed.YAMLObject(fmt.Sprintf(`
358+
struct:
359+
name: a
360+
scalarField_%s: a
361+
`, v1)),
362+
},
363+
Apply{
364+
Manager: "apply-one",
365+
APIVersion: v2,
366+
Object: `
367+
struct:
368+
name: a
369+
`,
370+
},
371+
},
372+
Object: `
373+
struct:
374+
name: a
375+
`,
376+
APIVersion: v3,
377+
Managed: fieldpath.ManagedFields{
378+
"apply-one": fieldpath.NewVersionedSet(
379+
_NS(
380+
_P("struct", "name"),
381+
),
382+
v2,
383+
false,
384+
),
385+
},
386+
},
387+
"unset_scalar_shared_owner": {
388+
Ops: []Operation{
389+
Apply{
390+
Manager: "apply-one",
391+
APIVersion: v1,
392+
Object: typed.YAMLObject(fmt.Sprintf(`
393+
struct:
394+
name: a
395+
scalarField_%s: a
396+
`, v1)),
397+
},
398+
Apply{
399+
Manager: "apply-two",
400+
APIVersion: v2,
401+
Object: typed.YAMLObject(fmt.Sprintf(`
402+
struct:
403+
scalarField_%s: a
404+
`, v2)),
405+
},
406+
Apply{
407+
Manager: "apply-one",
408+
APIVersion: v3,
409+
Object: `
410+
struct:
411+
name: a
412+
`,
413+
},
414+
},
415+
Object: typed.YAMLObject(fmt.Sprintf(`
416+
struct:
417+
name: a
418+
scalarField_%s: a
419+
`, v3)),
420+
APIVersion: v3,
421+
Managed: fieldpath.ManagedFields{
422+
"apply-one": fieldpath.NewVersionedSet(
423+
_NS(
424+
_P("struct", "name"),
425+
),
426+
v3,
427+
true,
428+
),
429+
"apply-two": fieldpath.NewVersionedSet(
430+
_NS(
431+
_P("struct", fmt.Sprintf("scalarField_%s", v2)),
432+
),
433+
v2,
434+
false,
435+
),
436+
},
437+
},
438+
"unset_complex_sole_owner": {
439+
Ops: []Operation{
440+
Apply{
441+
Manager: "apply-one",
442+
APIVersion: v1,
443+
Object: typed.YAMLObject(fmt.Sprintf(`
444+
struct:
445+
name: a
446+
complexField_%s:
447+
name: b
448+
`, v1)),
449+
},
450+
Apply{
451+
Manager: "apply-one",
452+
APIVersion: v2,
453+
Object: `
454+
struct:
455+
name: a
456+
`,
457+
},
458+
},
459+
Object: typed.YAMLObject(fmt.Sprintf(`
460+
struct:
461+
name: a
462+
complexField_%s: null
463+
`, v3)),
464+
APIVersion: v3,
465+
Managed: fieldpath.ManagedFields{
466+
"apply-one": fieldpath.NewVersionedSet(
467+
_NS(
468+
_P("struct", "name"),
469+
),
470+
v2,
471+
false,
472+
),
473+
},
474+
},
475+
"unset_complex_shared_owner": {
476+
Ops: []Operation{
477+
Apply{
478+
Manager: "apply-one",
479+
APIVersion: v1,
480+
Object: typed.YAMLObject(fmt.Sprintf(`
481+
struct:
482+
name: a
483+
complexField_%s:
484+
name: b
485+
`, v1)),
486+
},
487+
Apply{
488+
Manager: "apply-two",
489+
APIVersion: v2,
490+
Object: typed.YAMLObject(fmt.Sprintf(`
491+
struct:
492+
complexField_%s:
493+
name: b
494+
`, v2)),
495+
},
496+
Apply{
497+
Manager: "apply-one",
498+
APIVersion: v3,
499+
Object: `
500+
struct:
501+
name: a
502+
`,
503+
},
504+
},
505+
Object: typed.YAMLObject(fmt.Sprintf(`
506+
struct:
507+
name: a
508+
complexField_%s:
509+
name: b
510+
`, v3)),
511+
APIVersion: v3,
512+
Managed: fieldpath.ManagedFields{
513+
"apply-one": fieldpath.NewVersionedSet(
514+
_NS(
515+
_P("struct", "name"),
516+
),
517+
v3,
518+
false,
519+
),
520+
"apply-two": fieldpath.NewVersionedSet(
521+
_NS(
522+
_P("struct", fmt.Sprintf("complexField_%s", v2), "name"),
523+
),
524+
v2,
525+
false,
526+
),
527+
},
528+
},
529+
}
530+
531+
converter := renamingConverter{structMultiversionParser}
532+
for name, test := range tests {
533+
t.Run(name, func(t *testing.T) {
534+
if err := test.TestWithConverter(structMultiversionParser, converter); err != nil {
535+
t.Fatal(err)
536+
}
537+
})
538+
}
539+
}
540+
246541
func TestMultipleAppliersNestedType(t *testing.T) {
247542
tests := map[string]TestCase{
248543
"remove_one_keep_one_with_two_sub_items": {
@@ -1041,6 +1336,53 @@ func (r repeatingConverter) IsMissingVersionError(err error) bool {
10411336
return err == missingVersionError
10421337
}
10431338

1339+
// renamingConverter renames fields by substituting the version suffix of the field name. E.g.
1340+
// converting a map with a field named "name_v1" from v1 to v2 renames the field to "name_v2".
1341+
// Fields without a version suffix are not converted; they are the same in all versions.
1342+
// When parsing, this converter will look for the type by using the APIVersion of the
1343+
// object it's trying to parse. If trying to parse a "v1" object, a corresponding "v1" type
1344+
// should exist in the schema of the provided parser.
1345+
type renamingConverter struct {
1346+
parser Parser
1347+
}
1348+
1349+
// Convert implements merge.Converter
1350+
func (r renamingConverter) Convert(v *typed.TypedValue, version fieldpath.APIVersion) (*typed.TypedValue, error) {
1351+
inVersion := fieldpath.APIVersion(*v.TypeRef().NamedType)
1352+
outType := r.parser.Type(string(version))
1353+
return outType.FromUnstructured(renameFields(v.AsValue(), string(inVersion), string(version)))
1354+
}
1355+
1356+
func renameFields(v value.Value, oldSuffix, newSuffix string) interface{} {
1357+
if v.IsMap() {
1358+
out := map[string]interface{}{}
1359+
v.AsMap().Iterate(func(key string, value value.Value) bool {
1360+
if strings.HasSuffix(key, oldSuffix) {
1361+
out[strings.TrimSuffix(key, oldSuffix)+newSuffix] = renameFields(value, oldSuffix, newSuffix)
1362+
} else {
1363+
out[key] = renameFields(value, oldSuffix, newSuffix)
1364+
}
1365+
return true
1366+
})
1367+
return out
1368+
}
1369+
if v.IsList() {
1370+
var out []interface{}
1371+
ri := v.AsList().Range()
1372+
for ri.Next() {
1373+
_, v := ri.Item()
1374+
out = append(out, renameFields(v, oldSuffix, newSuffix))
1375+
}
1376+
return out
1377+
}
1378+
return v.Unstructured()
1379+
}
1380+
1381+
// Convert implements merge.Converter
1382+
func (r renamingConverter) IsMissingVersionError(err error) bool {
1383+
return err == missingVersionError
1384+
}
1385+
10441386
func BenchmarkMultipleApplierRecursiveRealConversion(b *testing.B) {
10451387
test := TestCase{
10461388
Ops: []Operation{

typed/typed.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ type TypedValue struct {
6161
schema *schema.Schema
6262
}
6363

64+
// TypeRef is the type of the value.
65+
func (tv TypedValue) TypeRef() schema.TypeRef {
66+
return tv.typeRef
67+
}
68+
6469
// AsValue removes the type from the TypedValue and only keeps the value.
6570
func (tv TypedValue) AsValue() value.Value {
6671
return tv.value

0 commit comments

Comments
 (0)