1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
|
package tomltest
import (
"fmt"
"math"
"reflect"
"sort"
"strings"
"time"
)
// CompareTOML compares the given arguments.
//
// The returned value is a copy of Test with Failure set to a (human-readable)
// description of the first element that is unequal. If both arguments are equal
// Test is returned unchanged.
//
// Reflect.DeepEqual could work here, but it won't tell us how the two
// structures are different.
func (r Test) CompareTOML(want, have any) Test {
if isTomlValue(want) {
if !isTomlValue(have) {
return r.fail("Type for key %q differs:\n"+
" Expected: %v (%s)\n"+
" Your encoder: %v (%s)",
r.Key, want, fmtType(want), have, fmtType(have))
}
if !deepEqual(want, have) {
return r.fail("Values for key %q differ:\n"+
" Expected: %v (%s)\n"+
" Your encoder: %v (%s)",
r.Key, want, fmtType(want), have, fmtType(have))
}
return r
}
switch w := want.(type) {
case map[string]any:
return r.cmpTOMLMap(w, have)
case []map[string]any:
ww := make([]any, 0, len(w))
for _, v := range w {
ww = append(ww, v)
}
return r.cmpTOMLArrays(ww, have)
case []any:
return r.cmpTOMLArrays(w, have)
default:
return r.fail("Unrecognized TOML structure: %s", fmtType(want))
}
}
func (r Test) cmpTOMLMap(want map[string]any, have any) Test {
haveMap, ok := have.(map[string]any)
if !ok {
return r.mismatch("table", want, haveMap)
}
wantKeys, haveKeys := mapKeys(want), mapKeys(haveMap)
// Check that the keys of each map are equivalent.
for _, k := range wantKeys {
if _, ok := haveMap[k]; !ok {
bunk := r.kjoin(k)
return bunk.fail("Could not find key %q in encoder output", bunk.Key)
}
}
for _, k := range haveKeys {
if _, ok := want[k]; !ok {
bunk := r.kjoin(k)
return bunk.fail("Could not find key %q in expected output", bunk.Key)
}
}
// Okay, now make sure that each value is equivalent.
for _, k := range wantKeys {
if sub := r.kjoin(k).CompareTOML(want[k], haveMap[k]); sub.Failed() {
return sub
}
}
return r
}
func (r Test) cmpTOMLArrays(want []any, have any) Test {
// Slice can be decoded to []any for an array of primitives, or
// []map[string]any for an array of tables.
//
// TODO: it would be nicer if it could always decode to []any?
haveSlice, ok := have.([]any)
if !ok {
tblArray, ok := have.([]map[string]any)
if !ok {
return r.mismatch("array", want, have)
}
haveSlice = make([]any, len(tblArray))
for i := range tblArray {
haveSlice[i] = tblArray[i]
}
}
if len(want) != len(haveSlice) {
return r.fail("Array lengths differ for key %q"+
" Expected: %[2]v (len=%[4]d)\n"+
" Your encoder: %[3]v (len=%[5]d)",
r.Key, want, haveSlice, len(want), len(haveSlice))
}
for i := 0; i < len(want); i++ {
if sub := r.CompareTOML(want[i], haveSlice[i]); sub.Failed() {
return sub
}
}
return r
}
// reflect.DeepEqual() that deals with NaN != NaN
func deepEqual(want, have any) bool {
var wantF, haveF float64
switch f := want.(type) {
case float32:
wantF = float64(f)
case float64:
wantF = f
}
switch f := have.(type) {
case float32:
haveF = float64(f)
case float64:
haveF = f
}
if math.IsNaN(wantF) && math.IsNaN(haveF) {
return true
}
// Time.Equal deals with some edge-cases such as offset +0000 and Z being
// identical.
if haveT, ok := have.(time.Time); ok {
if wantT, ok := want.(time.Time); ok {
return wantT.Equal(haveT)
}
}
return reflect.DeepEqual(want, have)
}
func isTomlValue(v any) bool {
switch v.(type) {
case map[string]any, []map[string]any, []any:
return false
}
return true
}
// fmt %T with "interface {}" replaced with "any", which is far more readable.
func fmtType(t any) string { return strings.ReplaceAll(fmt.Sprintf("%T", t), "interface {}", "any") }
func fmtHashV(t any) string { return strings.ReplaceAll(fmt.Sprintf("%#v", t), "interface {}", "any") }
func mapKeys[M ~map[string]V, V any](m M) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
|