mirror of https://github.com/databricks/cli.git
520 lines
15 KiB
Go
520 lines
15 KiB
Go
package merge
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/databricks/cli/libs/dyn"
|
|
assert "github.com/databricks/cli/libs/dyn/dynassert"
|
|
)
|
|
|
|
type overrideTestCase struct {
|
|
name string
|
|
left dyn.Value
|
|
right dyn.Value
|
|
state visitorState
|
|
expected dyn.Value
|
|
}
|
|
|
|
func TestOverride_Primitive(t *testing.T) {
|
|
leftLocation := dyn.Location{File: "left.yml", Line: 1, Column: 1}
|
|
rightLocation := dyn.Location{File: "right.yml", Line: 1, Column: 1}
|
|
|
|
modifiedTestCases := []overrideTestCase{
|
|
{
|
|
name: "string (updated)",
|
|
state: visitorState{updated: []string{"root"}},
|
|
left: dyn.NewValue("a", []dyn.Location{leftLocation}),
|
|
right: dyn.NewValue("b", []dyn.Location{rightLocation}),
|
|
expected: dyn.NewValue("b", []dyn.Location{rightLocation}),
|
|
},
|
|
{
|
|
name: "string (not updated)",
|
|
state: visitorState{},
|
|
left: dyn.NewValue("a", []dyn.Location{leftLocation}),
|
|
right: dyn.NewValue("a", []dyn.Location{rightLocation}),
|
|
expected: dyn.NewValue("a", []dyn.Location{leftLocation}),
|
|
},
|
|
{
|
|
name: "bool (updated)",
|
|
state: visitorState{updated: []string{"root"}},
|
|
left: dyn.NewValue(true, []dyn.Location{leftLocation}),
|
|
right: dyn.NewValue(false, []dyn.Location{rightLocation}),
|
|
expected: dyn.NewValue(false, []dyn.Location{rightLocation}),
|
|
},
|
|
{
|
|
name: "bool (not updated)",
|
|
state: visitorState{},
|
|
left: dyn.NewValue(true, []dyn.Location{leftLocation}),
|
|
right: dyn.NewValue(true, []dyn.Location{rightLocation}),
|
|
expected: dyn.NewValue(true, []dyn.Location{leftLocation}),
|
|
},
|
|
{
|
|
name: "int (updated)",
|
|
state: visitorState{updated: []string{"root"}},
|
|
left: dyn.NewValue(1, []dyn.Location{leftLocation}),
|
|
right: dyn.NewValue(2, []dyn.Location{rightLocation}),
|
|
expected: dyn.NewValue(2, []dyn.Location{rightLocation}),
|
|
},
|
|
{
|
|
name: "int (not updated)",
|
|
state: visitorState{},
|
|
left: dyn.NewValue(int32(1), []dyn.Location{leftLocation}),
|
|
right: dyn.NewValue(int64(1), []dyn.Location{rightLocation}),
|
|
expected: dyn.NewValue(int32(1), []dyn.Location{leftLocation}),
|
|
},
|
|
{
|
|
name: "float (updated)",
|
|
state: visitorState{updated: []string{"root"}},
|
|
left: dyn.NewValue(1.0, []dyn.Location{leftLocation}),
|
|
right: dyn.NewValue(2.0, []dyn.Location{rightLocation}),
|
|
expected: dyn.NewValue(2.0, []dyn.Location{rightLocation}),
|
|
},
|
|
{
|
|
name: "float (not updated)",
|
|
state: visitorState{},
|
|
left: dyn.NewValue(float32(1.0), []dyn.Location{leftLocation}),
|
|
right: dyn.NewValue(float64(1.0), []dyn.Location{rightLocation}),
|
|
expected: dyn.NewValue(float32(1.0), []dyn.Location{leftLocation}),
|
|
},
|
|
{
|
|
name: "time (updated)",
|
|
state: visitorState{updated: []string{"root"}},
|
|
left: dyn.NewValue(dyn.FromTime(time.UnixMilli(10000)), []dyn.Location{leftLocation}),
|
|
right: dyn.NewValue(dyn.FromTime(time.UnixMilli(10001)), []dyn.Location{rightLocation}),
|
|
expected: dyn.NewValue(dyn.FromTime(time.UnixMilli(10001)), []dyn.Location{rightLocation}),
|
|
},
|
|
{
|
|
name: "time (not updated)",
|
|
state: visitorState{},
|
|
left: dyn.NewValue(dyn.FromTime(time.UnixMilli(10000)), []dyn.Location{leftLocation}),
|
|
right: dyn.NewValue(dyn.FromTime(time.UnixMilli(10000)), []dyn.Location{rightLocation}),
|
|
expected: dyn.NewValue(dyn.FromTime(time.UnixMilli(10000)), []dyn.Location{leftLocation}),
|
|
},
|
|
{
|
|
name: "different types (updated)",
|
|
state: visitorState{updated: []string{"root"}},
|
|
left: dyn.NewValue("a", []dyn.Location{leftLocation}),
|
|
right: dyn.NewValue(42, []dyn.Location{rightLocation}),
|
|
expected: dyn.NewValue(42, []dyn.Location{rightLocation}),
|
|
},
|
|
{
|
|
name: "map - remove 'a', update 'b'",
|
|
state: visitorState{
|
|
removed: []string{"root.a"},
|
|
updated: []string{"root.b"},
|
|
},
|
|
left: dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"a": dyn.NewValue(42, []dyn.Location{leftLocation}),
|
|
"b": dyn.NewValue(10, []dyn.Location{leftLocation}),
|
|
},
|
|
[]dyn.Location{leftLocation}),
|
|
|
|
right: dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"b": dyn.NewValue(20, []dyn.Location{rightLocation}),
|
|
},
|
|
[]dyn.Location{rightLocation}),
|
|
|
|
expected: dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"b": dyn.NewValue(20, []dyn.Location{rightLocation}),
|
|
},
|
|
[]dyn.Location{leftLocation}),
|
|
},
|
|
{
|
|
name: "map - add 'a'",
|
|
state: visitorState{
|
|
added: []string{"root.a"},
|
|
},
|
|
left: dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"b": dyn.NewValue(10, []dyn.Location{leftLocation}),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
|
|
right: dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"a": dyn.NewValue(42, []dyn.Location{rightLocation}),
|
|
"b": dyn.NewValue(10, []dyn.Location{rightLocation}),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
|
|
expected: dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"a": dyn.NewValue(42, []dyn.Location{rightLocation}),
|
|
// location hasn't changed because value hasn't changed
|
|
"b": dyn.NewValue(10, []dyn.Location{leftLocation}),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
},
|
|
{
|
|
name: "map - remove 'a'",
|
|
state: visitorState{
|
|
removed: []string{"root.a"},
|
|
},
|
|
left: dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"a": dyn.NewValue(42, []dyn.Location{leftLocation}),
|
|
"b": dyn.NewValue(10, []dyn.Location{leftLocation}),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
|
|
right: dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"b": dyn.NewValue(10, []dyn.Location{rightLocation}),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
|
|
expected: dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
// location hasn't changed because value hasn't changed
|
|
"b": dyn.NewValue(10, []dyn.Location{leftLocation}),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
},
|
|
{
|
|
name: "map - add 'jobs.job_1'",
|
|
state: visitorState{
|
|
added: []string{"root.jobs.job_1"},
|
|
},
|
|
left: dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"jobs": dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"job_0": dyn.NewValue(42, []dyn.Location{leftLocation}),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
|
|
right: dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"jobs": dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"job_0": dyn.NewValue(42, []dyn.Location{rightLocation}),
|
|
"job_1": dyn.NewValue(1337, []dyn.Location{rightLocation}),
|
|
},
|
|
[]dyn.Location{rightLocation},
|
|
),
|
|
},
|
|
[]dyn.Location{rightLocation},
|
|
),
|
|
|
|
expected: dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"jobs": dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"job_0": dyn.NewValue(42, []dyn.Location{leftLocation}),
|
|
"job_1": dyn.NewValue(1337, []dyn.Location{rightLocation}),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
},
|
|
{
|
|
name: "map - remove nested key",
|
|
state: visitorState{removed: []string{"root.jobs.job_1"}},
|
|
left: dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"jobs": dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"job_0": dyn.NewValue(42, []dyn.Location{leftLocation}),
|
|
"job_1": dyn.NewValue(1337, []dyn.Location{rightLocation}),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
right: dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"jobs": dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"job_0": dyn.NewValue(42, []dyn.Location{rightLocation}),
|
|
},
|
|
[]dyn.Location{rightLocation},
|
|
),
|
|
},
|
|
[]dyn.Location{rightLocation},
|
|
),
|
|
expected: dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"jobs": dyn.NewValue(
|
|
map[string]dyn.Value{
|
|
"job_0": dyn.NewValue(42, []dyn.Location{leftLocation}),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
},
|
|
{
|
|
name: "sequence - add",
|
|
state: visitorState{added: []string{"root[1]"}},
|
|
left: dyn.NewValue(
|
|
[]dyn.Value{
|
|
dyn.NewValue(42, []dyn.Location{leftLocation}),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
right: dyn.NewValue(
|
|
[]dyn.Value{
|
|
dyn.NewValue(42, []dyn.Location{rightLocation}),
|
|
dyn.NewValue(10, []dyn.Location{rightLocation}),
|
|
},
|
|
[]dyn.Location{rightLocation},
|
|
),
|
|
expected: dyn.NewValue(
|
|
[]dyn.Value{
|
|
dyn.NewValue(42, []dyn.Location{leftLocation}),
|
|
dyn.NewValue(10, []dyn.Location{rightLocation}),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
},
|
|
{
|
|
name: "sequence - remove",
|
|
state: visitorState{removed: []string{"root[1]"}},
|
|
left: dyn.NewValue(
|
|
[]dyn.Value{
|
|
dyn.NewValue(42, []dyn.Location{leftLocation}),
|
|
dyn.NewValue(10, []dyn.Location{leftLocation}),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
right: dyn.NewValue(
|
|
[]dyn.Value{
|
|
dyn.NewValue(42, []dyn.Location{rightLocation}),
|
|
},
|
|
[]dyn.Location{rightLocation},
|
|
),
|
|
expected: dyn.NewValue(
|
|
[]dyn.Value{
|
|
dyn.NewValue(42, []dyn.Location{leftLocation}),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
// location hasn't changed because value hasn't changed
|
|
},
|
|
{
|
|
name: "sequence (not updated)",
|
|
state: visitorState{},
|
|
left: dyn.NewValue(
|
|
[]dyn.Value{
|
|
dyn.NewValue(42, []dyn.Location{leftLocation}),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
right: dyn.NewValue(
|
|
[]dyn.Value{
|
|
dyn.NewValue(42, []dyn.Location{rightLocation}),
|
|
},
|
|
[]dyn.Location{rightLocation},
|
|
),
|
|
expected: dyn.NewValue(
|
|
[]dyn.Value{
|
|
dyn.NewValue(42, []dyn.Location{leftLocation}),
|
|
},
|
|
[]dyn.Location{leftLocation},
|
|
),
|
|
},
|
|
{
|
|
name: "nil (not updated)",
|
|
state: visitorState{},
|
|
left: dyn.NilValue.WithLocations([]dyn.Location{leftLocation}),
|
|
right: dyn.NilValue.WithLocations([]dyn.Location{rightLocation}),
|
|
expected: dyn.NilValue.WithLocations([]dyn.Location{leftLocation}),
|
|
},
|
|
{
|
|
name: "nil (updated)",
|
|
state: visitorState{updated: []string{"root"}},
|
|
left: dyn.NilValue,
|
|
right: dyn.NewValue(42, []dyn.Location{rightLocation}),
|
|
expected: dyn.NewValue(42, []dyn.Location{rightLocation}),
|
|
},
|
|
{
|
|
name: "change kind (updated)",
|
|
state: visitorState{updated: []string{"root"}},
|
|
left: dyn.NewValue(42.0, []dyn.Location{leftLocation}),
|
|
right: dyn.NewValue(42, []dyn.Location{rightLocation}),
|
|
expected: dyn.NewValue(42, []dyn.Location{rightLocation}),
|
|
},
|
|
}
|
|
|
|
for _, tc := range modifiedTestCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
s, visitor := createVisitor(visitorOpts{})
|
|
out, err := override(dyn.NewPath(dyn.Key("root")), tc.left, tc.right, visitor)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tc.state, *s)
|
|
assert.Equal(t, tc.expected, out)
|
|
})
|
|
|
|
modified := len(tc.state.removed)+len(tc.state.added)+len(tc.state.updated) > 0
|
|
|
|
// visitor is not used unless there is a change
|
|
|
|
if modified {
|
|
t.Run(tc.name+" - visitor has error", func(t *testing.T) {
|
|
_, visitor := createVisitor(visitorOpts{error: fmt.Errorf("unexpected change in test")})
|
|
_, err := override(dyn.EmptyPath, tc.left, tc.right, visitor)
|
|
|
|
assert.EqualError(t, err, "unexpected change in test")
|
|
})
|
|
|
|
t.Run(tc.name+" - visitor overrides value", func(t *testing.T) {
|
|
expected := dyn.V("return value")
|
|
s, visitor := createVisitor(visitorOpts{returnValue: &expected})
|
|
out, err := override(dyn.EmptyPath, tc.left, tc.right, visitor)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
for _, added := range s.added {
|
|
actual, err := dyn.GetByPath(out, dyn.MustPathFromString(added))
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, expected, actual)
|
|
}
|
|
|
|
for _, updated := range s.updated {
|
|
actual, err := dyn.GetByPath(out, dyn.MustPathFromString(updated))
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, expected, actual)
|
|
}
|
|
})
|
|
|
|
if len(tc.state.removed) > 0 {
|
|
t.Run(tc.name+" - visitor can undo delete", func(t *testing.T) {
|
|
s, visitor := createVisitor(visitorOpts{deleteError: ErrOverrideUndoDelete})
|
|
out, err := override(dyn.EmptyPath, tc.left, tc.right, visitor)
|
|
require.NoError(t, err)
|
|
|
|
for _, removed := range s.removed {
|
|
expected, err := dyn.GetByPath(tc.left, dyn.MustPathFromString(removed))
|
|
require.NoError(t, err)
|
|
|
|
actual, err := dyn.GetByPath(out, dyn.MustPathFromString(removed))
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, expected, actual)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOverride_PreserveMappingKeys(t *testing.T) {
|
|
leftLocation := dyn.Location{File: "left.yml", Line: 1, Column: 1}
|
|
leftKeyLocation := dyn.Location{File: "left.yml", Line: 2, Column: 1}
|
|
leftValueLocation := dyn.Location{File: "left.yml", Line: 3, Column: 1}
|
|
|
|
rightLocation := dyn.Location{File: "right.yml", Line: 1, Column: 1}
|
|
rightKeyLocation := dyn.Location{File: "right.yml", Line: 2, Column: 1}
|
|
rightValueLocation := dyn.Location{File: "right.yml", Line: 3, Column: 1}
|
|
|
|
left := dyn.NewMapping()
|
|
left.Set(dyn.NewValue("a", []dyn.Location{leftKeyLocation}), dyn.NewValue(42, []dyn.Location{leftValueLocation}))
|
|
|
|
right := dyn.NewMapping()
|
|
right.Set(dyn.NewValue("a", []dyn.Location{rightKeyLocation}), dyn.NewValue(7, []dyn.Location{rightValueLocation}))
|
|
|
|
state, visitor := createVisitor(visitorOpts{})
|
|
|
|
out, err := override(
|
|
dyn.EmptyPath,
|
|
dyn.NewValue(left, []dyn.Location{leftLocation}),
|
|
dyn.NewValue(right, []dyn.Location{rightLocation}),
|
|
visitor,
|
|
)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
if err != nil {
|
|
outPairs := out.MustMap().Pairs()
|
|
|
|
assert.Equal(t, visitorState{updated: []string{"a"}}, state)
|
|
assert.Equal(t, 1, len(outPairs))
|
|
|
|
// mapping was first defined in left, so it should keep its location
|
|
assert.Equal(t, leftLocation, out.Location())
|
|
|
|
// if there is a validation error for key value, it should point
|
|
// to where it was initially defined
|
|
assert.Equal(t, leftKeyLocation, outPairs[0].Key.Location())
|
|
|
|
// the value should have updated location, because it has changed
|
|
assert.Equal(t, rightValueLocation, outPairs[0].Value.Location())
|
|
}
|
|
}
|
|
|
|
type visitorState struct {
|
|
added []string
|
|
removed []string
|
|
updated []string
|
|
}
|
|
|
|
type visitorOpts struct {
|
|
error error
|
|
deleteError error
|
|
returnValue *dyn.Value
|
|
}
|
|
|
|
func createVisitor(opts visitorOpts) (*visitorState, OverrideVisitor) {
|
|
s := visitorState{}
|
|
|
|
return &s, OverrideVisitor{
|
|
VisitUpdate: func(valuePath dyn.Path, left dyn.Value, right dyn.Value) (dyn.Value, error) {
|
|
s.updated = append(s.updated, valuePath.String())
|
|
|
|
if opts.error != nil {
|
|
return dyn.NilValue, opts.error
|
|
} else if opts.returnValue != nil {
|
|
return *opts.returnValue, nil
|
|
} else {
|
|
return right, nil
|
|
}
|
|
},
|
|
VisitDelete: func(valuePath dyn.Path, left dyn.Value) error {
|
|
s.removed = append(s.removed, valuePath.String())
|
|
|
|
if opts.error != nil {
|
|
return opts.error
|
|
} else if opts.deleteError != nil {
|
|
return opts.deleteError
|
|
} else {
|
|
return nil
|
|
}
|
|
},
|
|
VisitInsert: func(valuePath dyn.Path, right dyn.Value) (dyn.Value, error) {
|
|
s.added = append(s.added, valuePath.String())
|
|
|
|
if opts.error != nil {
|
|
return dyn.NilValue, opts.error
|
|
} else if opts.returnValue != nil {
|
|
return *opts.returnValue, nil
|
|
} else {
|
|
return right, nil
|
|
}
|
|
},
|
|
}
|
|
}
|