| package yaml |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "math" |
| "reflect" |
| "sort" |
| "strconv" |
| "testing" |
| |
| "github.com/davecgh/go-spew/spew" |
| yaml "gopkg.in/yaml.v2" |
| ) |
| |
| type MarshalTest struct { |
| A string |
| B int64 |
| // Would like to test float64, but it's not supported in go-yaml. |
| // (See https://github.com/go-yaml/yaml/issues/83.) |
| C float32 |
| } |
| |
| func TestMarshal(t *testing.T) { |
| f32String := strconv.FormatFloat(math.MaxFloat32, 'g', -1, 32) |
| s := MarshalTest{"a", math.MaxInt64, math.MaxFloat32} |
| e := []byte(fmt.Sprintf("A: a\nB: %d\nC: %s\n", math.MaxInt64, f32String)) |
| |
| y, err := Marshal(s) |
| if err != nil { |
| t.Errorf("error marshaling YAML: %v", err) |
| } |
| |
| if !reflect.DeepEqual(y, e) { |
| t.Errorf("marshal YAML was unsuccessful, expected: %#v, got: %#v", |
| string(e), string(y)) |
| } |
| } |
| |
| type UnmarshalString struct { |
| A string |
| True string |
| } |
| |
| type UnmarshalStringMap struct { |
| A map[string]string |
| } |
| |
| type UnmarshalNestedString struct { |
| A NestedString |
| } |
| |
| type NestedString struct { |
| A string |
| } |
| |
| type UnmarshalSlice struct { |
| A []NestedSlice |
| } |
| |
| type NestedSlice struct { |
| B string |
| C *string |
| } |
| |
| func TestUnmarshal(t *testing.T) { |
| y := []byte("a: 1") |
| s1 := UnmarshalString{} |
| e1 := UnmarshalString{A: "1"} |
| unmarshal(t, y, &s1, &e1) |
| |
| y = []byte("a: true") |
| s1 = UnmarshalString{} |
| e1 = UnmarshalString{A: "true"} |
| unmarshal(t, y, &s1, &e1) |
| |
| y = []byte("true: 1") |
| s1 = UnmarshalString{} |
| e1 = UnmarshalString{True: "1"} |
| unmarshal(t, y, &s1, &e1) |
| |
| y = []byte("a:\n a: 1") |
| s2 := UnmarshalNestedString{} |
| e2 := UnmarshalNestedString{NestedString{"1"}} |
| unmarshal(t, y, &s2, &e2) |
| |
| y = []byte("a:\n - b: abc\n c: def\n - b: 123\n c: 456\n") |
| s3 := UnmarshalSlice{} |
| e3 := UnmarshalSlice{[]NestedSlice{NestedSlice{"abc", strPtr("def")}, NestedSlice{"123", strPtr("456")}}} |
| unmarshal(t, y, &s3, &e3) |
| |
| y = []byte("a:\n b: 1") |
| s4 := UnmarshalStringMap{} |
| e4 := UnmarshalStringMap{map[string]string{"b": "1"}} |
| unmarshal(t, y, &s4, &e4) |
| |
| y = []byte(` |
| a: |
| name: TestA |
| b: |
| name: TestB |
| `) |
| type NamedThing struct { |
| Name string `json:"name"` |
| } |
| s5 := map[string]*NamedThing{} |
| e5 := map[string]*NamedThing{ |
| "a": &NamedThing{Name: "TestA"}, |
| "b": &NamedThing{Name: "TestB"}, |
| } |
| unmarshal(t, y, &s5, &e5) |
| } |
| |
| func unmarshal(t *testing.T, y []byte, s, e interface{}, opts ...JSONOpt) { |
| err := Unmarshal(y, s, opts...) |
| if err != nil { |
| t.Errorf("error unmarshaling YAML: %v", err) |
| } |
| |
| if !reflect.DeepEqual(s, e) { |
| t.Errorf("unmarshal YAML was unsuccessful, expected: %+#v, got: %+#v", |
| e, s) |
| } |
| } |
| |
| func TestUnmarshalStrict(t *testing.T) { |
| y := []byte("a: 1") |
| s1 := UnmarshalString{} |
| e1 := UnmarshalString{A: "1"} |
| unmarshalStrict(t, y, &s1, &e1) |
| |
| y = []byte("a: true") |
| s1 = UnmarshalString{} |
| e1 = UnmarshalString{A: "true"} |
| unmarshalStrict(t, y, &s1, &e1) |
| |
| y = []byte("true: 1") |
| s1 = UnmarshalString{} |
| e1 = UnmarshalString{True: "1"} |
| unmarshalStrict(t, y, &s1, &e1) |
| |
| y = []byte("a:\n a: 1") |
| s2 := UnmarshalNestedString{} |
| e2 := UnmarshalNestedString{NestedString{"1"}} |
| unmarshalStrict(t, y, &s2, &e2) |
| |
| y = []byte("a:\n - b: abc\n c: def\n - b: 123\n c: 456\n") |
| s3 := UnmarshalSlice{} |
| e3 := UnmarshalSlice{[]NestedSlice{NestedSlice{"abc", strPtr("def")}, NestedSlice{"123", strPtr("456")}}} |
| unmarshalStrict(t, y, &s3, &e3) |
| |
| y = []byte("a:\n b: 1") |
| s4 := UnmarshalStringMap{} |
| e4 := UnmarshalStringMap{map[string]string{"b": "1"}} |
| unmarshalStrict(t, y, &s4, &e4) |
| |
| y = []byte(` |
| a: |
| name: TestA |
| b: |
| name: TestB |
| `) |
| type NamedThing struct { |
| Name string `json:"name"` |
| } |
| s5 := map[string]*NamedThing{} |
| e5 := map[string]*NamedThing{ |
| "a": &NamedThing{Name: "TestA"}, |
| "b": &NamedThing{Name: "TestB"}, |
| } |
| unmarshal(t, y, &s5, &e5) |
| |
| // When using not-so-strict unmarshal, we should |
| // be picking up the ID-1 as the value in the "id" field |
| y = []byte(` |
| a: |
| name: TestA |
| id: ID-A |
| id: ID-1 |
| `) |
| type NamedThing2 struct { |
| Name string `json:"name"` |
| ID string `json:"id"` |
| } |
| s6 := map[string]*NamedThing2{} |
| e6 := map[string]*NamedThing2{ |
| "a": {Name: "TestA", ID: "ID-1"}, |
| } |
| unmarshal(t, y, &s6, &e6) |
| } |
| |
| func TestUnmarshalStrictFails(t *testing.T) { |
| y := []byte("a: true\na: false") |
| s1 := UnmarshalString{} |
| unmarshalStrictFail(t, y, &s1) |
| |
| y = []byte("a:\n - b: abc\n c: 32\n b: 123") |
| s2 := UnmarshalSlice{} |
| unmarshalStrictFail(t, y, &s2) |
| |
| y = []byte("a:\n b: 1\n c: 3") |
| s3 := UnmarshalStringMap{} |
| unmarshalStrictFail(t, y, &s3) |
| |
| type NamedThing struct { |
| Name string `json:"name"` |
| ID string `json:"id"` |
| } |
| // When using strict unmarshal, we should see |
| // the unmarshal fail if there are multiple keys |
| y = []byte(` |
| a: |
| name: TestA |
| id: ID-A |
| id: ID-1 |
| `) |
| s4 := NamedThing{} |
| unmarshalStrictFail(t, y, &s4) |
| |
| // Strict unmarshal should fail for unknown fields |
| y = []byte(` |
| name: TestB |
| id: ID-B |
| unknown: Some-Value |
| `) |
| s5 := NamedThing{} |
| unmarshalStrictFail(t, y, &s5) |
| } |
| |
| func unmarshalStrict(t *testing.T, y []byte, s, e interface{}, opts ...JSONOpt) { |
| err := UnmarshalStrict(y, s, opts...) |
| if err != nil { |
| t.Errorf("error unmarshaling YAML: %v", err) |
| } |
| |
| if !reflect.DeepEqual(s, e) { |
| t.Errorf("unmarshal YAML was unsuccessful, expected: %+#v, got: %+#v", |
| e, s) |
| } |
| } |
| |
| func unmarshalStrictFail(t *testing.T, y []byte, s interface{}, opts ...JSONOpt) { |
| err := UnmarshalStrict(y, s, opts...) |
| if err == nil { |
| t.Errorf("error unmarshaling YAML: %v", err) |
| } |
| } |
| |
| type Case struct { |
| input string |
| output string |
| // By default we test that reversing the output == input. But if there is a |
| // difference in the reversed output, you can optionally specify it here. |
| reverse *string |
| } |
| |
| type RunType int |
| |
| const ( |
| RunTypeJSONToYAML RunType = iota |
| RunTypeYAMLToJSON |
| ) |
| |
| func TestJSONToYAML(t *testing.T) { |
| cases := []Case{ |
| { |
| `{"t":"a"}`, |
| "t: a\n", |
| nil, |
| }, { |
| `{"t":null}`, |
| "t: null\n", |
| nil, |
| }, |
| } |
| |
| runCases(t, RunTypeJSONToYAML, cases) |
| } |
| |
| func TestYAMLToJSON(t *testing.T) { |
| cases := []Case{ |
| { |
| "t: a\n", |
| `{"t":"a"}`, |
| nil, |
| }, { |
| "t: \n", |
| `{"t":null}`, |
| strPtr("t: null\n"), |
| }, { |
| "t: null\n", |
| `{"t":null}`, |
| nil, |
| }, { |
| "1: a\n", |
| `{"1":"a"}`, |
| strPtr("\"1\": a\n"), |
| }, { |
| "1000000000000000000000000000000000000: a\n", |
| `{"1e+36":"a"}`, |
| strPtr("\"1e+36\": a\n"), |
| }, { |
| "1e+36: a\n", |
| `{"1e+36":"a"}`, |
| strPtr("\"1e+36\": a\n"), |
| }, { |
| "\"1e+36\": a\n", |
| `{"1e+36":"a"}`, |
| nil, |
| }, { |
| "\"1.2\": a\n", |
| `{"1.2":"a"}`, |
| nil, |
| }, { |
| "- t: a\n", |
| `[{"t":"a"}]`, |
| nil, |
| }, { |
| "- t: a\n" + |
| "- t:\n" + |
| " b: 1\n" + |
| " c: 2\n", |
| `[{"t":"a"},{"t":{"b":1,"c":2}}]`, |
| nil, |
| }, { |
| `[{t: a}, {t: {b: 1, c: 2}}]`, |
| `[{"t":"a"},{"t":{"b":1,"c":2}}]`, |
| strPtr("- t: a\n" + |
| "- t:\n" + |
| " b: 1\n" + |
| " c: 2\n"), |
| }, { |
| "- t: \n", |
| `[{"t":null}]`, |
| strPtr("- t: null\n"), |
| }, { |
| "- t: null\n", |
| `[{"t":null}]`, |
| nil, |
| }, |
| } |
| |
| // Cases that should produce errors. |
| _ = []Case{ |
| { |
| "~: a", |
| `{"null":"a"}`, |
| nil, |
| }, { |
| "a: !!binary gIGC\n", |
| "{\"a\":\"\x80\x81\x82\"}", |
| nil, |
| }, |
| } |
| |
| runCases(t, RunTypeYAMLToJSON, cases) |
| } |
| |
| func runCases(t *testing.T, runType RunType, cases []Case) { |
| var f func([]byte) ([]byte, error) |
| var invF func([]byte) ([]byte, error) |
| var msg string |
| var invMsg string |
| if runType == RunTypeJSONToYAML { |
| f = JSONToYAML |
| invF = YAMLToJSON |
| msg = "JSON to YAML" |
| invMsg = "YAML back to JSON" |
| } else { |
| f = YAMLToJSON |
| invF = JSONToYAML |
| msg = "YAML to JSON" |
| invMsg = "JSON back to YAML" |
| } |
| |
| for _, c := range cases { |
| // Convert the string. |
| t.Logf("converting %s\n", c.input) |
| output, err := f([]byte(c.input)) |
| if err != nil { |
| t.Errorf("Failed to convert %s, input: `%s`, err: %v", msg, c.input, err) |
| } |
| |
| // Check it against the expected output. |
| if string(output) != c.output { |
| t.Errorf("Failed to convert %s, input: `%s`, expected `%s`, got `%s`", |
| msg, c.input, c.output, string(output)) |
| } |
| |
| // Set the string that we will compare the reversed output to. |
| reverse := c.input |
| // If a special reverse string was specified, use that instead. |
| if c.reverse != nil { |
| reverse = *c.reverse |
| } |
| |
| // Reverse the output. |
| input, err := invF(output) |
| if err != nil { |
| t.Errorf("Failed to convert %s, input: `%s`, err: %v", invMsg, string(output), err) |
| } |
| |
| // Check the reverse is equal to the input (or to *c.reverse). |
| if string(input) != reverse { |
| t.Errorf("Failed to convert %s, input: `%s`, expected `%s`, got `%s`", |
| invMsg, string(output), reverse, string(input)) |
| } |
| } |
| |
| } |
| |
| // To be able to easily fill in the *Case.reverse string above. |
| func strPtr(s string) *string { |
| return &s |
| } |
| |
| func TestYAMLToJSONStrict(t *testing.T) { |
| const data = ` |
| foo: bar |
| foo: baz |
| ` |
| if _, err := YAMLToJSON([]byte(data)); err != nil { |
| t.Error("expected YAMLtoJSON to pass on duplicate field names") |
| } |
| if _, err := YAMLToJSONStrict([]byte(data)); err == nil { |
| t.Error("expected YAMLtoJSONStrict to fail on duplicate field names") |
| } |
| } |
| |
| func TestJSONObjectToYAMLObject(t *testing.T) { |
| const bigUint64 = ((uint64(1) << 63) + 500) / 1000 * 1000 |
| intOrInt64 := func(i64 int64) interface{} { |
| if i := int(i64); i64 == int64(i) { |
| return i |
| } |
| return i64 |
| } |
| |
| tests := []struct { |
| name string |
| input map[string]interface{} |
| expected yaml.MapSlice |
| }{ |
| {name: "nil", expected: yaml.MapSlice(nil)}, |
| {name: "empty", input: map[string]interface{}{}, expected: yaml.MapSlice(nil)}, |
| { |
| name: "values", |
| input: map[string]interface{}{ |
| "nil slice": []interface{}(nil), |
| "nil map": map[string]interface{}(nil), |
| "empty slice": []interface{}{}, |
| "empty map": map[string]interface{}{}, |
| "bool": true, |
| "float64": float64(42.1), |
| "fractionless": float64(42), |
| "int": int(42), |
| "int64": int64(42), |
| "int64 big": float64(math.Pow(2, 62)), |
| "negative int64 big": -float64(math.Pow(2, 62)), |
| "map": map[string]interface{}{"foo": "bar"}, |
| "slice": []interface{}{"foo", "bar"}, |
| "string": string("foo"), |
| "uint64 big": bigUint64, |
| }, |
| expected: yaml.MapSlice{ |
| {Key: "nil slice"}, |
| {Key: "nil map"}, |
| {Key: "empty slice", Value: []interface{}{}}, |
| {Key: "empty map", Value: yaml.MapSlice(nil)}, |
| {Key: "bool", Value: true}, |
| {Key: "float64", Value: float64(42.1)}, |
| {Key: "fractionless", Value: int(42)}, |
| {Key: "int", Value: int(42)}, |
| {Key: "int64", Value: int(42)}, |
| {Key: "int64 big", Value: intOrInt64(int64(1) << 62)}, |
| {Key: "negative int64 big", Value: intOrInt64(-(1 << 62))}, |
| {Key: "map", Value: yaml.MapSlice{{Key: "foo", Value: "bar"}}}, |
| {Key: "slice", Value: []interface{}{"foo", "bar"}}, |
| {Key: "string", Value: string("foo")}, |
| {Key: "uint64 big", Value: bigUint64}, |
| }, |
| }, |
| } |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| got := JSONObjectToYAMLObject(tt.input) |
| sortMapSlicesInPlace(tt.expected) |
| sortMapSlicesInPlace(got) |
| if !reflect.DeepEqual(got, tt.expected) { |
| t.Errorf("jsonToYAML() = %v, want %v", spew.Sdump(got), spew.Sdump(tt.expected)) |
| } |
| |
| jsonBytes, err := json.Marshal(tt.input) |
| if err != nil { |
| t.Fatalf("unexpected json.Marshal error: %v", err) |
| } |
| var gotByRoundtrip yaml.MapSlice |
| if err := yaml.Unmarshal(jsonBytes, &gotByRoundtrip); err != nil { |
| t.Fatalf("unexpected yaml.Unmarshal error: %v", err) |
| } |
| |
| // yaml.Unmarshal loses precision, it's rounding to the 4th last digit. |
| // Replicate this here in the test, but don't change the type. |
| for i := range got { |
| switch got[i].Key { |
| case "int64 big", "uint64 big", "negative int64 big": |
| switch v := got[i].Value.(type) { |
| case int64: |
| d := int64(500) |
| if v < 0 { |
| d = -500 |
| } |
| got[i].Value = int64((v+d)/1000) * 1000 |
| case uint64: |
| got[i].Value = uint64((v+500)/1000) * 1000 |
| case int: |
| d := int(500) |
| if v < 0 { |
| d = -500 |
| } |
| got[i].Value = int((v+d)/1000) * 1000 |
| default: |
| t.Fatalf("unexpected type for key %s: %v:%T", got[i].Key, v, v) |
| } |
| } |
| } |
| |
| if !reflect.DeepEqual(got, gotByRoundtrip) { |
| t.Errorf("yaml.Unmarshal(json.Marshal(tt.input)) = %v, want %v\njson: %s", spew.Sdump(gotByRoundtrip), spew.Sdump(got), string(jsonBytes)) |
| } |
| }) |
| } |
| } |
| |
| func sortMapSlicesInPlace(x interface{}) { |
| switch x := x.(type) { |
| case []interface{}: |
| for i := range x { |
| sortMapSlicesInPlace(x[i]) |
| } |
| case yaml.MapSlice: |
| sort.Slice(x, func(a, b int) bool { |
| return x[a].Key.(string) < x[b].Key.(string) |
| }) |
| } |
| } |