-
Notifications
You must be signed in to change notification settings - Fork 72
/
toml.go
176 lines (158 loc) · 4.19 KB
/
toml.go
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
167
168
169
170
171
172
173
174
175
176
package tomltest
import (
"fmt"
"math"
"reflect"
"sort"
"strconv"
"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: %s (%s)\n"+
" Your encoder: %s (%s)",
r.Key, fmtVal(want), fmtType(want), fmtVal(have), fmtType(have))
}
if !deepEqual(want, have) {
return r.fail("Values for key %q differ:\n"+
" Expected: %s (%s)\n"+
" Your encoder: %s (%s)",
r.Key, fmtVal(want), fmtType(want), fmtVal(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 fmtVal(v any) string {
switch vv := v.(type) {
case float64:
return strconv.FormatFloat(vv, 'f', -1, 64)
default:
return fmt.Sprintf("%v", vv)
}
}
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
}