refactor(10.2-01): nest framework packages under modules
- Move remaining beach packages and embedded admin assets\n- Rewrite framework, example, build, and gate paths
This commit is contained in:
288
modules/tide/diff.go
Normal file
288
modules/tide/diff.go
Normal file
@@ -0,0 +1,288 @@
|
||||
package tide
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func compareBodies(want, got Response, step Step) []Diff {
|
||||
wantJSON := isJSONContentType(want.Headers)
|
||||
gotJSON := isJSONContentType(got.Headers)
|
||||
if wantJSON && gotJSON {
|
||||
wantRaw, wantDiffs := normalizeJSON([]byte(want.Body), step)
|
||||
gotRaw, gotDiffs := normalizeJSON([]byte(got.Body), step)
|
||||
diffs := append([]Diff{}, wantDiffs...)
|
||||
diffs = append(diffs, gotDiffs...)
|
||||
diffs = append(diffs, diffJSON(wantRaw, gotRaw)...)
|
||||
return diffs
|
||||
}
|
||||
return diffBytes([]byte(want.Body), []byte(got.Body))
|
||||
}
|
||||
|
||||
var globalCompareHeaders = []string{
|
||||
"Content-Type",
|
||||
"X-Total-Count",
|
||||
"Access-Control-Allow-Origin",
|
||||
"Access-Control-Allow-Credentials",
|
||||
"Access-Control-Allow-Headers",
|
||||
"Access-Control-Allow-Methods",
|
||||
"Access-Control-Expose-Headers",
|
||||
}
|
||||
|
||||
var extraCompareHeaders = []string{
|
||||
"Cache-Control",
|
||||
"Pragma",
|
||||
"WWW-Authenticate",
|
||||
"Content-Disposition",
|
||||
"Link",
|
||||
"Location",
|
||||
}
|
||||
|
||||
var neverCompareHeaders = []string{
|
||||
"Date",
|
||||
"Server",
|
||||
"X-Request-Id",
|
||||
"X-Request-ID",
|
||||
"X-Correlation-Id",
|
||||
}
|
||||
|
||||
func compareHeaders(want, got, extra map[string]string) []Diff {
|
||||
allow := map[string]bool{}
|
||||
for _, n := range globalCompareHeaders {
|
||||
allow[http.CanonicalHeaderKey(n)] = true
|
||||
}
|
||||
for _, n := range extraCompareHeaders {
|
||||
allow[http.CanonicalHeaderKey(n)] = true
|
||||
}
|
||||
for n := range extra {
|
||||
allow[http.CanonicalHeaderKey(n)] = true
|
||||
}
|
||||
for _, n := range neverCompareHeaders {
|
||||
delete(allow, http.CanonicalHeaderKey(n))
|
||||
}
|
||||
var diffs []Diff
|
||||
for k, wv := range want {
|
||||
ck := http.CanonicalHeaderKey(k)
|
||||
if !allow[ck] {
|
||||
continue
|
||||
}
|
||||
gv := headerValue(got, k)
|
||||
if gv == wv {
|
||||
continue
|
||||
}
|
||||
if gv == "" {
|
||||
gv = "<missing>"
|
||||
}
|
||||
diffs = append(diffs, Diff{Path: "header." + ck, Expected: wv, Actual: gv})
|
||||
}
|
||||
return diffs
|
||||
}
|
||||
|
||||
func isJSONContentType(headers map[string]string) bool {
|
||||
ct := headerValue(headers, "Content-Type")
|
||||
if ct == "" {
|
||||
return false
|
||||
}
|
||||
media, _, err := mime.ParseMediaType(ct)
|
||||
if err != nil {
|
||||
return strings.Contains(strings.ToLower(ct), "json")
|
||||
}
|
||||
return media == "application/json" || strings.HasSuffix(media, "+json")
|
||||
}
|
||||
|
||||
func headerValue(headers map[string]string, name string) string {
|
||||
if v, ok := headers[name]; ok {
|
||||
return v
|
||||
}
|
||||
for k, v := range headers {
|
||||
if strings.EqualFold(k, name) {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func diffJSON(want, got []byte) []Diff {
|
||||
wantVal, err := decodeJSON(want)
|
||||
if err != nil {
|
||||
return []Diff{{Path: "$", Expected: "valid JSON", Actual: err.Error()}}
|
||||
}
|
||||
gotVal, err := decodeJSON(got)
|
||||
if err != nil {
|
||||
return []Diff{{Path: "$", Expected: formatValue(wantVal), Actual: err.Error()}}
|
||||
}
|
||||
var diffs []Diff
|
||||
compareValue("$", wantVal, gotVal, &diffs)
|
||||
return diffs
|
||||
}
|
||||
|
||||
func decodeJSON(raw []byte) (any, error) {
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.UseNumber()
|
||||
var v any
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dec.More() {
|
||||
return nil, fmt.Errorf("trailing JSON after first value")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func compareValue(path string, want, got any, diffs *[]Diff) {
|
||||
switch w := want.(type) {
|
||||
case map[string]any:
|
||||
g, ok := got.(map[string]any)
|
||||
if !ok {
|
||||
*diffs = append(*diffs, Diff{Path: path, Expected: formatValue(want), Actual: formatValue(got)})
|
||||
return
|
||||
}
|
||||
for k, wv := range w {
|
||||
gv, exists := g[k]
|
||||
child := pathJoin(path, k)
|
||||
if !exists {
|
||||
*diffs = append(*diffs, Diff{Path: child, Expected: formatValue(wv), Actual: "<missing>"})
|
||||
continue
|
||||
}
|
||||
compareValue(child, wv, gv, diffs)
|
||||
}
|
||||
for k, gv := range g {
|
||||
if _, exists := w[k]; !exists {
|
||||
*diffs = append(*diffs, Diff{Path: pathJoin(path, k), Expected: "<missing>", Actual: formatValue(gv)})
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
g, ok := got.([]any)
|
||||
if !ok {
|
||||
*diffs = append(*diffs, Diff{Path: path, Expected: formatValue(want), Actual: formatValue(got)})
|
||||
return
|
||||
}
|
||||
if len(w) != len(g) {
|
||||
*diffs = append(*diffs, Diff{
|
||||
Path: path,
|
||||
Expected: fmt.Sprintf("array[%d]", len(w)),
|
||||
Actual: fmt.Sprintf("array[%d]", len(g)),
|
||||
})
|
||||
}
|
||||
n := min(len(w), len(g))
|
||||
for i := 0; i < n; i++ {
|
||||
compareValue(fmt.Sprintf("%s[%d]", path, i), w[i], g[i], diffs)
|
||||
}
|
||||
default:
|
||||
if !scalarEqual(want, got) {
|
||||
*diffs = append(*diffs, Diff{Path: path, Expected: formatValue(want), Actual: formatValue(got)})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func scalarEqual(want, got any) bool {
|
||||
if want == nil || got == nil {
|
||||
return want == nil && got == nil
|
||||
}
|
||||
switch w := want.(type) {
|
||||
case json.Number:
|
||||
g, ok := got.(json.Number)
|
||||
return ok && w == g
|
||||
case string:
|
||||
g, ok := got.(string)
|
||||
return ok && w == g
|
||||
case bool:
|
||||
g, ok := got.(bool)
|
||||
return ok && w == g
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func formatValue(v any) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "null"
|
||||
case json.Number:
|
||||
return "number " + string(t)
|
||||
case string:
|
||||
return strconv.Quote(t)
|
||||
case bool:
|
||||
return fmt.Sprintf("%v", t)
|
||||
case map[string]any:
|
||||
return "object"
|
||||
case []any:
|
||||
return fmt.Sprintf("array[%d]", len(t))
|
||||
default:
|
||||
return fmt.Sprintf("%v", t)
|
||||
}
|
||||
}
|
||||
|
||||
func pathJoin(parent, key string) string {
|
||||
if parent == "$" {
|
||||
return "$." + key
|
||||
}
|
||||
return parent + "." + key
|
||||
}
|
||||
|
||||
func diffBytes(want, got []byte) []Diff {
|
||||
n := min(len(want), len(got))
|
||||
off := n
|
||||
for i := 0; i < n; i++ {
|
||||
if want[i] != got[i] {
|
||||
off = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if off == n && len(want) == len(got) {
|
||||
return nil
|
||||
}
|
||||
return []Diff{{
|
||||
Path: fmt.Sprintf("body[%d]", off),
|
||||
Expected: printableWindow(want, off),
|
||||
Actual: printableWindow(got, off),
|
||||
Offset: off,
|
||||
Byte: true,
|
||||
}}
|
||||
}
|
||||
|
||||
func printableWindow(b []byte, off int) string {
|
||||
if len(b) == 0 {
|
||||
return `""`
|
||||
}
|
||||
start := off - 8
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
end := off + 8
|
||||
if end > len(b) {
|
||||
end = len(b)
|
||||
}
|
||||
return quotePrintable(b[start:end])
|
||||
}
|
||||
|
||||
func quotePrintable(b []byte) string {
|
||||
var buf strings.Builder
|
||||
buf.WriteByte('"')
|
||||
for i := 0; i < len(b); {
|
||||
r, size := utf8.DecodeRune(b[i:])
|
||||
if r == utf8.RuneError && size == 1 {
|
||||
fmt.Fprintf(&buf, "\\x%02x", b[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if r == '\\' || r == '"' {
|
||||
buf.WriteByte('\\')
|
||||
buf.WriteRune(r)
|
||||
} else if unicode.IsPrint(r) {
|
||||
buf.WriteRune(r)
|
||||
} else {
|
||||
fmt.Fprintf(&buf, "\\u%04x", r)
|
||||
}
|
||||
i += size
|
||||
}
|
||||
buf.WriteByte('"')
|
||||
return buf.String()
|
||||
}
|
||||
Reference in New Issue
Block a user