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  }