|
@@ -0,0 +1,235 @@
|
|
|
+// Copyright (c) 2018 Uber Technologies, Inc.
|
|
|
+//
|
|
|
+// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
+// of this software and associated documentation files (the "Software"), to deal
|
|
|
+// in the Software without restriction, including without limitation the rights
|
|
|
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
+// copies of the Software, and to permit persons to whom the Software is
|
|
|
+// furnished to do so, subject to the following conditions:
|
|
|
+//
|
|
|
+// The above copyright notice and this permission notice shall be included in
|
|
|
+// all copies or substantial portions of the Software.
|
|
|
+//
|
|
|
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
|
+// THE SOFTWARE.
|
|
|
+
|
|
|
+package yamlpatch
|
|
|
+
|
|
|
+import (
|
|
|
+ "bytes"
|
|
|
+ "io/ioutil"
|
|
|
+ "strings"
|
|
|
+ "testing"
|
|
|
+
|
|
|
+ "github.com/stretchr/testify/assert"
|
|
|
+ "github.com/stretchr/testify/require"
|
|
|
+ yaml "gopkg.in/yaml.v2"
|
|
|
+)
|
|
|
+
|
|
|
+func trimcr(s string) string {
|
|
|
+ return strings.ReplaceAll(s, "\r\n", "\n")
|
|
|
+}
|
|
|
+
|
|
|
+func mustRead(t testing.TB, fname string) []byte {
|
|
|
+ contents, err := ioutil.ReadFile(fname)
|
|
|
+ require.NoError(t, err, "failed to read file: %s", fname)
|
|
|
+ return contents
|
|
|
+}
|
|
|
+
|
|
|
+func dump(t testing.TB, actual, expected string) {
|
|
|
+ // It's impossible to debug YAML if the actual and expected values are
|
|
|
+ // printed on a single line.
|
|
|
+ t.Logf("Actual:\n\n%s\n\n", actual)
|
|
|
+ t.Logf("Expected:\n\n%s\n\n", expected)
|
|
|
+}
|
|
|
+
|
|
|
+func strip(s string) string {
|
|
|
+ // It's difficult to write string constants that are valid YAML. Normalize
|
|
|
+ // strings for ease of testing.
|
|
|
+ s = strings.TrimSpace(s)
|
|
|
+ s = strings.Replace(s, "\t", " ", -1)
|
|
|
+ return s
|
|
|
+}
|
|
|
+
|
|
|
+func canonicalize(t testing.TB, s string) string {
|
|
|
+ // round-trip to canonicalize formatting
|
|
|
+ var i interface{}
|
|
|
+ require.NoError(t,
|
|
|
+ yaml.Unmarshal([]byte(strip(s)), &i),
|
|
|
+ "canonicalize: couldn't unmarshal YAML",
|
|
|
+ )
|
|
|
+ formatted, err := yaml.Marshal(i)
|
|
|
+ require.NoError(t, err, "canonicalize: couldn't marshal YAML")
|
|
|
+ return string(bytes.TrimSpace(formatted))
|
|
|
+}
|
|
|
+
|
|
|
+func unmarshal(t testing.TB, s string) interface{} {
|
|
|
+ var i interface{}
|
|
|
+ require.NoError(t, yaml.Unmarshal([]byte(strip(s)), &i), "unmarshaling failed")
|
|
|
+ return i
|
|
|
+}
|
|
|
+
|
|
|
+func succeeds(t testing.TB, strict bool, left, right, expect string) {
|
|
|
+ l, r := unmarshal(t, left), unmarshal(t, right)
|
|
|
+ m, err := merge(l, r, strict)
|
|
|
+ require.NoError(t, err, "merge failed")
|
|
|
+
|
|
|
+ actualBytes, err := yaml.Marshal(m)
|
|
|
+ require.NoError(t, err, "couldn't marshal merged structure")
|
|
|
+ actual := canonicalize(t, string(actualBytes))
|
|
|
+ expect = canonicalize(t, expect)
|
|
|
+ if !assert.Equal(t, expect, actual) {
|
|
|
+ dump(t, actual, expect)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func fails(t testing.TB, strict bool, left, right string) {
|
|
|
+ _, err := merge(unmarshal(t, left), unmarshal(t, right), strict)
|
|
|
+ assert.Error(t, err, "merge succeeded")
|
|
|
+}
|
|
|
+
|
|
|
+func TestIntegration(t *testing.T) {
|
|
|
+ base := mustRead(t, "testdata/base.yaml")
|
|
|
+ prod := mustRead(t, "testdata/production.yaml")
|
|
|
+ expect := mustRead(t, "testdata/expect.yaml")
|
|
|
+
|
|
|
+ merged, err := YAML([][]byte{base, prod}, true /* strict */)
|
|
|
+ require.NoError(t, err, "merge failed")
|
|
|
+
|
|
|
+ if !assert.Equal(t, trimcr(string(expect)), merged.String(), "unexpected contents") {
|
|
|
+ dump(t, merged.String(), string(expect))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestEmpty(t *testing.T) {
|
|
|
+ full := []byte("foo: bar\n")
|
|
|
+ null := []byte("~")
|
|
|
+
|
|
|
+ tests := []struct {
|
|
|
+ desc string
|
|
|
+ sources [][]byte
|
|
|
+ expect string
|
|
|
+ }{
|
|
|
+ {"empty base", [][]byte{nil, full}, string(full)},
|
|
|
+ {"empty override", [][]byte{full, nil}, string(full)},
|
|
|
+ {"both empty", [][]byte{nil, nil}, ""},
|
|
|
+ {"null base", [][]byte{null, full}, string(full)},
|
|
|
+ {"null override", [][]byte{full, null}, "null\n"},
|
|
|
+ {"empty base and null override", [][]byte{nil, null}, "null\n"},
|
|
|
+ {"null base and empty override", [][]byte{null, nil}, "null\n"},
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, tt := range tests {
|
|
|
+ t.Run(tt.desc, func(t *testing.T) {
|
|
|
+ merged, err := YAML(tt.sources, true /* strict */)
|
|
|
+ require.NoError(t, err, "merge failed")
|
|
|
+ assert.Equal(t, tt.expect, merged.String(), "wrong contents after merge")
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestSuccess(t *testing.T) {
|
|
|
+ left := `
|
|
|
+fun: [maserati, porsche]
|
|
|
+practical: {toyota: camry, honda: accord}
|
|
|
+occupants:
|
|
|
+ honda: {driver: jane, backseat: [nate]}
|
|
|
+ `
|
|
|
+ right := `
|
|
|
+fun: [lamborghini, porsche]
|
|
|
+practical: {honda: civic, nissan: altima}
|
|
|
+occupants:
|
|
|
+ honda: {passenger: arthur, backseat: [nora]}
|
|
|
+ `
|
|
|
+ expect := `
|
|
|
+fun: [lamborghini, porsche]
|
|
|
+practical: {toyota: camry, honda: civic, nissan: altima}
|
|
|
+occupants:
|
|
|
+ honda: {passenger: arthur, driver: jane, backseat: [nora]}
|
|
|
+ `
|
|
|
+ succeeds(t, true, left, right, expect)
|
|
|
+ succeeds(t, false, left, right, expect)
|
|
|
+}
|
|
|
+
|
|
|
+func TestErrors(t *testing.T) {
|
|
|
+ check := func(t testing.TB, strict bool, sources ...[]byte) error {
|
|
|
+ _, err := YAML(sources, strict)
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ t.Run("tabs in source", func(t *testing.T) {
|
|
|
+ src := []byte("foo:\n\tbar:baz")
|
|
|
+ assert.Error(t, check(t, false, src), "expected error in permissive mode")
|
|
|
+ assert.Error(t, check(t, true, src), "expected error in strict mode")
|
|
|
+ })
|
|
|
+
|
|
|
+ t.Run("duplicated keys", func(t *testing.T) {
|
|
|
+ src := []byte("{foo: bar, foo: baz}")
|
|
|
+ assert.NoError(t, check(t, false, src), "expected success in permissive mode")
|
|
|
+ assert.Error(t, check(t, true, src), "expected error in permissive mode")
|
|
|
+ })
|
|
|
+
|
|
|
+ t.Run("merge error", func(t *testing.T) {
|
|
|
+ left := []byte("foo: [1, 2]")
|
|
|
+ right := []byte("foo: {bar: baz}")
|
|
|
+ assert.NoError(t, check(t, false, left, right), "expected success in permissive mode")
|
|
|
+ assert.Error(t, check(t, true, left, right), "expected error in strict mode")
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func TestMismatchedTypes(t *testing.T) {
|
|
|
+ tests := []struct {
|
|
|
+ desc string
|
|
|
+ left, right string
|
|
|
+ }{
|
|
|
+ {"sequence and mapping", "[one, two]", "{foo: bar}"},
|
|
|
+ {"sequence and scalar", "[one, two]", "foo"},
|
|
|
+ {"mapping and scalar", "{foo: bar}", "foo"},
|
|
|
+ {"nested", "{foo: [one, two]}", "{foo: bar}"},
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, tt := range tests {
|
|
|
+ t.Run(tt.desc+" strict", func(t *testing.T) {
|
|
|
+ fails(t, true, tt.left, tt.right)
|
|
|
+ })
|
|
|
+ t.Run(tt.desc+" permissive", func(t *testing.T) {
|
|
|
+ // prefer the higher-priority value
|
|
|
+ succeeds(t, false, tt.left, tt.right, tt.right)
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestBooleans(t *testing.T) {
|
|
|
+ // YAML helpfully interprets many strings as Booleans.
|
|
|
+ tests := []struct {
|
|
|
+ in, out string
|
|
|
+ }{
|
|
|
+ {"yes", "true"},
|
|
|
+ {"YES", "true"},
|
|
|
+ {"on", "true"},
|
|
|
+ {"ON", "true"},
|
|
|
+ {"no", "false"},
|
|
|
+ {"NO", "false"},
|
|
|
+ {"off", "false"},
|
|
|
+ {"OFF", "false"},
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, tt := range tests {
|
|
|
+ t.Run(tt.in, func(t *testing.T) {
|
|
|
+ succeeds(t, true, "", tt.in, tt.out)
|
|
|
+ succeeds(t, false, "", tt.in, tt.out)
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestExplicitNil(t *testing.T) {
|
|
|
+ base := `foo: {one: two}`
|
|
|
+ override := `foo: ~`
|
|
|
+ expect := `foo: ~`
|
|
|
+ succeeds(t, true, base, override, expect)
|
|
|
+ succeeds(t, false, base, override, expect)
|
|
|
+}
|