| // Licensed to Apache Software Foundation (ASF) under one or more contributor |
| // license agreements. See the NOTICE file distributed with |
| // this work for additional information regarding copyright |
| // ownership. Apache Software Foundation (ASF) licenses this file to you under |
| // the Apache License, Version 2.0 (the "License"); you may |
| // not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| package data |
| |
| import ( |
| "bytes" |
| "testing" |
| |
| "google.golang.org/protobuf/proto" |
| |
| commonv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/common/v1" |
| measurev1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/measure/v1" |
| ) |
| |
| // TestRawFrameMagic_ProtoUnmarshalFailsLoud is the G9f spec's load-bearing |
| // proof test (Principle 3, G9f.2 codec contract): a raw vec columnar frame's |
| // leading 0x00 byte deterministically forces a flag-off node's |
| // proto.Unmarshal of the frame body into *measurev1.InternalQueryResponse{} |
| // to return a non-nil error, collapsing the dangerous "garbage-but-parsed |
| // silently-empty" outcome into an unmistakable hard decode error. Verified |
| // against google.golang.org/protobuf@v1.36.11: 0x00 decodes as field number 0 |
| // which protowire.ConsumeTag rejects with errCodeFieldNumber, and |
| // proto/decode.go converts the resulting negative tag length into a hard |
| // errDecode *before* any unknown-field skip — independent of every byte |
| // that follows. |
| func TestRawFrameMagic_ProtoUnmarshalFailsLoud(t *testing.T) { |
| cases := []struct { |
| name string |
| body []byte |
| }{ |
| {name: "magic-only", body: []byte{RawFrameMagicLeadingByte}}, |
| {name: "magic-with-version", body: []byte{RawFrameMagicLeadingByte, 0x01}}, |
| {name: "magic-with-columnar-payload", body: []byte{RawFrameMagicLeadingByte, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}}, |
| } |
| for _, tc := range cases { |
| t.Run(tc.name, func(t *testing.T) { |
| var m measurev1.InternalQueryResponse |
| if err := proto.Unmarshal(tc.body, &m); err == nil { |
| t.Fatalf("proto.Unmarshal of 0x00-leading raw frame %x into *InternalQueryResponse{} returned nil error; want fail-loud (silently-parsed message: %+v)", tc.body, &m) |
| } |
| }) |
| } |
| } |
| |
| // TestRawFrameCodec_NilEmptyBody_NotMagicValidated covers the spec carve-out |
| // (G9f.0 exit, sub.go:195-205 / pub.go:519-521 audit): an empty/nil response |
| // body is a legitimate empty distributed result and MUST NOT be magic-validated, |
| // else legitimate empties would fail loud incorrectly. |
| func TestRawFrameCodec_NilEmptyBody_NotMagicValidated(t *testing.T) { |
| codec := NewRawFrameCodec() |
| for _, tc := range []struct { |
| name string |
| body []byte |
| }{ |
| {name: "nil", body: nil}, |
| {name: "empty", body: []byte{}}, |
| } { |
| t.Run(tc.name, func(t *testing.T) { |
| v, err := codec.Unmarshal(tc.body) |
| if err != nil { |
| t.Fatalf("RawFrameCodec.Unmarshal(%v) returned err=%v; want nil (empty body must NOT be magic-validated)", tc.body, err) |
| } |
| b, _ := v.([]byte) |
| if len(b) != 0 { |
| t.Fatalf("RawFrameCodec.Unmarshal(%v) returned non-empty body %v; want empty", tc.body, b) |
| } |
| }) |
| } |
| } |
| |
| // TestRawFrameCodec_WrongMagic_FailsLoud covers the positive side of the |
| // G9f.0 codec contract: a non-empty body whose leading byte is not 0x00 |
| // must be rejected loudly. |
| func TestRawFrameCodec_WrongMagic_FailsLoud(t *testing.T) { |
| codec := NewRawFrameCodec() |
| for _, body := range [][]byte{{0x01}, {0xff, 0x00}, {0x08, 0x01, 0x02}} { |
| if _, err := codec.Unmarshal(body); err == nil { |
| t.Fatalf("RawFrameCodec.Unmarshal(%x) returned nil error; want fail-loud on non-0x00 leading byte", body) |
| } |
| } |
| } |
| |
| // TestTopicResponseMap_ProtoCodec_RoundTripsByteIdentical is the G9f.0 |
| // blast-radius regression: every proto-bodied topic in TopicResponseMap |
| // (all ~18 stream/measure/topn/property/trace/collect-info topics, excluding |
| // TopicInternalMeasureQuery which uses the wire-mode-dispatching codec) must |
| // marshal/unmarshal byte-identically to the pre-G9f.0 |
| // proto.Marshal/proto.Unmarshal path. Empty round-trip is sufficient to prove |
| // the wrapper is a pure passthrough. |
| func TestTopicResponseMap_ProtoCodec_RoundTripsByteIdentical(t *testing.T) { |
| var protoTopics int |
| for topic, codec := range TopicResponseMap { |
| if topic == TopicInternalMeasureQuery || topic == TopicTraceQuery { |
| // Wire-mode-dispatching codecs; covered separately (measure below, |
| // trace in trace_frame_test.go including flag-off byte-identity). |
| continue |
| } |
| pc, ok := codec.(*ProtoCodec) |
| if !ok { |
| t.Errorf("topic %s: expected *ProtoCodec, got %T", topic, codec) |
| continue |
| } |
| protoTopics++ |
| msg := pc.newMessage() |
| wantBytes, err := proto.Marshal(msg) |
| if err != nil { |
| t.Errorf("topic %s: baseline proto.Marshal failed: %v", topic, err) |
| continue |
| } |
| gotBytes, err := pc.Marshal(msg) |
| if err != nil { |
| t.Errorf("topic %s: ProtoCodec.Marshal failed: %v", topic, err) |
| continue |
| } |
| if !bytes.Equal(gotBytes, wantBytes) { |
| t.Errorf("topic %s: marshal mismatch: got %x want %x", topic, gotBytes, wantBytes) |
| continue |
| } |
| decoded, err := pc.Unmarshal(gotBytes) |
| if err != nil { |
| t.Errorf("topic %s: ProtoCodec.Unmarshal failed: %v", topic, err) |
| continue |
| } |
| if _, ok := decoded.(proto.Message); !ok { |
| t.Errorf("topic %s: decoded type %T does not implement proto.Message", topic, decoded) |
| } |
| } |
| if protoTopics == 0 { |
| t.Fatal("no ProtoCodec topics found in TopicResponseMap — blast-radius regression test would be a no-op") |
| } |
| } |
| |
| // TestMeasureQueryResponseCodec_DispatchesOnWireMode covers the G9f.0 |
| // topic-AND-process-wire-mode guard: TopicInternalMeasureQuery is served by a |
| // single static supplier in the map but the codec selected per-encode/decode |
| // must depend on the per-process wire-mode flag set via SetMeasureWireModeRaw |
| // (flag-on → RawFrameCodec; flag-off → ProtoCodec). |
| func TestMeasureQueryResponseCodec_DispatchesOnWireMode(t *testing.T) { |
| codec, ok := TopicResponseMap[TopicInternalMeasureQuery] |
| if !ok { |
| t.Fatal("TopicInternalMeasureQuery is not registered in TopicResponseMap") |
| } |
| |
| // The wire mode is a process-wide atomic. Save and restore so the test |
| // is hermetic and the suite can run in any order. |
| prev := MeasureWireModeRaw() |
| t.Cleanup(func() { SetMeasureWireModeRaw(prev) }) |
| |
| // Flag OFF: proto behavior, byte-identical to pre-G9f.0 path for the |
| // real measurev1.InternalQueryResponse{} body. |
| SetMeasureWireModeRaw(false) |
| msg := &measurev1.InternalQueryResponse{} |
| body, err := codec.Marshal(msg) |
| if err != nil { |
| t.Fatalf("flag-off Marshal failed: %v", err) |
| } |
| decoded, err := codec.Unmarshal(body) |
| if err != nil { |
| t.Fatalf("flag-off Unmarshal failed: %v", err) |
| } |
| if _, ok := decoded.(*measurev1.InternalQueryResponse); !ok { |
| t.Fatalf("flag-off Unmarshal returned %T; want *measurev1.InternalQueryResponse", decoded) |
| } |
| |
| // Flag ON: raw passthrough; bytes in == bytes out, 0x00-leading. |
| SetMeasureWireModeRaw(true) |
| rawBody := []byte{RawFrameMagicLeadingByte, 0x01, 0xAA, 0xBB} |
| encoded, err := codec.Marshal(rawBody) |
| if err != nil { |
| t.Fatalf("flag-on Marshal failed: %v", err) |
| } |
| if !bytes.Equal(encoded, rawBody) { |
| t.Fatalf("flag-on Marshal is not passthrough: got %x want %x", encoded, rawBody) |
| } |
| out, err := codec.Unmarshal(rawBody) |
| if err != nil { |
| t.Fatalf("flag-on Unmarshal of 0x00-leading body failed: %v", err) |
| } |
| outBytes, _ := out.([]byte) |
| if !bytes.Equal(outBytes, rawBody) { |
| t.Fatalf("flag-on Unmarshal is not passthrough: got %x want %x", outBytes, rawBody) |
| } |
| |
| // Flag ON + body with the wrong magic now routes to the proto envelope decoder. |
| if _, err := codec.Unmarshal([]byte{0xff, 0x02}); err == nil { |
| t.Fatal("flag-on Unmarshal of invalid proto envelope returned nil error; want fail-loud") |
| } |
| } |
| |
| func TestMeasureQueryResponseCodec_RawModeTraceEnvelopeRoundTrip(t *testing.T) { |
| codec, ok := TopicResponseMap[TopicInternalMeasureQuery] |
| if !ok { |
| t.Fatal("TopicInternalMeasureQuery is not registered in TopicResponseMap") |
| } |
| prev := MeasureWireModeRaw() |
| t.Cleanup(func() { SetMeasureWireModeRaw(prev) }) |
| SetMeasureWireModeRaw(true) |
| |
| rawBody := []byte{RawFrameMagicLeadingByte, 0x01, 0xAA, 0xBB} |
| envelope := &measurev1.InternalQueryResponse{ |
| RawFrameBody: rawBody, |
| Trace: &commonv1.Trace{TraceId: "trace-1"}, |
| } |
| encoded, err := codec.Marshal(envelope) |
| if err != nil { |
| t.Fatalf("raw-mode envelope Marshal failed: %v", err) |
| } |
| if len(encoded) == 0 || encoded[0] == RawFrameMagicLeadingByte { |
| t.Fatalf("raw-mode envelope encoded with raw-frame prefix: %x", encoded) |
| } |
| decoded, err := codec.Unmarshal(encoded) |
| if err != nil { |
| t.Fatalf("raw-mode envelope Unmarshal failed: %v", err) |
| } |
| got, ok := decoded.(*measurev1.InternalQueryResponse) |
| if !ok { |
| t.Fatalf("raw-mode envelope Unmarshal returned %T; want *measurev1.InternalQueryResponse", decoded) |
| } |
| if !bytes.Equal(got.GetRawFrameBody(), rawBody) { |
| t.Fatalf("RawFrameBody mismatch: got %x want %x", got.GetRawFrameBody(), rawBody) |
| } |
| if got.GetTrace().GetTraceId() != "trace-1" { |
| t.Fatalf("TraceId mismatch: got %q", got.GetTrace().GetTraceId()) |
| } |
| } |
| |
| func TestMeasureQueryResponseCodec_RawModeTraceEmptyEnvelopeRoundTrip(t *testing.T) { |
| codec, ok := TopicResponseMap[TopicInternalMeasureQuery] |
| if !ok { |
| t.Fatal("TopicInternalMeasureQuery is not registered in TopicResponseMap") |
| } |
| prev := MeasureWireModeRaw() |
| t.Cleanup(func() { SetMeasureWireModeRaw(prev) }) |
| SetMeasureWireModeRaw(true) |
| |
| envelope := &measurev1.InternalQueryResponse{Trace: &commonv1.Trace{TraceId: "empty"}} |
| encoded, err := codec.Marshal(envelope) |
| if err != nil { |
| t.Fatalf("raw-mode empty envelope Marshal failed: %v", err) |
| } |
| decoded, err := codec.Unmarshal(encoded) |
| if err != nil { |
| t.Fatalf("raw-mode empty envelope Unmarshal failed: %v", err) |
| } |
| got, ok := decoded.(*measurev1.InternalQueryResponse) |
| if !ok { |
| t.Fatalf("raw-mode empty envelope Unmarshal returned %T; want *measurev1.InternalQueryResponse", decoded) |
| } |
| if len(got.GetRawFrameBody()) != 0 { |
| t.Fatalf("RawFrameBody length = %d, want 0", len(got.GetRawFrameBody())) |
| } |
| if got.GetTrace().GetTraceId() != "empty" { |
| t.Fatalf("TraceId mismatch: got %q", got.GetTrace().GetTraceId()) |
| } |
| } |