From 97ede56352f520c931269049b9749d141a5a053f Mon Sep 17 00:00:00 2001 From: Anju Date: Mon, 6 Apr 2026 06:14:58 -0400 Subject: [PATCH 1/5] chore: add grpc-protoscope sample app for Keploy gRPC field-ordering bug --- grpc-protoscope/README.md | 349 ++++++++++++++++ grpc-protoscope/client/main.go | 68 +++ grpc-protoscope/go.mod | 65 +++ grpc-protoscope/go.sum | 156 +++++++ grpc-protoscope/keploy.yml | 96 +++++ grpc-protoscope/proto/search.proto | 43 ++ grpc-protoscope/searchpb/search.pb.go | 460 +++++++++++++++++++++ grpc-protoscope/searchpb/search_grpc.pb.go | 121 ++++++ grpc-protoscope/server/main.go | 158 +++++++ 9 files changed, 1516 insertions(+) create mode 100644 grpc-protoscope/README.md create mode 100644 grpc-protoscope/client/main.go create mode 100644 grpc-protoscope/go.mod create mode 100644 grpc-protoscope/go.sum create mode 100755 grpc-protoscope/keploy.yml create mode 100644 grpc-protoscope/proto/search.proto create mode 100644 grpc-protoscope/searchpb/search.pb.go create mode 100644 grpc-protoscope/searchpb/search_grpc.pb.go create mode 100644 grpc-protoscope/server/main.go diff --git a/grpc-protoscope/README.md b/grpc-protoscope/README.md new file mode 100644 index 00000000..feabcfd1 --- /dev/null +++ b/grpc-protoscope/README.md @@ -0,0 +1,349 @@ +# grpc-protoscope — Reproducing the Keploy gRPC Field-Ordering Bug + +## Table of Contents + +1. [The Issue Reported by the Client](#1-the-issue-reported-by-the-client) +2. [Root Cause Analysis](#2-root-cause-analysis) +3. [About This Sample Application](#3-about-this-sample-application) +4. [How to Run](#4-how-to-run) +5. [Reproducing the Bug with Keploy](#5-reproducing-the-bug-with-keploy) +6. [Files in This Repository](#6-files-in-this-repository) + +--- + +## 1. The Issue Reported by the Client + +A user reported that Keploy was **failing gRPC tests** even though the recorded and replayed responses had **identical structure and values**. The only difference was the **order of individual fields** inside nested protobuf sub-messages. + +### Client's Exact Input + +```yaml +expected: |- + 1: 67.0i32 # 0x42860000i32 + 4: {"{\"hits\":[{\"_index\":\"pvid_search_products_v4\",\"_score\":15100000000000000000,\"_sou" + "rce\":{\"_rankingInfo\":{\"typosPresent\":true,\"numberOfWordsMatched\":1}},\"match_type" + "\":\"Other\",\"attributes\":{\"subThemes\":null},\"_id\":\"4f30407c-6a3c-4a4e-8a3d-652217d" + "4b6cb_d67c25f8-3adb-40c1-9113-b46d54a6e8aa\",\"trimming_meta\":{\"trimming_type\":\"L3" + "\"}}]}"} + 8: 0 + 9: { 3: { 1: { 2: {2: 0.0} # 0x0i64 + 1: {"candidateCnt"}} + 1: { 2: {3: {"OVS"}} + 1: {"type"}}} + 2: { 1: { 2: {2: 1.0} # 0x3ff0000000000000i64 + 1: {"candidateCnt"}} + 1: { 2: {2: 1.0} # 0x3ff0000000000000i64 + 1: {"resultCnt"}}}} +actual: |- + 1: 67.0i32 # 0x42860000i32 + 4: {"{\"hits\":[{\"_index\":\"pvid_search_products_v4\",\"_score\":15100000000000000000,\"_sou" + "rce\":{\"_rankingInfo\":{\"typosPresent\":true,\"numberOfWordsMatched\":1}},\"match_type" + "\":\"Other\",\"attributes\":{\"subThemes\":null},\"_id\":\"4f30407c-6a3c-4a4e-8a3d-652217d" + "4b6cb_d67c25f8-3adb-40c1-9113-b46d54a6e8aa\",\"trimming_meta\":{\"trimming_type\":\"L3" + "\"}}]}"} + 8: 0 + 9: { 3: { 1: { 2: {3: {"OVS"}} + 1: {"type"}} + 1: { 2: {2: 0.0} # 0x0i64 + 1: {"candidateCnt"}}} + 2: { 1: { 2: {2: 1.0} # 0x3ff0000000000000i64 + 1: {"candidateCnt"}} + 1: { 2: {2: 1.0} # 0x3ff0000000000000i64 + 1: {"resultCnt"}}}} +``` + +### The Failure Classification + +```yaml +failure_info: + risk: HIGH + category: + - SCHEMA_BROKEN +``` + +### What's Actually Different? + +If you look closely at field `9.3` (the availability facet bucket), the **same two sub-messages** appear but in **reversed order**: + +**Expected** (recorded): +``` +9: { 3: { 1: { 2: {2: 0.0} # candidateCnt (numeric=0.0) + 1: {"candidateCnt"}} + 1: { 2: {3: {"OVS"}} # type (text="OVS") + 1: {"type"}}} +``` + +**Actual** (replayed): +``` +9: { 3: { 1: { 2: {3: {"OVS"}} # type (text="OVS") — now first + 1: {"type"}} + 1: { 2: {2: 0.0} # 0x0i64 # candidateCnt — now second + 1: {"candidateCnt"}}} +``` + +The values are **identical**: `candidateCnt = 0.0` and `type = "OVS"`. Only the wire serialization order changed — which is **perfectly valid** in protobuf, where `repeated` fields and map entries have no guaranteed order. + +--- + +## 2. Root Cause Analysis + +The bug lives in **three interacting layers** in Keploy's codebase. + +### Layer 1: Protoscope Assigns Position-Dependent Indentation + +Keploy uses the [`protoscope`](https://github.com/protocolbuffers/protoscope) library to convert raw protobuf wire bytes into human-readable text. The protoscope renderer assigns **indentation based on position**, not content. + +When a sub-message is small enough, protoscope inlines it on the same line as the parent `{`: + +``` +9: { 3: { 1: { 2: {2: 0.0} # 0x0i64 ← 6 spaces indent (inline) + 1: {"candidateCnt"}} ← 2 spaces indent (next line) +``` + +The **first** sub-message gets deeper inline indentation (it continues on the same line as `{`). The **second** sub-message starts on a new line with less indentation. So when the wire order flips, the same content gets **different leading whitespace**. + +### Layer 2: Canonicalization Sorts With Indentation Included + +The canonicalization function in `pkg/matcher/grpc/canonical.go` (`CanonicalizeTopLevelBlocks`) is designed to make protoscope text order-insensitive. It: + +1. Splits text into "top-level field blocks" (lines starting with `\d+:`) +2. Recursively canonicalizes the content inside each `{...}` block +3. **Sorts blocks lexicographically** +4. Joins them back + +The problem: `normalizeWhitespace()` only trims **trailing** whitespace and collapses blank lines. It does **not** strip or normalize **leading** indentation. So when `sort.Strings(blocks)` runs, the sort order is determined by the leading spaces, not the content: + +``` +" 2: {2: 0.0}" sorts before " 1: {\"candidateCnt\"}" +``` + +because `" "` (6 spaces) sorts before `" 1"` (2 spaces then `1`) in ASCII. But when the wire order flips, the indentation flips too, producing a different sorted result — even though the actual protobuf data is identical. + +### Layer 3: Non-JSON Mismatch Is Classified as SCHEMA_BROKEN + +In `pkg/matcher/grpc/match.go`, when the two canonicalized strings don't match: + +```go +if !decodedDataNormal { + if json.Valid([]byte(expectedDecodedData)) && json.Valid([]byte(actualDecodedData)) { + // JSON comparison with failure assessment + } else { + // non-JSON payload mismatch → Broken + currentRisk = models.High + currentCategories = append(currentCategories, models.SchemaBroken) + } +} +``` + +Since protoscope text is **not valid JSON**, it falls into the `else` branch, which unconditionally classifies the failure as `HIGH` risk / `SCHEMA_BROKEN` — the most alarming category. + +### Summary of the Chain + +``` +Wire bytes have different field order (valid in protobuf) + → protoscope assigns different indentation + → canonicalization sorts by indentation instead of content + → canonicalized strings differ + → classified as SCHEMA_BROKEN / HIGH risk +``` + +### The Fix + +The fix needs to **strip leading whitespace from each block before sorting** in `canonicalizeRecursive`: + +```go +// Before sorting, strip leading whitespace so that +// sort order depends on content, not position-dependent indentation. +for i := range blocks { + blocks[i] = strings.TrimLeft(blocks[i], " \t") +} +sort.Strings(blocks) +``` + +--- + +## 3. About This Sample Application + +This is a minimal Go gRPC client-server app that reproduces the exact conditions from the bug report. + +### Why a Normal gRPC Server Isn't Enough + +Go's standard `proto.Marshal()` serializes `repeated` fields and `map` entries in a **deterministic** (sorted) order. So a normal gRPC server would produce identical wire bytes on every call — the bug would never trigger. + +### What This Server Does Differently + +The server uses **raw wire encoding** via `google.golang.org/protobuf/encoding/protowire` to manually construct the protobuf response bytes with `rand.Shuffle()` on the repeated field entries: + +```go +availEntries := [][]byte{ + buildFacetEntry("candidateCnt", &zero, nil), + buildFacetEntry("type", nil, &ovs), +} +rand.Shuffle(len(availEntries), func(i, j int) { + availEntries[i], availEntries[j] = availEntries[j], availEntries[i] +}) +``` + +A `rawCodec` gRPC codec passes these pre-built bytes straight to the wire without re-marshaling, preserving the randomized field ordering. + +### Proto Schema + +```protobuf +message FacetValue { + oneof value { + double numeric = 2; + string text = 3; + } +} + +message FacetEntry { + string name = 1; + FacetValue data = 2; +} + +message FacetBucket { + repeated FacetEntry entries = 1; +} + +message FacetInfo { + FacetBucket pricing = 2; + FacetBucket availability = 3; +} + +message SearchResponse { + float score = 1; + string hits_json = 4; + int32 total = 8; + FacetInfo facets = 9; +} +``` + +The field numbers (`1`, `4`, `8`, `9`) and nesting structure match the bug report exactly. + +### Example: Recorded Test Case (Protoscope Format) + +When Keploy records this server's response, the YAML test case looks like this: + +```yaml +decoded_data: | + 1: 67.0i32 # 0x42860000i32 + 4: { + "{\"hits\":[{\"_index\":\"pvid_search_products_v4\",..." + } + 8: 0 + 9: { + 3: { + 1: { + 1: {"type"} + 2: {3: {"OVS"}} + } + 1: { + 1: {"candidateCnt"} + 2: {2: 0.0} # 0x0i64 + } + } + 2: { + 1: { + 1: {"candidateCnt"} + 2: {2: 1.0} # 0x3ff0000000000000i64 + } + 1: { + 1: {"resultCnt"} + 2: {2: 1.0} # 0x3ff0000000000000i64 + } + } + } +``` + +On the next run (test mode), the `rand.Shuffle` may flip the inner field order, producing different protoscope indentation — triggering the `SCHEMA_BROKEN` false positive. + +--- + +## 4. How to Run + +### Prerequisites + +- Go 1.21+ +- `protoc` compiler (only needed if modifying the `.proto` file) + +### Run without Keploy + +```bash +# Terminal 1 — start the server +cd /home/anju/grpc-protoscope +go run ./server/ + +# Terminal 2 — call it (run multiple times to see different field orderings) +cd /home/anju/grpc-protoscope +go run ./client/ +go run ./client/ +go run ./client/ +``` + +You'll see the facet entries printed in different orders across calls. + +--- + +## 5. Reproducing the Bug with Keploy + +### Step 1: Build Keploy from source (if needed) + +```bash +cd /home/anju/keploy +go build -ldflags="-X main.apiServerURI=https://api.keploy.io" -o keploy +``` + +### Step 2: Record a test case + +```bash +cd /home/anju/grpc-protoscope + +# Start recording +/home/anju/keploy/keploy record -c "go run ./server/" +``` + +In another terminal, trigger the gRPC call: + +```bash +cd /home/anju/grpc-protoscope +go run ./client/ +``` + +Then press `Ctrl+C` in the recording terminal. Keploy saves the test case in `keploy/test-set-0/tests/test-1.yaml`. + +### Step 3: Replay (test mode) + +```bash +cd /home/anju/grpc-protoscope +/home/anju/keploy/keploy test -c "go run ./server/" +``` + +**Expected result:** Because `rand.Shuffle` randomizes field ordering each time, ~50% of test runs will produce a different wire order than the recording, triggering: + +``` +failure_info: + risk: HIGH + category: + - SCHEMA_BROKEN +``` + +If the test passes (same random order happened to match), delete the `keploy/` folder and repeat steps 2–3. + +--- + +## 6. Files in This Repository + +``` +grpc-protoscope/ +├── README.md ← This file +├── proto/search.proto ← Protobuf schema matching the bug report structure +├── searchpb/ ← Generated Go protobuf/gRPC code +│ ├── search.pb.go +│ └── search_grpc.pb.go +├── server/main.go ← gRPC server with randomized wire field ordering +├── client/main.go ← gRPC client that calls the Search RPC +├── go.mod +├── go.sum +└── keploy/ ← Keploy test artifacts (created after recording) + └── test-set-0/tests/test-1.yaml +``` diff --git a/grpc-protoscope/client/main.go b/grpc-protoscope/client/main.go new file mode 100644 index 00000000..7d8ac9c0 --- /dev/null +++ b/grpc-protoscope/client/main.go @@ -0,0 +1,68 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + pb "zepto-grpc/searchpb" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func main() { + conn, err := grpc.NewClient("localhost:50051", + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + log.Fatalf("failed to connect: %v", err) + } + defer conn.Close() + + client := pb.NewSearchServiceClient(conn) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + resp, err := client.Search(ctx, &pb.SearchRequest{Query: "shoes"}) + if err != nil { + log.Fatalf("Search failed: %v", err) + } + + fmt.Printf("Score: %.1f\n", resp.GetScore()) + fmt.Printf("Total: %d\n", resp.GetTotal()) + fmt.Printf("HitsJSON: %.80s...\n", resp.GetHitsJson()) + fmt.Println("Facets:") + if f := resp.GetFacets(); f != nil { + if a := f.GetAvailability(); a != nil { + fmt.Println(" availability:") + for _, e := range a.GetEntries() { + fmt.Printf(" %s → ", e.GetName()) + if d := e.GetData(); d != nil { + switch v := d.GetValue().(type) { + case *pb.FacetValue_Numeric: + fmt.Printf("%.1f\n", v.Numeric) + case *pb.FacetValue_Text: + fmt.Printf("%s\n", v.Text) + } + } + } + } + if p := f.GetPricing(); p != nil { + fmt.Println(" pricing:") + for _, e := range p.GetEntries() { + fmt.Printf(" %s → ", e.GetName()) + if d := e.GetData(); d != nil { + switch v := d.GetValue().(type) { + case *pb.FacetValue_Numeric: + fmt.Printf("%.1f\n", v.Numeric) + case *pb.FacetValue_Text: + fmt.Printf("%s\n", v.Text) + } + } + } + } + } +} diff --git a/grpc-protoscope/go.mod b/grpc-protoscope/go.mod new file mode 100644 index 00000000..f7e3756d --- /dev/null +++ b/grpc-protoscope/go.mod @@ -0,0 +1,65 @@ +module zepto-grpc + +go 1.26.0 + +require ( + github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9 + go.keploy.io/server/v3 v3.0.0-00010101000000-000000000000 + google.golang.org/grpc v1.80.0 + google.golang.org/protobuf v1.36.11 +) + +require ( + github.com/7sDream/geko v0.1.1 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/bufbuild/protocompile v0.14.1 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/creack/pty v1.1.24 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/getsentry/sentry-go v0.28.1 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/k0kubun/pp/v3 v3.2.0 // indirect + github.com/keploy/jsonDiff v1.0.8 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect + github.com/olekukonko/errors v1.1.0 // indirect + github.com/olekukonko/ll v0.1.1 // indirect + github.com/olekukonko/tablewriter v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/cobra v1.10.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.2.0 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + go.mongodb.org/mongo-driver/v2 v2.4.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + helm.sh/helm/v3 v3.18.3 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + +replace go.keploy.io/server/v3 => /home/anju/keploy diff --git a/grpc-protoscope/go.sum b/grpc-protoscope/go.sum new file mode 100644 index 00000000..8b2ac71c --- /dev/null +++ b/grpc-protoscope/go.sum @@ -0,0 +1,156 @@ +github.com/7sDream/geko v0.1.1 h1:Ms9RVcUJDPsUuRlk3T3sGmSGAMuttqh2/3okZ4yWUpU= +github.com/7sDream/geko v0.1.1/go.mod h1:NcSQXSUpcoqNP8BkOLP2TsinxRVjnMkDYCKl9L1Czxk= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k= +github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/k0kubun/pp/v3 v3.2.0 h1:h33hNTZ9nVFNP3u2Fsgz8JXiF5JINoZfFq4SvKJwNcs= +github.com/k0kubun/pp/v3 v3.2.0/go.mod h1:ODtJQbQcIRfAD3N+theGCV1m/CBxweERz2dapdz1EwA= +github.com/keploy/jsonDiff v1.0.8 h1:B/75cfLNZ7kayAjF1Jox70Iwa/nwCjBYNOuSt+RHH5A= +github.com/keploy/jsonDiff v1.0.8/go.mod h1:wUuLbVs3Oe3mIQ61C7G88bppP//ArLtoDU0S9Awwv+s= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.1.1 h1:9Dfeed5/Mgaxb9lHRAftLK9pVfYETvHn+If6lywVhJc= +github.com/olekukonko/ll v0.1.1/go.mod h1:2dJo+hYZcJMLMbKwHEWvxCUbAOLc/CXWS9noET22Mdo= +github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY= +github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9 h1:arwj11zP0yJIxIRiDn22E0H8PxfF7TsTrc2wIPFIsf4= +github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9/go.mod h1:SKZx6stCn03JN3BOWTwvVIO2ajMkb/zQdTceXYhKw/4= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +go.mongodb.org/mongo-driver/v2 v2.4.1 h1:hGDMngUao03OVQ6sgV5csk+RWOIkF+CuLsTPobNMGNI= +go.mongodb.org/mongo-driver/v2 v2.4.1/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +helm.sh/helm/v3 v3.18.3 h1:+cvyGKgs7Jt7BN3Klmb4SsG4IkVpA7GAZVGvMz6VO4I= +helm.sh/helm/v3 v3.18.3/go.mod h1:wUc4n3txYBocM7S9RjTeZBN9T/b5MjffpcSsWEjSIpw= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/grpc-protoscope/keploy.yml b/grpc-protoscope/keploy.yml new file mode 100755 index 00000000..c13b9c0c --- /dev/null +++ b/grpc-protoscope/keploy.yml @@ -0,0 +1,96 @@ +# Generated by Keploy (3-dev) +path: "" +appName: zepto-grpc +appId: 0 +command: go run ./server/ +templatize: + testSets: [] +port: 0 +e2e: false +dnsPort: 26789 +proxyPort: 16789 +incomingProxyPort: 36789 +debug: false +disableTele: false +disableANSI: false +containerName: "" +networkName: "" +buildDelay: 30 +test: + selectedTests: {} + globalNoise: + global: {} + test-sets: {} + replaceWith: + global: + url: {} + test-sets: {} + delay: 5 + host: localhost + port: 0 + grpcPort: 0 + apiTimeout: 5 + skipCoverage: false + coverageReportPath: "" + ignoreOrdering: true + mongoPassword: default@123 + language: "" + removeUnusedMocks: false + fallBackOnMiss: false + jacocoAgentPath: "" + basePath: "" + mocking: true + ignoredTests: {} + disableLineCoverage: false + disableMockUpload: true + useLocalMock: false + updateTemplate: false + mustPass: false + maxFailAttempts: 5 + maxFlakyChecks: 1 + protoFile: "" + protoDir: "" + protoInclude: [] + compareAll: false + schemaMatch: false + updateTestMapping: false +record: + filters: [] + basePath: "" + recordTimer: 0s + metadata: "" + sync: false + enableSampling: 0 + globalPassthrough: false + tlsPrivateKeyPath: "" +report: + selectedTestSets: {} + showFullBody: false + reportPath: "" + summary: false + testCaseIDs: [] + format: "" +disableMapping: true +retryPassing: false +configPath: "" +bypassRules: [] +generateGithubActions: false +keployContainer: keploy-v3 +keployNetwork: keploy-network +cmdType: native +contract: + services: [] + tests: [] + path: "" + download: false + generate: false + driven: consumer + mappings: + servicesMapping: {} + self: s1 +inCi: false +serverPort: 0 +mockDownload: + registryIds: [] + +# Visit [https://keploy.io/docs/running-keploy/configuration-file/] to learn about using keploy through configration file. diff --git a/grpc-protoscope/proto/search.proto b/grpc-protoscope/proto/search.proto new file mode 100644 index 00000000..1efd4ebc --- /dev/null +++ b/grpc-protoscope/proto/search.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; + +package search; + +option go_package = "./searchpb"; + +service SearchService { + rpc Search(SearchRequest) returns (SearchResponse); +} + +message SearchRequest { + string query = 1; +} + +// Minimal nested structure — small enough that protoscope +// will inline the sub-messages, producing position-dependent indentation. +message FacetValue { + oneof value { + double numeric = 2; + string text = 3; + } +} + +message FacetEntry { + string name = 1; + FacetValue data = 2; +} + +message FacetBucket { + repeated FacetEntry entries = 1; +} + +message FacetInfo { + FacetBucket pricing = 2; + FacetBucket availability = 3; +} + +message SearchResponse { + float score = 1; + string hits_json = 4; + int32 total = 8; + FacetInfo facets = 9; +} diff --git a/grpc-protoscope/searchpb/search.pb.go b/grpc-protoscope/searchpb/search.pb.go new file mode 100644 index 00000000..b49a99d3 --- /dev/null +++ b/grpc-protoscope/searchpb/search.pb.go @@ -0,0 +1,460 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.21.12 +// source: proto/search.proto + +package searchpb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SearchRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SearchRequest) Reset() { + *x = SearchRequest{} + mi := &file_proto_search_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SearchRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchRequest) ProtoMessage() {} + +func (x *SearchRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_search_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchRequest.ProtoReflect.Descriptor instead. +func (*SearchRequest) Descriptor() ([]byte, []int) { + return file_proto_search_proto_rawDescGZIP(), []int{0} +} + +func (x *SearchRequest) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +// Minimal nested structure — small enough that protoscope +// will inline the sub-messages, producing position-dependent indentation. +type FacetValue struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Value: + // + // *FacetValue_Numeric + // *FacetValue_Text + Value isFacetValue_Value `protobuf_oneof:"value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FacetValue) Reset() { + *x = FacetValue{} + mi := &file_proto_search_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FacetValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FacetValue) ProtoMessage() {} + +func (x *FacetValue) ProtoReflect() protoreflect.Message { + mi := &file_proto_search_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FacetValue.ProtoReflect.Descriptor instead. +func (*FacetValue) Descriptor() ([]byte, []int) { + return file_proto_search_proto_rawDescGZIP(), []int{1} +} + +func (x *FacetValue) GetValue() isFacetValue_Value { + if x != nil { + return x.Value + } + return nil +} + +func (x *FacetValue) GetNumeric() float64 { + if x != nil { + if x, ok := x.Value.(*FacetValue_Numeric); ok { + return x.Numeric + } + } + return 0 +} + +func (x *FacetValue) GetText() string { + if x != nil { + if x, ok := x.Value.(*FacetValue_Text); ok { + return x.Text + } + } + return "" +} + +type isFacetValue_Value interface { + isFacetValue_Value() +} + +type FacetValue_Numeric struct { + Numeric float64 `protobuf:"fixed64,2,opt,name=numeric,proto3,oneof"` +} + +type FacetValue_Text struct { + Text string `protobuf:"bytes,3,opt,name=text,proto3,oneof"` +} + +func (*FacetValue_Numeric) isFacetValue_Value() {} + +func (*FacetValue_Text) isFacetValue_Value() {} + +type FacetEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Data *FacetValue `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FacetEntry) Reset() { + *x = FacetEntry{} + mi := &file_proto_search_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FacetEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FacetEntry) ProtoMessage() {} + +func (x *FacetEntry) ProtoReflect() protoreflect.Message { + mi := &file_proto_search_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FacetEntry.ProtoReflect.Descriptor instead. +func (*FacetEntry) Descriptor() ([]byte, []int) { + return file_proto_search_proto_rawDescGZIP(), []int{2} +} + +func (x *FacetEntry) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *FacetEntry) GetData() *FacetValue { + if x != nil { + return x.Data + } + return nil +} + +type FacetBucket struct { + state protoimpl.MessageState `protogen:"open.v1"` + Entries []*FacetEntry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FacetBucket) Reset() { + *x = FacetBucket{} + mi := &file_proto_search_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FacetBucket) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FacetBucket) ProtoMessage() {} + +func (x *FacetBucket) ProtoReflect() protoreflect.Message { + mi := &file_proto_search_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FacetBucket.ProtoReflect.Descriptor instead. +func (*FacetBucket) Descriptor() ([]byte, []int) { + return file_proto_search_proto_rawDescGZIP(), []int{3} +} + +func (x *FacetBucket) GetEntries() []*FacetEntry { + if x != nil { + return x.Entries + } + return nil +} + +type FacetInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Pricing *FacetBucket `protobuf:"bytes,2,opt,name=pricing,proto3" json:"pricing,omitempty"` + Availability *FacetBucket `protobuf:"bytes,3,opt,name=availability,proto3" json:"availability,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FacetInfo) Reset() { + *x = FacetInfo{} + mi := &file_proto_search_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FacetInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FacetInfo) ProtoMessage() {} + +func (x *FacetInfo) ProtoReflect() protoreflect.Message { + mi := &file_proto_search_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FacetInfo.ProtoReflect.Descriptor instead. +func (*FacetInfo) Descriptor() ([]byte, []int) { + return file_proto_search_proto_rawDescGZIP(), []int{4} +} + +func (x *FacetInfo) GetPricing() *FacetBucket { + if x != nil { + return x.Pricing + } + return nil +} + +func (x *FacetInfo) GetAvailability() *FacetBucket { + if x != nil { + return x.Availability + } + return nil +} + +type SearchResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Score float32 `protobuf:"fixed32,1,opt,name=score,proto3" json:"score,omitempty"` + HitsJson string `protobuf:"bytes,4,opt,name=hits_json,json=hitsJson,proto3" json:"hits_json,omitempty"` + Total int32 `protobuf:"varint,8,opt,name=total,proto3" json:"total,omitempty"` + Facets *FacetInfo `protobuf:"bytes,9,opt,name=facets,proto3" json:"facets,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SearchResponse) Reset() { + *x = SearchResponse{} + mi := &file_proto_search_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SearchResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchResponse) ProtoMessage() {} + +func (x *SearchResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_search_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchResponse.ProtoReflect.Descriptor instead. +func (*SearchResponse) Descriptor() ([]byte, []int) { + return file_proto_search_proto_rawDescGZIP(), []int{5} +} + +func (x *SearchResponse) GetScore() float32 { + if x != nil { + return x.Score + } + return 0 +} + +func (x *SearchResponse) GetHitsJson() string { + if x != nil { + return x.HitsJson + } + return "" +} + +func (x *SearchResponse) GetTotal() int32 { + if x != nil { + return x.Total + } + return 0 +} + +func (x *SearchResponse) GetFacets() *FacetInfo { + if x != nil { + return x.Facets + } + return nil +} + +var File_proto_search_proto protoreflect.FileDescriptor + +const file_proto_search_proto_rawDesc = "" + + "\n" + + "\x12proto/search.proto\x12\x06search\"%\n" + + "\rSearchRequest\x12\x14\n" + + "\x05query\x18\x01 \x01(\tR\x05query\"G\n" + + "\n" + + "FacetValue\x12\x1a\n" + + "\anumeric\x18\x02 \x01(\x01H\x00R\anumeric\x12\x14\n" + + "\x04text\x18\x03 \x01(\tH\x00R\x04textB\a\n" + + "\x05value\"H\n" + + "\n" + + "FacetEntry\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12&\n" + + "\x04data\x18\x02 \x01(\v2\x12.search.FacetValueR\x04data\";\n" + + "\vFacetBucket\x12,\n" + + "\aentries\x18\x01 \x03(\v2\x12.search.FacetEntryR\aentries\"s\n" + + "\tFacetInfo\x12-\n" + + "\apricing\x18\x02 \x01(\v2\x13.search.FacetBucketR\apricing\x127\n" + + "\favailability\x18\x03 \x01(\v2\x13.search.FacetBucketR\favailability\"\x84\x01\n" + + "\x0eSearchResponse\x12\x14\n" + + "\x05score\x18\x01 \x01(\x02R\x05score\x12\x1b\n" + + "\thits_json\x18\x04 \x01(\tR\bhitsJson\x12\x14\n" + + "\x05total\x18\b \x01(\x05R\x05total\x12)\n" + + "\x06facets\x18\t \x01(\v2\x11.search.FacetInfoR\x06facets2H\n" + + "\rSearchService\x127\n" + + "\x06Search\x12\x15.search.SearchRequest\x1a\x16.search.SearchResponseB\fZ\n" + + "./searchpbb\x06proto3" + +var ( + file_proto_search_proto_rawDescOnce sync.Once + file_proto_search_proto_rawDescData []byte +) + +func file_proto_search_proto_rawDescGZIP() []byte { + file_proto_search_proto_rawDescOnce.Do(func() { + file_proto_search_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_search_proto_rawDesc), len(file_proto_search_proto_rawDesc))) + }) + return file_proto_search_proto_rawDescData +} + +var file_proto_search_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_proto_search_proto_goTypes = []any{ + (*SearchRequest)(nil), // 0: search.SearchRequest + (*FacetValue)(nil), // 1: search.FacetValue + (*FacetEntry)(nil), // 2: search.FacetEntry + (*FacetBucket)(nil), // 3: search.FacetBucket + (*FacetInfo)(nil), // 4: search.FacetInfo + (*SearchResponse)(nil), // 5: search.SearchResponse +} +var file_proto_search_proto_depIdxs = []int32{ + 1, // 0: search.FacetEntry.data:type_name -> search.FacetValue + 2, // 1: search.FacetBucket.entries:type_name -> search.FacetEntry + 3, // 2: search.FacetInfo.pricing:type_name -> search.FacetBucket + 3, // 3: search.FacetInfo.availability:type_name -> search.FacetBucket + 4, // 4: search.SearchResponse.facets:type_name -> search.FacetInfo + 0, // 5: search.SearchService.Search:input_type -> search.SearchRequest + 5, // 6: search.SearchService.Search:output_type -> search.SearchResponse + 6, // [6:7] is the sub-list for method output_type + 5, // [5:6] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_proto_search_proto_init() } +func file_proto_search_proto_init() { + if File_proto_search_proto != nil { + return + } + file_proto_search_proto_msgTypes[1].OneofWrappers = []any{ + (*FacetValue_Numeric)(nil), + (*FacetValue_Text)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_search_proto_rawDesc), len(file_proto_search_proto_rawDesc)), + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_search_proto_goTypes, + DependencyIndexes: file_proto_search_proto_depIdxs, + MessageInfos: file_proto_search_proto_msgTypes, + }.Build() + File_proto_search_proto = out.File + file_proto_search_proto_goTypes = nil + file_proto_search_proto_depIdxs = nil +} diff --git a/grpc-protoscope/searchpb/search_grpc.pb.go b/grpc-protoscope/searchpb/search_grpc.pb.go new file mode 100644 index 00000000..5bc0832b --- /dev/null +++ b/grpc-protoscope/searchpb/search_grpc.pb.go @@ -0,0 +1,121 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc v3.21.12 +// source: proto/search.proto + +package searchpb + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + SearchService_Search_FullMethodName = "/search.SearchService/Search" +) + +// SearchServiceClient is the client API for SearchService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type SearchServiceClient interface { + Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) +} + +type searchServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewSearchServiceClient(cc grpc.ClientConnInterface) SearchServiceClient { + return &searchServiceClient{cc} +} + +func (c *searchServiceClient) Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SearchResponse) + err := c.cc.Invoke(ctx, SearchService_Search_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SearchServiceServer is the server API for SearchService service. +// All implementations must embed UnimplementedSearchServiceServer +// for forward compatibility. +type SearchServiceServer interface { + Search(context.Context, *SearchRequest) (*SearchResponse, error) + mustEmbedUnimplementedSearchServiceServer() +} + +// UnimplementedSearchServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedSearchServiceServer struct{} + +func (UnimplementedSearchServiceServer) Search(context.Context, *SearchRequest) (*SearchResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Search not implemented") +} +func (UnimplementedSearchServiceServer) mustEmbedUnimplementedSearchServiceServer() {} +func (UnimplementedSearchServiceServer) testEmbeddedByValue() {} + +// UnsafeSearchServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SearchServiceServer will +// result in compilation errors. +type UnsafeSearchServiceServer interface { + mustEmbedUnimplementedSearchServiceServer() +} + +func RegisterSearchServiceServer(s grpc.ServiceRegistrar, srv SearchServiceServer) { + // If the following call panics, it indicates UnimplementedSearchServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&SearchService_ServiceDesc, srv) +} + +func _SearchService_Search_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SearchRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SearchServiceServer).Search(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SearchService_Search_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SearchServiceServer).Search(ctx, req.(*SearchRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// SearchService_ServiceDesc is the grpc.ServiceDesc for SearchService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var SearchService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "search.SearchService", + HandlerType: (*SearchServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Search", + Handler: _SearchService_Search_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/search.proto", +} diff --git a/grpc-protoscope/server/main.go b/grpc-protoscope/server/main.go new file mode 100644 index 00000000..529dfca1 --- /dev/null +++ b/grpc-protoscope/server/main.go @@ -0,0 +1,158 @@ +package main + +import ( + "context" + "fmt" + "log" + "math" + "math/rand" + "net" + + pb "zepto-grpc/searchpb" + + "google.golang.org/grpc" + "google.golang.org/protobuf/encoding/protowire" + "google.golang.org/protobuf/proto" +) + +// buildFacetEntry builds a FacetEntry wire: { 1: name, 2: { FacetValue } } +func buildFacetEntry(name string, numericVal *float64, textVal *string) []byte { + var fv []byte // FacetValue oneof + if numericVal != nil { + fv = protowire.AppendTag(fv, 2, protowire.Fixed64Type) + fv = protowire.AppendFixed64(fv, math.Float64bits(*numericVal)) + } else if textVal != nil { + fv = protowire.AppendTag(fv, 3, protowire.BytesType) + fv = protowire.AppendString(fv, *textVal) + } + var entry []byte + entry = protowire.AppendTag(entry, 1, protowire.BytesType) + entry = protowire.AppendString(entry, name) + entry = protowire.AppendTag(entry, 2, protowire.BytesType) + entry = protowire.AppendBytes(entry, fv) + return entry +} + +// buildResponse constructs SearchResponse wire bytes with randomized +// repeated-field ordering inside the facet buckets. +func buildResponse() []byte { + var buf []byte + + // field 1: float score = 67.0 + buf = protowire.AppendTag(buf, 1, protowire.Fixed32Type) + buf = protowire.AppendFixed32(buf, math.Float32bits(67.0)) + + // field 4: string hits_json + hitsJSON := `{"hits":[{"_index":"pvid_search_products_v4","_score":15100000000000000000,` + + `"_source":{"_rankingInfo":{"typosPresent":true,"numberOfWordsMatched":1}},` + + `"match_type":"Other","attributes":{"subThemes":null},` + + `"_id":"4f30407c-6a3c-4a4e-8a3d-652217d4b6cb_d67c25f8-3adb-40c1-9113-b46d54a6e8aa",` + + `"trimming_meta":{"trimming_type":"L3"}}]}` + buf = protowire.AppendTag(buf, 4, protowire.BytesType) + buf = protowire.AppendString(buf, hitsJSON) + + // field 8: int32 total = 0 + buf = protowire.AppendTag(buf, 8, protowire.VarintType) + buf = protowire.AppendVarint(buf, 0) + + // --- field 9: FacetInfo message --- + + // Build availability bucket entries (field 3 inside FacetInfo) + zero := 0.0 + ovs := "OVS" + availEntries := [][]byte{ + buildFacetEntry("candidateCnt", &zero, nil), + buildFacetEntry("type", nil, &ovs), + } + // RANDOMIZE repeated entries — triggers the bug + rand.Shuffle(len(availEntries), func(i, j int) { + availEntries[i], availEntries[j] = availEntries[j], availEntries[i] + }) + var availBucket []byte + for _, e := range availEntries { + availBucket = protowire.AppendTag(availBucket, 1, protowire.BytesType) + availBucket = protowire.AppendBytes(availBucket, e) + } + + // Build pricing bucket entries (field 2 inside FacetInfo) + one := 1.0 + pricingEntries := [][]byte{ + buildFacetEntry("candidateCnt", &one, nil), + buildFacetEntry("resultCnt", &one, nil), + } + rand.Shuffle(len(pricingEntries), func(i, j int) { + pricingEntries[i], pricingEntries[j] = pricingEntries[j], pricingEntries[i] + }) + var pricingBucket []byte + for _, e := range pricingEntries { + pricingBucket = protowire.AppendTag(pricingBucket, 1, protowire.BytesType) + pricingBucket = protowire.AppendBytes(pricingBucket, e) + } + + // Assemble FacetInfo: field 3 = availability, field 2 = pricing + var facetInfo []byte + facetInfo = protowire.AppendTag(facetInfo, 3, protowire.BytesType) + facetInfo = protowire.AppendBytes(facetInfo, availBucket) + facetInfo = protowire.AppendTag(facetInfo, 2, protowire.BytesType) + facetInfo = protowire.AppendBytes(facetInfo, pricingBucket) + + buf = protowire.AppendTag(buf, 9, protowire.BytesType) + buf = protowire.AppendBytes(buf, facetInfo) + + return buf +} + +// rawCodec sends pre-built wire bytes without proto re-marshaling. +type rawCodec struct{} + +func (rawCodec) Name() string { return "proto" } +func (rawCodec) Marshal(v interface{}) ([]byte, error) { + if b, ok := v.(*rawFrame); ok { + return b.data, nil + } + return proto.Marshal(v.(proto.Message)) +} +func (rawCodec) Unmarshal(data []byte, v interface{}) error { + if b, ok := v.(*rawFrame); ok { + b.data = append(b.data[:0], data...) + return nil + } + return proto.Unmarshal(data, v.(proto.Message)) +} + +type rawFrame struct{ data []byte } + +func main() { + lis, err := net.Listen("tcp", ":50051") + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + + s := grpc.NewServer(grpc.ForceServerCodec(rawCodec{})) + + // Register service with a custom handler that returns raw wire bytes + // with randomized field ordering each time. + sd := grpc.ServiceDesc{ + ServiceName: "search.SearchService", + HandlerType: (*pb.SearchServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Search", + Handler: func(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) { + req := new(pb.SearchRequest) + if err := dec(req); err != nil { + return nil, err + } + log.Printf("Received search query: %s", req.GetQuery()) + return &rawFrame{data: buildResponse()}, nil + }, + }, + }, + } + s.RegisterService(&sd, &struct{ pb.UnimplementedSearchServiceServer }{}) + + fmt.Println("gRPC server listening on :50051") + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} From 518cbeb539d36866407673d297a0c1b0a31034a7 Mon Sep 17 00:00:00 2001 From: Anju Date: Mon, 6 Apr 2026 06:19:42 -0400 Subject: [PATCH 2/5] chore: remove keploy.yml from grpc-protoscope sample --- grpc-protoscope/keploy.yml | 96 -------------------------------------- 1 file changed, 96 deletions(-) delete mode 100755 grpc-protoscope/keploy.yml diff --git a/grpc-protoscope/keploy.yml b/grpc-protoscope/keploy.yml deleted file mode 100755 index c13b9c0c..00000000 --- a/grpc-protoscope/keploy.yml +++ /dev/null @@ -1,96 +0,0 @@ -# Generated by Keploy (3-dev) -path: "" -appName: zepto-grpc -appId: 0 -command: go run ./server/ -templatize: - testSets: [] -port: 0 -e2e: false -dnsPort: 26789 -proxyPort: 16789 -incomingProxyPort: 36789 -debug: false -disableTele: false -disableANSI: false -containerName: "" -networkName: "" -buildDelay: 30 -test: - selectedTests: {} - globalNoise: - global: {} - test-sets: {} - replaceWith: - global: - url: {} - test-sets: {} - delay: 5 - host: localhost - port: 0 - grpcPort: 0 - apiTimeout: 5 - skipCoverage: false - coverageReportPath: "" - ignoreOrdering: true - mongoPassword: default@123 - language: "" - removeUnusedMocks: false - fallBackOnMiss: false - jacocoAgentPath: "" - basePath: "" - mocking: true - ignoredTests: {} - disableLineCoverage: false - disableMockUpload: true - useLocalMock: false - updateTemplate: false - mustPass: false - maxFailAttempts: 5 - maxFlakyChecks: 1 - protoFile: "" - protoDir: "" - protoInclude: [] - compareAll: false - schemaMatch: false - updateTestMapping: false -record: - filters: [] - basePath: "" - recordTimer: 0s - metadata: "" - sync: false - enableSampling: 0 - globalPassthrough: false - tlsPrivateKeyPath: "" -report: - selectedTestSets: {} - showFullBody: false - reportPath: "" - summary: false - testCaseIDs: [] - format: "" -disableMapping: true -retryPassing: false -configPath: "" -bypassRules: [] -generateGithubActions: false -keployContainer: keploy-v3 -keployNetwork: keploy-network -cmdType: native -contract: - services: [] - tests: [] - path: "" - download: false - generate: false - driven: consumer - mappings: - servicesMapping: {} - self: s1 -inCi: false -serverPort: 0 -mockDownload: - registryIds: [] - -# Visit [https://keploy.io/docs/running-keploy/configuration-file/] to learn about using keploy through configration file. From 5f0889fc3e00bd9b5f61332cdb13144a7927bf5c Mon Sep 17 00:00:00 2001 From: Anju Date: Mon, 6 Apr 2026 06:31:55 -0400 Subject: [PATCH 3/5] docs: update README with correct Go version, relative paths, and file tree --- grpc-protoscope/README.md | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/grpc-protoscope/README.md b/grpc-protoscope/README.md index feabcfd1..26bc24d4 100644 --- a/grpc-protoscope/README.md +++ b/grpc-protoscope/README.md @@ -263,18 +263,16 @@ On the next run (test mode), the `rand.Shuffle` may flip the inner field order, ### Prerequisites -- Go 1.21+ +- Go 1.26+ - `protoc` compiler (only needed if modifying the `.proto` file) ### Run without Keploy ```bash # Terminal 1 — start the server -cd /home/anju/grpc-protoscope go run ./server/ # Terminal 2 — call it (run multiple times to see different field orderings) -cd /home/anju/grpc-protoscope go run ./client/ go run ./client/ go run ./client/ @@ -286,26 +284,26 @@ You'll see the facet entries printed in different orders across calls. ## 5. Reproducing the Bug with Keploy -### Step 1: Build Keploy from source (if needed) +### Step 1: Install Keploy + +Install Keploy using the [official installation guide](https://keploy.io/docs/server/installation/), or build from source: ```bash -cd /home/anju/keploy +git clone https://github.com/keploy/keploy.git && cd keploy go build -ldflags="-X main.apiServerURI=https://api.keploy.io" -o keploy +export PATH=$PWD:$PATH ``` ### Step 2: Record a test case ```bash -cd /home/anju/grpc-protoscope - # Start recording -/home/anju/keploy/keploy record -c "go run ./server/" +keploy record -c "go run ./server/" ``` In another terminal, trigger the gRPC call: ```bash -cd /home/anju/grpc-protoscope go run ./client/ ``` @@ -314,8 +312,7 @@ Then press `Ctrl+C` in the recording terminal. Keploy saves the test case in `ke ### Step 3: Replay (test mode) ```bash -cd /home/anju/grpc-protoscope -/home/anju/keploy/keploy test -c "go run ./server/" +keploy test -c "go run ./server/" ``` **Expected result:** Because `rand.Shuffle` randomizes field ordering each time, ~50% of test runs will produce a different wire order than the recording, triggering: @@ -336,14 +333,14 @@ If the test passes (same random order happened to match), delete the `keploy/` f ``` grpc-protoscope/ ├── README.md ← This file +├── go.mod +├── go.sum ├── proto/search.proto ← Protobuf schema matching the bug report structure ├── searchpb/ ← Generated Go protobuf/gRPC code │ ├── search.pb.go │ └── search_grpc.pb.go ├── server/main.go ← gRPC server with randomized wire field ordering -├── client/main.go ← gRPC client that calls the Search RPC -├── go.mod -├── go.sum -└── keploy/ ← Keploy test artifacts (created after recording) - └── test-set-0/tests/test-1.yaml +└── client/main.go ← gRPC client that calls the Search RPC ``` + +> **Note:** The `keploy/` directory (test artifacts) is generated at runtime when you run `keploy record` and is not checked into the repository. From 159b6f0a8dff84bc86aa4116a8033ed6ffb5c22f Mon Sep 17 00:00:00 2001 From: Anju Date: Tue, 7 Apr 2026 01:51:14 -0400 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94=20?= =?UTF-8?q?drop=20unused=20keploy=20dep,=20seed=20rand,=20safe=20codec=20a?= =?UTF-8?q?ssertions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- grpc-protoscope/go.mod | 50 -------------- grpc-protoscope/go.sum | 118 --------------------------------- grpc-protoscope/server/main.go | 19 ++++-- 3 files changed, 15 insertions(+), 172 deletions(-) diff --git a/grpc-protoscope/go.mod b/grpc-protoscope/go.mod index f7e3756d..12435948 100644 --- a/grpc-protoscope/go.mod +++ b/grpc-protoscope/go.mod @@ -3,63 +3,13 @@ module zepto-grpc go 1.26.0 require ( - github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9 - go.keploy.io/server/v3 v3.0.0-00010101000000-000000000000 google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 ) require ( - github.com/7sDream/geko v0.1.1 // indirect - github.com/andybalholm/brotli v1.2.0 // indirect - github.com/bufbuild/protocompile v0.14.1 // indirect - github.com/clipperhouse/uax29/v2 v2.7.0 // indirect - github.com/creack/pty v1.1.24 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/getsentry/sentry-go v0.28.1 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/k0kubun/pp/v3 v3.2.0 // indirect - github.com/keploy/jsonDiff v1.0.8 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect - github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect - github.com/olekukonko/errors v1.1.0 // indirect - github.com/olekukonko/ll v0.1.1 // indirect - github.com/olekukonko/tablewriter v1.1.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/sagikazarmark/locafero v0.12.0 // indirect - github.com/spf13/afero v1.15.0 // indirect - github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/cobra v1.10.1 // indirect - github.com/spf13/pflag v1.0.10 // indirect - github.com/spf13/viper v1.21.0 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/stretchr/testify v1.11.1 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/tidwall/gjson v1.18.0 // indirect - github.com/tidwall/match v1.2.0 // indirect - github.com/tidwall/pretty v1.2.1 // indirect - go.mongodb.org/mongo-driver/v2 v2.4.1 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect - go.yaml.in/yaml/v2 v2.4.3 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.50.0 // indirect - golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect - golang.org/x/term v0.40.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - helm.sh/helm/v3 v3.18.3 // indirect - sigs.k8s.io/yaml v1.6.0 // indirect ) - -replace go.keploy.io/server/v3 => /home/anju/keploy diff --git a/grpc-protoscope/go.sum b/grpc-protoscope/go.sum index 8b2ac71c..37f73fbd 100644 --- a/grpc-protoscope/go.sum +++ b/grpc-protoscope/go.sum @@ -1,110 +1,15 @@ -github.com/7sDream/geko v0.1.1 h1:Ms9RVcUJDPsUuRlk3T3sGmSGAMuttqh2/3okZ4yWUpU= -github.com/7sDream/geko v0.1.1/go.mod h1:NcSQXSUpcoqNP8BkOLP2TsinxRVjnMkDYCKl9L1Czxk= -github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= -github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= -github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= -github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= -github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k= -github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg= -github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/k0kubun/pp/v3 v3.2.0 h1:h33hNTZ9nVFNP3u2Fsgz8JXiF5JINoZfFq4SvKJwNcs= -github.com/k0kubun/pp/v3 v3.2.0/go.mod h1:ODtJQbQcIRfAD3N+theGCV1m/CBxweERz2dapdz1EwA= -github.com/keploy/jsonDiff v1.0.8 h1:B/75cfLNZ7kayAjF1Jox70Iwa/nwCjBYNOuSt+RHH5A= -github.com/keploy/jsonDiff v1.0.8/go.mod h1:wUuLbVs3Oe3mIQ61C7G88bppP//ArLtoDU0S9Awwv+s= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= -github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= -github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= -github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= -github.com/olekukonko/ll v0.1.1 h1:9Dfeed5/Mgaxb9lHRAftLK9pVfYETvHn+If6lywVhJc= -github.com/olekukonko/ll v0.1.1/go.mod h1:2dJo+hYZcJMLMbKwHEWvxCUbAOLc/CXWS9noET22Mdo= -github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY= -github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= -github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9 h1:arwj11zP0yJIxIRiDn22E0H8PxfF7TsTrc2wIPFIsf4= -github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9/go.mod h1:SKZx6stCn03JN3BOWTwvVIO2ajMkb/zQdTceXYhKw/4= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= -github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= -github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= -github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= -github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= -github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -go.mongodb.org/mongo-driver/v2 v2.4.1 h1:hGDMngUao03OVQ6sgV5csk+RWOIkF+CuLsTPobNMGNI= -go.mongodb.org/mongo-driver/v2 v2.4.1/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= @@ -117,25 +22,10 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= -go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= @@ -146,11 +36,3 @@ google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -helm.sh/helm/v3 v3.18.3 h1:+cvyGKgs7Jt7BN3Klmb4SsG4IkVpA7GAZVGvMz6VO4I= -helm.sh/helm/v3 v3.18.3/go.mod h1:wUc4n3txYBocM7S9RjTeZBN9T/b5MjffpcSsWEjSIpw= -sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= -sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/grpc-protoscope/server/main.go b/grpc-protoscope/server/main.go index 529dfca1..ceda2366 100644 --- a/grpc-protoscope/server/main.go +++ b/grpc-protoscope/server/main.go @@ -7,6 +7,7 @@ import ( "math" "math/rand" "net" + "time" pb "zepto-grpc/searchpb" @@ -33,6 +34,8 @@ func buildFacetEntry(name string, numericVal *float64, textVal *string) []byte { return entry } +var rng = rand.New(rand.NewSource(time.Now().UnixNano())) + // buildResponse constructs SearchResponse wire bytes with randomized // repeated-field ordering inside the facet buckets. func buildResponse() []byte { @@ -65,7 +68,7 @@ func buildResponse() []byte { buildFacetEntry("type", nil, &ovs), } // RANDOMIZE repeated entries — triggers the bug - rand.Shuffle(len(availEntries), func(i, j int) { + rng.Shuffle(len(availEntries), func(i, j int) { availEntries[i], availEntries[j] = availEntries[j], availEntries[i] }) var availBucket []byte @@ -80,7 +83,7 @@ func buildResponse() []byte { buildFacetEntry("candidateCnt", &one, nil), buildFacetEntry("resultCnt", &one, nil), } - rand.Shuffle(len(pricingEntries), func(i, j int) { + rng.Shuffle(len(pricingEntries), func(i, j int) { pricingEntries[i], pricingEntries[j] = pricingEntries[j], pricingEntries[i] }) var pricingBucket []byte @@ -110,14 +113,22 @@ func (rawCodec) Marshal(v interface{}) ([]byte, error) { if b, ok := v.(*rawFrame); ok { return b.data, nil } - return proto.Marshal(v.(proto.Message)) + msg, ok := v.(proto.Message) + if !ok { + return nil, fmt.Errorf("rawCodec.Marshal: expected *rawFrame or proto.Message, got %T", v) + } + return proto.Marshal(msg) } func (rawCodec) Unmarshal(data []byte, v interface{}) error { if b, ok := v.(*rawFrame); ok { b.data = append(b.data[:0], data...) return nil } - return proto.Unmarshal(data, v.(proto.Message)) + msg, ok := v.(proto.Message) + if !ok { + return fmt.Errorf("rawCodec.Unmarshal: expected *rawFrame or proto.Message, got %T", v) + } + return proto.Unmarshal(data, msg) } type rawFrame struct{ data []byte } From e4318f8de13c315f2445ac803077b01aff3cdb9e Mon Sep 17 00:00:00 2001 From: Anju Date: Tue, 7 Apr 2026 02:05:53 -0400 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20address=20round-2=20review=20?= =?UTF-8?q?=E2=80=94=20go=201.24,=20concurrency-safe=20rand,=20interceptor?= =?UTF-8?q?=20support,=20better=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- grpc-protoscope/README.md | 2 +- grpc-protoscope/client/main.go | 4 ++-- grpc-protoscope/go.mod | 2 +- grpc-protoscope/server/main.go | 30 ++++++++++++++++++++---------- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/grpc-protoscope/README.md b/grpc-protoscope/README.md index 26bc24d4..1f61ea45 100644 --- a/grpc-protoscope/README.md +++ b/grpc-protoscope/README.md @@ -263,7 +263,7 @@ On the next run (test mode), the `rand.Shuffle` may flip the inner field order, ### Prerequisites -- Go 1.26+ +- Go 1.24+ - `protoc` compiler (only needed if modifying the `.proto` file) ### Run without Keploy diff --git a/grpc-protoscope/client/main.go b/grpc-protoscope/client/main.go index 7d8ac9c0..c0ba8fd3 100644 --- a/grpc-protoscope/client/main.go +++ b/grpc-protoscope/client/main.go @@ -17,7 +17,7 @@ func main() { grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { - log.Fatalf("failed to connect: %v", err) + log.Fatalf("failed to connect to localhost:50051: %v (ensure the server is running)", err) } defer conn.Close() @@ -28,7 +28,7 @@ func main() { resp, err := client.Search(ctx, &pb.SearchRequest{Query: "shoes"}) if err != nil { - log.Fatalf("Search failed: %v", err) + log.Fatalf("Search RPC failed: %v (ensure the server is running and the service is registered)", err) } fmt.Printf("Score: %.1f\n", resp.GetScore()) diff --git a/grpc-protoscope/go.mod b/grpc-protoscope/go.mod index 12435948..fd8dbd43 100644 --- a/grpc-protoscope/go.mod +++ b/grpc-protoscope/go.mod @@ -1,6 +1,6 @@ module zepto-grpc -go 1.26.0 +go 1.24.0 require ( google.golang.org/grpc v1.80.0 diff --git a/grpc-protoscope/server/main.go b/grpc-protoscope/server/main.go index ceda2366..c45c9f50 100644 --- a/grpc-protoscope/server/main.go +++ b/grpc-protoscope/server/main.go @@ -7,7 +7,6 @@ import ( "math" "math/rand" "net" - "time" pb "zepto-grpc/searchpb" @@ -34,8 +33,6 @@ func buildFacetEntry(name string, numericVal *float64, textVal *string) []byte { return entry } -var rng = rand.New(rand.NewSource(time.Now().UnixNano())) - // buildResponse constructs SearchResponse wire bytes with randomized // repeated-field ordering inside the facet buckets. func buildResponse() []byte { @@ -68,7 +65,7 @@ func buildResponse() []byte { buildFacetEntry("type", nil, &ovs), } // RANDOMIZE repeated entries — triggers the bug - rng.Shuffle(len(availEntries), func(i, j int) { + rand.Shuffle(len(availEntries), func(i, j int) { availEntries[i], availEntries[j] = availEntries[j], availEntries[i] }) var availBucket []byte @@ -83,7 +80,7 @@ func buildResponse() []byte { buildFacetEntry("candidateCnt", &one, nil), buildFacetEntry("resultCnt", &one, nil), } - rng.Shuffle(len(pricingEntries), func(i, j int) { + rand.Shuffle(len(pricingEntries), func(i, j int) { pricingEntries[i], pricingEntries[j] = pricingEntries[j], pricingEntries[i] }) var pricingBucket []byte @@ -136,7 +133,7 @@ type rawFrame struct{ data []byte } func main() { lis, err := net.Listen("tcp", ":50051") if err != nil { - log.Fatalf("failed to listen: %v", err) + log.Fatalf("failed to listen on :50051: %v (check if the port is already in use)", err) } s := grpc.NewServer(grpc.ForceServerCodec(rawCodec{})) @@ -154,16 +151,29 @@ func main() { if err := dec(req); err != nil { return nil, err } - log.Printf("Received search query: %s", req.GetQuery()) - return &rawFrame{data: buildResponse()}, nil + handler := func(ctx context.Context, req any) (any, error) { + searchReq := req.(*pb.SearchRequest) + log.Printf("Received search query: %s", searchReq.GetQuery()) + return &rawFrame{data: buildResponse()}, nil + } + if interceptor == nil { + return handler(ctx, req) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/search.SearchService/Search", + } + return interceptor(ctx, req, info, handler) }, }, }, } - s.RegisterService(&sd, &struct{ pb.UnimplementedSearchServiceServer }{}) + s.RegisterService(&sd, &struct { + pb.UnimplementedSearchServiceServer + }{}) fmt.Println("gRPC server listening on :50051") if err := s.Serve(lis); err != nil { - log.Fatalf("failed to serve: %v", err) + log.Fatalf("gRPC server stopped: %v", err) } }