git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/toml/internal/toml-test/json.go (about) 1 //go:build go1.16 2 // +build go1.16 3 4 package tomltest 5 6 import ( 7 "strconv" 8 "strings" 9 "time" 10 ) 11 12 // CompareJSON compares the given arguments. 13 // 14 // The returned value is a copy of Test with Failure set to a (human-readable) 15 // description of the first element that is unequal. If both arguments are 16 // equal, Test is returned unchanged. 17 // 18 // reflect.DeepEqual could work here, but it won't tell us how the two 19 // structures are different. 20 func (r Test) CompareJSON(want, have interface{}) Test { 21 switch w := want.(type) { 22 case map[string]interface{}: 23 return r.cmpJSONMaps(w, have) 24 case []interface{}: 25 return r.cmpJSONArrays(w, have) 26 default: 27 return r.fail( 28 "Key '%s' in expected output should be a map or a list of maps, but it's a %T", 29 r.Key, want) 30 } 31 } 32 33 func (r Test) cmpJSONMaps(want map[string]interface{}, have interface{}) Test { 34 haveMap, ok := have.(map[string]interface{}) 35 if !ok { 36 return r.mismatch("table", want, haveMap) 37 } 38 39 // Check to make sure both or neither are values. 40 if isValue(want) && !isValue(haveMap) { 41 return r.fail( 42 "Key '%s' is supposed to be a value, but the parser reports it as a table", 43 r.Key) 44 } 45 if !isValue(want) && isValue(haveMap) { 46 return r.fail( 47 "Key '%s' is supposed to be a table, but the parser reports it as a value", 48 r.Key) 49 } 50 if isValue(want) && isValue(haveMap) { 51 return r.cmpJSONValues(want, haveMap) 52 } 53 54 // Check that the keys of each map are equivalent. 55 for k := range want { 56 if _, ok := haveMap[k]; !ok { 57 bunk := r.kjoin(k) 58 return bunk.fail("Could not find key '%s' in parser output.", 59 bunk.Key) 60 } 61 } 62 for k := range haveMap { 63 if _, ok := want[k]; !ok { 64 bunk := r.kjoin(k) 65 return bunk.fail("Could not find key '%s' in expected output.", 66 bunk.Key) 67 } 68 } 69 70 // Okay, now make sure that each value is equivalent. 71 for k := range want { 72 if sub := r.kjoin(k).CompareJSON(want[k], haveMap[k]); sub.Failed() { 73 return sub 74 } 75 } 76 return r 77 } 78 79 func (r Test) cmpJSONArrays(want, have interface{}) Test { 80 wantSlice, ok := want.([]interface{}) 81 if !ok { 82 return r.bug("'value' should be a JSON array when 'type=array', but it is a %T", want) 83 } 84 85 haveSlice, ok := have.([]interface{}) 86 if !ok { 87 return r.fail( 88 "Malformed output from your encoder: 'value' is not a JSON array: %T", have) 89 } 90 91 if len(wantSlice) != len(haveSlice) { 92 return r.fail("Array lengths differ for key '%s':\n"+ 93 " Expected: %d\n"+ 94 " Your encoder: %d", 95 r.Key, len(wantSlice), len(haveSlice)) 96 } 97 for i := 0; i < len(wantSlice); i++ { 98 if sub := r.CompareJSON(wantSlice[i], haveSlice[i]); sub.Failed() { 99 return sub 100 } 101 } 102 return r 103 } 104 105 func (r Test) cmpJSONValues(want, have map[string]interface{}) Test { 106 wantType, ok := want["type"].(string) 107 if !ok { 108 return r.bug("'type' should be a string, but it is a %T", want["type"]) 109 } 110 111 haveType, ok := have["type"].(string) 112 if !ok { 113 return r.fail("Malformed output from your encoder: 'type' is not a string: %T", have["type"]) 114 } 115 116 if wantType != haveType { 117 return r.valMismatch(wantType, haveType, want, have) 118 } 119 120 // If this is an array, then we've got to do some work to check equality. 121 if wantType == "array" { 122 return r.cmpJSONArrays(want, have) 123 } 124 125 // Atomic values are always strings 126 wantVal, ok := want["value"].(string) 127 if !ok { 128 return r.bug("'value' %v should be a string, but it is a %[1]T", want["value"]) 129 } 130 131 haveVal, ok := have["value"].(string) 132 if !ok { 133 return r.fail("Malformed output from your encoder: %T is not a string", have["value"]) 134 } 135 136 // Excepting floats and datetimes, other values can be compared as strings. 137 switch wantType { 138 case "float": 139 return r.cmpFloats(wantVal, haveVal) 140 case "datetime", "datetime-local", "date-local", "time-local": 141 return r.cmpAsDatetimes(wantType, wantVal, haveVal) 142 default: 143 return r.cmpAsStrings(wantVal, haveVal) 144 } 145 } 146 147 func (r Test) cmpAsStrings(want, have string) Test { 148 if want != have { 149 return r.fail("Values for key '%s' don't match:\n"+ 150 " Expected: %s\n"+ 151 " Your encoder: %s", 152 r.Key, want, have) 153 } 154 return r 155 } 156 157 func (r Test) cmpFloats(want, have string) Test { 158 // Special case for NaN, since NaN != NaN. 159 if strings.HasSuffix(want, "nan") || strings.HasSuffix(have, "nan") { 160 if want != have { 161 return r.fail("Values for key '%s' don't match:\n"+ 162 " Expected: %v\n"+ 163 " Your encoder: %v", 164 r.Key, want, have) 165 } 166 return r 167 } 168 169 wantF, err := strconv.ParseFloat(want, 64) 170 if err != nil { 171 return r.bug("Could not read '%s' as a float value for key '%s'", want, r.Key) 172 } 173 174 haveF, err := strconv.ParseFloat(have, 64) 175 if err != nil { 176 return r.fail("Malformed output from your encoder: key '%s' is not a float: '%s'", r.Key, have) 177 } 178 179 if wantF != haveF { 180 return r.fail("Values for key '%s' don't match:\n"+ 181 " Expected: %v\n"+ 182 " Your encoder: %v", 183 r.Key, wantF, haveF) 184 } 185 return r 186 } 187 188 var datetimeRepl = strings.NewReplacer( 189 " ", "T", 190 "t", "T", 191 "z", "Z") 192 193 var layouts = map[string]string{ 194 "datetime": time.RFC3339Nano, 195 "datetime-local": "2006-01-02T15:04:05.999999999", 196 "date-local": "2006-01-02", 197 "time-local": "15:04:05", 198 } 199 200 func (r Test) cmpAsDatetimes(kind, want, have string) Test { 201 layout, ok := layouts[kind] 202 if !ok { 203 panic("should never happen") 204 } 205 206 wantT, err := time.Parse(layout, datetimeRepl.Replace(want)) 207 if err != nil { 208 return r.bug("Could not read '%s' as a datetime value for key '%s'", want, r.Key) 209 } 210 211 haveT, err := time.Parse(layout, datetimeRepl.Replace(want)) 212 if err != nil { 213 return r.fail("Malformed output from your encoder: key '%s' is not a datetime: '%s'", r.Key, have) 214 } 215 if !wantT.Equal(haveT) { 216 return r.fail("Values for key '%s' don't match:\n"+ 217 " Expected: %v\n"+ 218 " Your encoder: %v", 219 r.Key, wantT, haveT) 220 } 221 return r 222 } 223 224 func (r Test) kjoin(key string) Test { 225 if len(r.Key) == 0 { 226 r.Key = key 227 } else { 228 r.Key += "." + key 229 } 230 return r 231 } 232 233 func isValue(m map[string]interface{}) bool { 234 if len(m) != 2 { 235 return false 236 } 237 if _, ok := m["type"]; !ok { 238 return false 239 } 240 if _, ok := m["value"]; !ok { 241 return false 242 } 243 return true 244 } 245 246 func (r Test) mismatch(wantType string, want, have interface{}) Test { 247 return r.fail("Key '%s' is not an %s but %[4]T:\n"+ 248 " Expected: %#[3]v\n"+ 249 " Your encoder: %#[4]v", 250 r.Key, wantType, want, have) 251 } 252 253 func (r Test) valMismatch(wantType, haveType string, want, have interface{}) Test { 254 return r.fail("Key '%s' is not an %s but %s:\n"+ 255 " Expected: %#[3]v\n"+ 256 " Your encoder: %#[4]v", 257 r.Key, wantType, want, have) 258 }