Files
Jan Novak 2b15280d03
All checks were successful
Deploy to K8s / deploy (push) Successful in 7s
fix(go): exclude /api/version from parity diff — identity, not contract
/api/version returns each binary's own tag/commit/build_date, which
differs by design between independently built backends. Diffing it
always produces a false positive. Drop it from allRoutes; the route
remains reachable via `make parity ARGS="-route /api/version"`.

Also remove the vestigial `build_meta` allowlist entry (Python returns
the build dict as the top-level response body, not nested under
build_meta, so the scrubber never matched anything).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 23:23:38 +02:00

134 lines
3.3 KiB
Go

package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
// /api/version is intentionally excluded — it returns each binary's own build
// identity (tag/commit/build_date), which differs by design between independently
// built backends. Pass -route /api/version to inspect it manually.
var allRoutes = []string{"/api/adults", "/api/juniors", "/api/payments"}
// defaultAllowlist holds dotted key paths to strip before diffing.
// render_time.total is forward-compatible insurance: today it lives in the Jinja
// template context only (app.py inject_render_time) and is logged via
// middleware/timer.go on the Go side, so it isn't in any JSON response. If either
// side ever surfaces it under a render_time envelope, the scrubber handles it.
var defaultAllowlist = []string{"render_time.total"}
func main() {
pyURL := flag.String("py", "http://localhost:5001", "Python backend base URL")
goURL := flag.String("go", "http://localhost:8080", "Go backend base URL")
route := flag.String("route", "", "single route to diff, e.g. /api/adults (default: all)")
timeout := flag.Duration("timeout", 30*time.Second, "per-request HTTP timeout")
flag.Parse()
client := &http.Client{Timeout: *timeout}
targets := allRoutes
if *route != "" {
targets = []string{*route}
}
matched, diffs, errs := 0, 0, 0
for _, r := range targets {
pyMap, err1 := fetch(client, *pyURL+r)
goMap, err2 := fetch(client, *goURL+r)
if err1 != nil || err2 != nil {
fmt.Printf("=== %s ===\n", r)
if err1 != nil {
fmt.Printf("ERROR (py): %v\n", err1)
}
if err2 != nil {
fmt.Printf("ERROR (go): %v\n", err2)
}
fmt.Println()
errs++
continue
}
scrub(pyMap, defaultAllowlist)
scrub(goMap, defaultAllowlist)
diff := cmp.Diff(pyMap, goMap, cmpopts.EquateEmpty())
if diff == "" {
fmt.Printf("=== %s ===\nOK\n\n", r)
matched++
} else {
fmt.Printf("=== %s ===\n%s\n", r, diff)
diffs++
}
}
total := len(targets)
fmt.Printf("parity: %d/%d routes match", matched, total)
if diffs > 0 {
fmt.Printf(", %d diff", diffs)
if diffs > 1 {
fmt.Print("s")
}
}
if errs > 0 {
fmt.Printf(", %d error", errs)
if errs > 1 {
fmt.Print("s")
}
}
fmt.Println()
if errs > 0 {
os.Exit(2)
}
if diffs > 0 {
os.Exit(1)
}
}
func fetch(client *http.Client, url string) (map[string]any, error) {
resp, err := client.Get(url) //nolint:noctx
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read body: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
var m map[string]any
if err := json.Unmarshal(body, &m); err != nil {
return nil, fmt.Errorf("decode JSON: %w", err)
}
return m, nil
}
// scrub removes keys from m whose dotted paths appear in paths.
// A bare segment (no dot) deletes a top-level key.
// A two-segment path "parent.child" deletes child from m["parent"] if it is a map.
func scrub(m map[string]any, paths []string) {
for _, path := range paths {
parts := strings.SplitN(path, ".", 2)
if len(parts) == 1 {
delete(m, parts[0])
} else {
if child, ok := m[parts[0]].(map[string]any); ok {
delete(child, parts[1])
}
}
}
}