Skip to content

Commit e4fddb1

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

File tree

2 files changed

+325
-0
lines changed

2 files changed

+325
-0
lines changed

merge/multiple_appliers_test.go

Lines changed: 320 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,275 @@ func TestMultipleAppliersSet(t *testing.T) {
243244
}
244245
}
245246

247+
var structParser = 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+
`)
304+
if err != nil {
305+
panic(err)
306+
}
307+
return parser
308+
}()
309+
310+
type versionPair struct {
311+
a fieldpath.APIVersion
312+
b fieldpath.APIVersion
313+
}
314+
315+
func TestMultipleAppliersFieldUnsetting(t *testing.T) {
316+
for _, a := range []fieldpath.APIVersion{"v1", "v2"} {
317+
for _, b := range []fieldpath.APIVersion{"v1", "v2"} {
318+
t.Run(fmt.Sprintf("%s-%s", a, b), func(t *testing.T) {
319+
testMultipleAppliersFieldUnsetting(t, versionPair{a, b})
320+
})
321+
}
322+
}
323+
}
324+
325+
func testMultipleAppliersFieldUnsetting(t *testing.T, versions versionPair) {
326+
tests := map[string]TestCase{
327+
"unset_scalar_sole_owner": {
328+
Ops: []Operation{
329+
Apply{
330+
Manager: "apply-one",
331+
APIVersion: versions.a,
332+
Object: typed.YAMLObject(fmt.Sprintf(`
333+
struct:
334+
name: a
335+
scalarField_%s: a
336+
`, versions.a)),
337+
},
338+
Apply{
339+
Manager: "apply-one",
340+
APIVersion: versions.b,
341+
Object: `
342+
struct:
343+
name: a
344+
`,
345+
},
346+
},
347+
Object: `
348+
struct:
349+
name: a
350+
`,
351+
APIVersion: versions.a,
352+
Managed: fieldpath.ManagedFields{
353+
"apply-one": fieldpath.NewVersionedSet(
354+
_NS(
355+
_P("struct", "name"),
356+
),
357+
versions.b,
358+
false,
359+
),
360+
},
361+
},
362+
"unset_scalar_shared_owner": {
363+
Ops: []Operation{
364+
Apply{
365+
Manager: "apply-one",
366+
APIVersion: versions.a,
367+
Object: typed.YAMLObject(fmt.Sprintf(`
368+
struct:
369+
name: a
370+
scalarField_%s: a
371+
`, versions.a)),
372+
},
373+
Apply{
374+
Manager: "apply-two",
375+
APIVersion: versions.b,
376+
Object: typed.YAMLObject(fmt.Sprintf(`
377+
struct:
378+
scalarField_%s: a
379+
`, versions.b)),
380+
},
381+
Apply{
382+
Manager: "apply-one",
383+
APIVersion: versions.a,
384+
Object: `
385+
struct:
386+
name: a
387+
`,
388+
},
389+
},
390+
Object: typed.YAMLObject(fmt.Sprintf(`
391+
struct:
392+
name: a
393+
scalarField_%s: a
394+
`, versions.a)),
395+
APIVersion: versions.a,
396+
Managed: fieldpath.ManagedFields{
397+
"apply-one": fieldpath.NewVersionedSet(
398+
_NS(
399+
_P("struct", "name"),
400+
),
401+
versions.a,
402+
true,
403+
),
404+
"apply-two": fieldpath.NewVersionedSet(
405+
_NS(
406+
_P("struct", fmt.Sprintf("scalarField_%s", versions.b)),
407+
),
408+
versions.b,
409+
false,
410+
),
411+
},
412+
},
413+
"unset_complex_sole_owner": {
414+
Ops: []Operation{
415+
Apply{
416+
Manager: "apply-one",
417+
APIVersion: versions.a,
418+
Object: typed.YAMLObject(fmt.Sprintf(`
419+
struct:
420+
name: a
421+
complexField_%s:
422+
name: b
423+
`, versions.a)),
424+
},
425+
Apply{
426+
Manager: "apply-one",
427+
APIVersion: versions.b,
428+
Object: `
429+
struct:
430+
name: a
431+
`,
432+
},
433+
},
434+
Object: typed.YAMLObject(fmt.Sprintf(`
435+
struct:
436+
name: a
437+
complexField_%s: null
438+
`, versions.a)),
439+
APIVersion: versions.a,
440+
Managed: fieldpath.ManagedFields{
441+
"apply-one": fieldpath.NewVersionedSet(
442+
_NS(
443+
_P("struct", "name"),
444+
),
445+
versions.b,
446+
false,
447+
),
448+
},
449+
},
450+
"unset_complex_shared_owner": {
451+
Ops: []Operation{
452+
Apply{
453+
Manager: "apply-one",
454+
APIVersion: versions.a,
455+
Object: typed.YAMLObject(fmt.Sprintf(`
456+
struct:
457+
name: a
458+
complexField_%s:
459+
name: b
460+
`, versions.a)),
461+
},
462+
Apply{
463+
Manager: "apply-two",
464+
APIVersion: versions.b,
465+
Object: typed.YAMLObject(fmt.Sprintf(`
466+
struct:
467+
complexField_%s:
468+
name: b
469+
`, versions.b)),
470+
},
471+
Apply{
472+
Manager: "apply-one",
473+
APIVersion: versions.a,
474+
Object: `
475+
struct:
476+
name: a
477+
`,
478+
},
479+
},
480+
Object: typed.YAMLObject(fmt.Sprintf(`
481+
struct:
482+
name: a
483+
complexField_%s:
484+
name: b
485+
`, versions.a)),
486+
APIVersion: versions.a,
487+
Managed: fieldpath.ManagedFields{
488+
"apply-one": fieldpath.NewVersionedSet(
489+
_NS(
490+
_P("struct", "name"),
491+
),
492+
versions.a,
493+
false,
494+
),
495+
"apply-two": fieldpath.NewVersionedSet(
496+
_NS(
497+
_P("struct", fmt.Sprintf("complexField_%s", versions.b), "name"),
498+
),
499+
versions.b,
500+
false,
501+
),
502+
},
503+
},
504+
}
505+
506+
converter := renamingConverter{structParser}
507+
for name, test := range tests {
508+
t.Run(name, func(t *testing.T) {
509+
if err := test.TestWithConverter(structParser, converter); err != nil {
510+
t.Fatal(err)
511+
}
512+
})
513+
}
514+
}
515+
246516
func TestMultipleAppliersNestedType(t *testing.T) {
247517
tests := map[string]TestCase{
248518
"remove_one_keep_one_with_two_sub_items": {
@@ -1041,6 +1311,56 @@ func (r repeatingConverter) IsMissingVersionError(err error) bool {
10411311
return err == missingVersionError
10421312
}
10431313

1314+
// renamingConverter renames fields by substituting the version suffix of the field name. E.g.
1315+
// converting a map with a field named "name_v1" from v1 to v2 renames the field to "name_v2".
1316+
// Fields without a version suffix are not converted; they are the same in all versions.
1317+
// When parsing, this converter will look for the type by using the APIVersion of the
1318+
// object it's trying to parse. If trying to parse a "v1" object, a corresponding "v1" type
1319+
// should exist in the schema of the provided parser.
1320+
type renamingConverter struct {
1321+
parser Parser
1322+
}
1323+
1324+
// Convert implements merge.Converter
1325+
func (r renamingConverter) Convert(v *typed.TypedValue, version fieldpath.APIVersion) (*typed.TypedValue, error) {
1326+
if !v.AsValue().IsMap() {
1327+
return v, nil
1328+
}
1329+
inVersion := fieldpath.APIVersion(*v.TypeRef().NamedType)
1330+
outType := r.parser.Type(string(version))
1331+
return outType.FromUnstructured(renameConvert(v.AsValue(), string(inVersion), string(version)))
1332+
}
1333+
1334+
func renameConvert(v value.Value, oldSuffix, newSuffix string) interface{} {
1335+
if v.IsMap() {
1336+
out := map[string]interface{}{}
1337+
v.AsMap().Iterate(func(key string, value value.Value) bool {
1338+
if strings.HasSuffix(key, oldSuffix) {
1339+
out[strings.TrimSuffix(key, oldSuffix)+newSuffix] = renameConvert(value, oldSuffix, newSuffix)
1340+
} else {
1341+
out[key] = renameConvert(value, oldSuffix, newSuffix)
1342+
}
1343+
return true
1344+
})
1345+
return out
1346+
}
1347+
if v.IsList() {
1348+
var out []interface{}
1349+
ri := v.AsList().Range()
1350+
for ri.Next() {
1351+
_, v := ri.Item()
1352+
out = append(out, renameConvert(v, oldSuffix, newSuffix))
1353+
}
1354+
return out
1355+
}
1356+
return v.Unstructured()
1357+
}
1358+
1359+
// Convert implements merge.Converter
1360+
func (r renamingConverter) IsMissingVersionError(err error) bool {
1361+
return err == missingVersionError
1362+
}
1363+
10441364
func BenchmarkMultipleApplierRecursiveRealConversion(b *testing.B) {
10451365
test := TestCase{
10461366
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)