miller/pkg/mlrval/mlrval_yaml_test.go
chaoliang yan 5e8175bc6b
fix: preserve key order in YAML output (#2034)
* fix: preserve key order in YAML output

MlrmapToYAMLNative converted the ordered Mlrmap to map[string]interface{},
which loses insertion order. yaml.v3.Marshal then sorts Go map keys
alphabetically, so YAML output always had sorted keys despite other formats
(JSON, pretty-print) correctly preserving order.

Switch to building yaml.Node trees that preserve the Mlrmap's linked-list
iteration order. Use yaml.Node.Encode for scalar values to correctly handle
edge cases (NaN/Inf, non-UTF-8 strings).

Fixes #2028.

* Update pkg/mlrval/mlrval_yaml_test.go with copilot code-review feedback

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: lawrence3699 <lawrence3699@users.noreply.github.com>
Co-authored-by: John Kerl <kerl.john.r@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-12 09:43:09 -04:00

226 lines
5.8 KiB
Go

// Tests for YAML decode/encode (MlrvalDecodeFromYAML, MlrmapToYAMLNative).
package mlrval
import (
"bytes"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
)
func TestMlrvalFromYAMLNativeScalars(t *testing.T) {
mv, err := mlrvalFromYAMLNative("hello")
assert.NoError(t, err)
assert.True(t, mv.IsString())
s, _ := mv.GetStringValue()
assert.Equal(t, "hello", s)
mv, err = mlrvalFromYAMLNative(42)
assert.NoError(t, err)
assert.True(t, mv.IsInt())
i, _ := mv.GetIntValue()
assert.Equal(t, int64(42), i)
mv, err = mlrvalFromYAMLNative(3.14)
assert.NoError(t, err)
assert.True(t, mv.IsFloat())
f, _ := mv.GetFloatValue()
assert.Equal(t, 3.14, f)
mv, err = mlrvalFromYAMLNative(true)
assert.NoError(t, err)
assert.True(t, mv.IsBool())
b, _ := mv.GetBoolValue()
assert.True(t, b)
mv, err = mlrvalFromYAMLNative(nil)
assert.NoError(t, err)
assert.True(t, mv.IsNull())
}
func TestMlrvalFromYAMLNativeMap(t *testing.T) {
// gopkg.in/yaml.v3 unmarshals into map[string]interface{}
m := map[string]interface{}{
"a": 1,
"b": "two",
"c": true,
}
mv, err := mlrvalFromYAMLNative(m)
assert.NoError(t, err)
assert.True(t, mv.IsMap())
rec := mv.GetMap()
assert.NotNil(t, rec)
assert.Equal(t, int64(3), rec.FieldCount)
av := rec.Get("a")
assert.True(t, av.IsInt())
ai, _ := av.GetIntValue()
assert.Equal(t, int64(1), ai)
bv := rec.Get("b")
assert.True(t, bv.IsString())
bs, _ := bv.GetStringValue()
assert.Equal(t, "two", bs)
}
func TestMlrvalDecodeFromYAML(t *testing.T) {
input := "a: 1\nb: 2\n"
decoder := yaml.NewDecoder(bytes.NewBufferString(input))
mv, eof, err := MlrvalDecodeFromYAML(decoder)
assert.NoError(t, err)
assert.False(t, eof)
assert.NotNil(t, mv)
assert.True(t, mv.IsMap())
rec := mv.GetMap()
assert.Equal(t, int64(2), rec.FieldCount)
v := rec.Get("a")
vi, _ := v.GetIntValue()
assert.Equal(t, int64(1), vi)
v = rec.Get("b")
vi, _ = v.GetIntValue()
assert.Equal(t, int64(2), vi)
// EOF on next decode
_, eof, err = MlrvalDecodeFromYAML(decoder)
assert.NoError(t, err)
assert.True(t, eof)
}
func TestMlrmapToYAMLNativeRoundTrip(t *testing.T) {
rec := NewMlrmapAsRecord()
rec.PutReference("x", FromInt(10))
rec.PutReference("y", FromString("hello"))
rec.PutReference("z", FromBool(true))
native, err := MlrmapToYAMLNative(rec)
assert.NoError(t, err)
assert.NotNil(t, native)
out, err := yaml.Marshal(native)
assert.NoError(t, err)
decoder := yaml.NewDecoder(bytes.NewReader(out))
mv, eof, err := MlrvalDecodeFromYAML(decoder)
assert.NoError(t, err)
assert.False(t, eof)
assert.True(t, mv.IsMap())
back := mv.GetMap()
xv := back.Get("x")
xi, _ := xv.GetIntValue()
assert.Equal(t, int64(10), xi)
yv := back.Get("y")
ys, _ := yv.GetStringValue()
assert.Equal(t, "hello", ys)
zv := back.Get("z")
zb, _ := zv.GetBoolValue()
assert.True(t, zb)
}
func TestMlrvalDecodeFromYAMLArrayOfMaps(t *testing.T) {
input := "- a: 1\n b: 2\n- a: 3\n b: 4\n"
decoder := yaml.NewDecoder(bytes.NewBufferString(input))
mv, eof, err := MlrvalDecodeFromYAML(decoder)
assert.NoError(t, err)
assert.False(t, eof)
assert.True(t, mv.IsArray())
arr := mv.GetArray()
assert.Len(t, arr, 2)
assert.True(t, arr[0].IsMap())
assert.True(t, arr[1].IsMap())
r0 := arr[0].GetMap()
av := r0.Get("a")
ai, _ := av.GetIntValue()
assert.Equal(t, int64(1), ai)
r1 := arr[1].GetMap()
av = r1.Get("a")
ai, _ = av.GetIntValue()
assert.Equal(t, int64(3), ai)
}
func TestMlrvalDecodeFromYAMLMultiDoc(t *testing.T) {
input := "a: 1\nb: 2\n---\nc: 3\nd: 4\n"
decoder := yaml.NewDecoder(bytes.NewBufferString(input))
mv1, eof, err := MlrvalDecodeFromYAML(decoder)
assert.NoError(t, err)
assert.False(t, eof)
assert.True(t, mv1.IsMap())
assert.Equal(t, int64(2), mv1.GetMap().FieldCount)
mv2, eof, err := MlrvalDecodeFromYAML(decoder)
assert.NoError(t, err)
assert.False(t, eof)
assert.True(t, mv2.IsMap())
assert.Equal(t, int64(2), mv2.GetMap().FieldCount)
_, eof, err = MlrvalDecodeFromYAML(decoder)
assert.NoError(t, err)
assert.True(t, eof)
}
func TestYAMLKeyStringNonStringKeys(t *testing.T) {
// map[interface{}]interface{} with int key
m := map[interface{}]interface{}{
1: "one",
"two": 2,
}
mv, err := mlrvalFromYAMLMap(m)
assert.NoError(t, err)
assert.True(t, mv.IsMap())
rec := mv.GetMap()
v := rec.Get("1")
assert.True(t, v.IsString())
s, _ := v.GetStringValue()
assert.Equal(t, "one", s)
v = rec.Get("two")
assert.True(t, v.IsInt())
}
func TestMlrmapToYAMLNativeKeyOrder(t *testing.T) {
rec := NewMlrmapAsRecord()
rec.PutReference("b", FromInt(22))
rec.PutReference("c", FromInt(33))
rec.PutReference("a", FromInt(11))
native, err := MlrmapToYAMLNative(rec)
assert.NoError(t, err)
out, err := yaml.Marshal(native)
assert.NoError(t, err)
var node yaml.Node
err = yaml.Unmarshal(out, &node)
assert.NoError(t, err)
if !assert.Len(t, node.Content, 1, "expected a single YAML document node") {
return
}
mapping := node.Content[0]
if !assert.Equal(t, yaml.MappingNode, mapping.Kind, "expected top-level YAML mapping") {
return
}
// Keys must appear in insertion order (b, c, a), not sorted (a, b, c).
if !assert.GreaterOrEqual(t, len(mapping.Content), 6, "expected at least three key/value pairs") {
return
}
assert.Equal(t, "b", mapping.Content[0].Value)
assert.Equal(t, "c", mapping.Content[2].Value)
assert.Equal(t, "a", mapping.Content[4].Value)
}
func TestMlrmapToYAMLNativeNested(t *testing.T) {
inner := NewMlrmapAsRecord()
inner.PutReference("p", FromInt(1))
inner.PutReference("q", FromInt(2))
rec := NewMlrmapAsRecord()
rec.PutReference("a", FromString("x"))
rec.PutReference("nested", FromMap(inner))
native, err := MlrmapToYAMLNative(rec)
assert.NoError(t, err)
out, err := yaml.Marshal(native)
assert.NoError(t, err)
assert.True(t, strings.Contains(string(out), "nested:"))
assert.True(t, strings.Contains(string(out), "p: 1"))
}