1
0
mirror of https://github.com/rs/zerolog synced 2026-06-08 17:13:30 +00:00
Files
rs-zerolog/internal/cbor/decoder_test.go
T
Marc Brooks f6fbd330be Test coverage improvements (#748)
* Test coverage improvements

Implements #397 and #591, might help with #473
Test coverage for core is 97.6% with only real fringe cases remaining.
Improve `Fields` `isNilValue()` portability.
Added a new global handler `FatalExitFunc` to allow intercepting `Fatal()` messages.
Fixed CBOR float constants (removed the CBOR prefix)

Added tests for:
  - global `FatalExitFunc` to allow intercepting Fatal messages (both for testing and public use).
  - `Logger`
    -  `DisableSampling`
    -  `.With()` copying existing context if present.
    -  `.With().Fields()` all forms of `ErrorStackMarshaler` returns.
    - `.WithLevel()` for `FatalLevel`, `PanicLevel`, and `DisabledLevel`.
    - `.Err()` with `nil` and non-`nil` `error` and test the resulting log level.
    - `.should()` covering `nil` writer.
    - `.Output()` gets context values.
    - `.UpdateContext()` on a disabled logger doesn't panic and is a nop.
    - `.With()` all forms of `ErrorStackMarshaler` returns.
  - with a `nil`writer.
  - `.Hook()` passing no hooks.
- `Array`
  - `.MarshalZerologArray` is a nop that won't panic.
- `Context`
  - ` .Err()` and `.AnErr()` for `nil` errors and all forms of `ErrorStackMarshaler` returns.
  - `Event`
    - `.Caller()` to ensure we don't panic or add invalid information if `runtime.Caller()` fails
    -` .Err()` and `.AnErr()` for `nil` errors and all forms of `ErrorStackMarshaler` returns.
  - `Fields`
    - ` .appendFields()`  all forms of `ErrorStackMarshaler` returns.
  - `HookLevel`
    - `.Run()` methods.
  - `LevelSampler`
    - `.Sample() methods.
  - `Syslog`
    - `.Write()`, `.WriteLevel()`, and `.Close()` methods.
    - `.WriteLevel()` with an `InvalidLevel`.
  - `Writer`
    - `.Write()` short write and error cases.
    - `MultiLevelWriter` `.WriteLevel()` and for `.Write()` error and  `.Close()` cases.
  - test of unmarshalling a level byte returns correct error.
  - CBOR decodeStream
    - `.decodeFloat()`, `.binaryFmt()`, `.DecodeIfBinaryToString()`, `.DecodeObjectToStr()`, `.DecodeIfBinaryToBytes()`, `.decodeTagData()`, and `.decodeSimpleFloat()`
    - handling of invalid UTF-8 sequences
    - handling of UTC times.
    - handling of timestamps
    - handling of various map lengths

Restructure `Event` `.caller()` so we test for ok and eliminate untestable coverage hole.

Restructure `Context` `.Err()` when the `ErrorStackMarshaler` returns a `nil` so there's code to cover.

Inverted logic for`Event` `.Caller()`'s call to `runtime.Caller()` for simpler testing.

Inverted logic for `Array` `.putArray()` and `Event` `.putEvent()` so there isn't uncoverable code.

Restructure `Field` `.appendFieldList()` to early return when `ErrorStackMarshaler` returns a `nil`

Added comments for things we can't get coverage on.

Did a go fmt ./...

Coverage of core is now 100% on `Array`, `Context`, `Ctx`, `Event`, `Field`, `Hook`, and `Syslog`.

Coverage of `Globals`, `Log`, `Sampler`, and `Writer` is almost all except some real edge-cases.

JSON encoder coverage is 100%

CBOR encoder coverage is 96.3% with base, cbor, string, time and types at 100% and decode_stream (which is lacks coverage on some panic states, and two incorrect coverage-tool lapses)

* Fix CBOR tests for StackMarshaler

Forgot to use the `decodeIfBinaryToString()`
2026-01-12 15:03:52 +00:00

490 lines
15 KiB
Go

package cbor
import (
"bytes"
"encoding/hex"
"math"
"testing"
"time"
"github.com/rs/zerolog/internal"
)
func TestDecodeInteger(t *testing.T) {
for _, tc := range internal.IntegerTestCases {
gotv := decodeInteger(getReader(tc.Binary))
if gotv != int64(tc.Val) {
t.Errorf("decodeInteger(0x%s)=0x%d, want: 0x%d",
hex.EncodeToString([]byte(tc.Binary)), gotv, tc.Val)
}
}
}
func TestDecodeString(t *testing.T) {
for _, tt := range encodeStringTests {
got := decodeUTF8String(getReader(tt.binary))
if string(got) != "\""+tt.json+"\"" {
t.Errorf("DecodeString(0x%s)=%s, want:\"%s\"\n", hex.EncodeToString([]byte(tt.binary)), string(got),
hex.EncodeToString([]byte(tt.json)))
}
}
}
func TestDecodeArray(t *testing.T) {
for _, tc := range internal.IntegerArrayTestCases {
buf := bytes.NewBuffer([]byte{})
array2Json(getReader(tc.Binary), buf)
if buf.String() != tc.Json {
t.Errorf("array2Json(0x%s)=%s, want: %s", hex.EncodeToString([]byte(tc.Binary)), buf.String(), tc.Json)
}
}
//Unspecified Length Array
var infiniteArrayTestCases = []struct {
in string
out string
}{
{"\x9f\x20\x00\x18\xc8\x14\xff", "[-1,0,200,20]"},
{"\x9f\x38\xc7\x29\x18\xc8\x19\x01\x90\xff", "[-200,-10,200,400]"},
{"\x9f\x01\x02\x03\xff", "[1,2,3]"},
{"\x9f\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x18\x18\x19\xff",
"[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]"},
}
for _, tc := range infiniteArrayTestCases {
buf := bytes.NewBuffer([]byte{})
array2Json(getReader(tc.in), buf)
if buf.String() != tc.out {
t.Errorf("array2Json(0x%s)=%s, want: %s", hex.EncodeToString([]byte(tc.out)), buf.String(), tc.out)
}
}
for _, tc := range internal.BooleanArrayTestCases {
buf := bytes.NewBuffer([]byte{})
array2Json(getReader(tc.Binary), buf)
if buf.String() != tc.Json {
t.Errorf("array2Json(0x%s)=%s, want: %s", hex.EncodeToString([]byte(tc.Binary)), buf.String(), tc.Json)
}
}
//TODO add cases for arrays of other types
}
var infiniteMapDecodeTestCases = []struct {
Bin []byte
Json string
}{
{[]byte("\xbf\x64IETF\x20\xff"), "{\"IETF\":-1}"},
{[]byte("\xbf\x65Array\x84\x20\x00\x18\xc8\x14\xff"), "{\"Array\":[-1,0,200,20]}"},
}
var mapDecodeTestCases = []struct {
Bin []byte
Json string
}{
{[]byte("\xa2\x64IETF\x20"), "{\"IETF\":-1}"},
{[]byte("\xa2\x65Array\x84\x20\x00\x18\xc8\x14"), "{\"Array\":[-1,0,200,20]}"},
{[]byte("\xa6\x61\x61\x01\x61\x62\x02\x61\x63\x03"), "{\"a\":1,\"b\":2,\"c\":3}"},
{[]byte("\xbf\x61a\x01\x61b\x02\xff"), "{\"a\":1,\"b\":2}"},
}
func TestDecodeMap(t *testing.T) {
for _, tc := range mapDecodeTestCases {
buf := bytes.NewBuffer([]byte{})
map2Json(getReader(string(tc.Bin)), buf)
if buf.String() != tc.Json {
t.Errorf("map2Json(0x%s)=%s, want: %s", hex.EncodeToString(tc.Bin), buf.String(), tc.Json)
}
}
for _, tc := range infiniteMapDecodeTestCases {
buf := bytes.NewBuffer([]byte{})
map2Json(getReader(string(tc.Bin)), buf)
if buf.String() != tc.Json {
t.Errorf("map2Json(0x%s)=%s, want: %s", hex.EncodeToString(tc.Bin), buf.String(), tc.Json)
}
}
}
func TestDecodeBool(t *testing.T) {
for _, tc := range internal.BooleanTestCases {
got := decodeSimpleFloat(getReader(tc.Binary))
if string(got) != tc.Json {
t.Errorf("decodeSimpleFloat(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.Binary)), string(got), tc.Json)
}
}
}
func TestDecodeFloat(t *testing.T) {
for _, tc := range internal.Float32TestCases {
got, _ := decodeFloat(getReader(tc.Binary))
if got != float64(tc.Val) && math.IsNaN(got) != math.IsNaN(float64(tc.Val)) {
t.Errorf("decodeFloat(0x%s)=%f, want:%f", hex.EncodeToString([]byte(tc.Binary)), got, tc.Val)
}
}
for _, tc := range internal.Float64TestCases {
got, _ := decodeFloat(getReader(tc.Binary))
if got != tc.Val && math.IsNaN(got) != math.IsNaN(tc.Val) {
t.Errorf("decodeFloat(0x%s)=%f, want:%f", hex.EncodeToString([]byte(tc.Binary)), got, tc.Val)
}
}
// Test float64 special values with correct CBOR encoding
float64Tests := []struct {
name string
input string
want float64
}{
{"float64 NaN", "\xfb\x7f\xf8\x00\x00\x00\x00\x00\x00", math.NaN()},
{"float64 +Inf", "\xfb\x7f\xf0\x00\x00\x00\x00\x00\x00", math.Inf(0)},
{"float64 -Inf", "\xfb\xff\xf0\x00\x00\x00\x00\x00\x00", math.Inf(-1)},
{"float64 1.0", "\xfb\x3f\xf0\x00\x00\x00\x00\x00\x00", 1.0},
}
for _, tt := range float64Tests {
t.Run(tt.name, func(t *testing.T) {
got, _ := decodeFloat(getReader(tt.input))
if math.IsNaN(tt.want) {
if !math.IsNaN(got) {
t.Errorf("decodeFloat(%q) = %f, want NaN", tt.input, got)
}
} else if math.IsInf(tt.want, 0) {
if !math.IsInf(got, 0) {
t.Errorf("decodeFloat(%q) = %f, want +Inf", tt.input, got)
}
} else if math.IsInf(tt.want, -1) {
if !math.IsInf(got, -1) {
t.Errorf("decodeFloat(%q) = %f, want -Inf", tt.input, got)
}
} else if got != tt.want {
t.Errorf("decodeFloat(%q) = %f, want %f", tt.input, got, tt.want)
}
})
}
}
func TestDecodeTimestamp(t *testing.T) {
decodeTimeZone, _ = time.LoadLocation("UTC")
for _, tc := range internal.TimeIntegerTestcases {
tm := decodeTagData(getReader(tc.Binary))
if string(tm) != "\""+tc.RfcStr+"\"" {
t.Errorf("decodeFloat(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.Binary)), tm, tc.RfcStr)
}
}
for _, tc := range internal.TimeFloatTestcases {
tm := decodeTagData(getReader(tc.Out))
//Since we convert to float and back - it may be slightly off - so
//we cannot check for exact equality instead, we'll check it is
//very close to each other Less than a Microsecond (lets not yet do nanosec)
got, _ := time.Parse(string(tm), string(tm))
want, _ := time.Parse(tc.RfcStr, tc.RfcStr)
if got.Sub(want) > time.Microsecond {
t.Errorf("decodeFloat(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.Out)), tm, tc.RfcStr)
}
}
// Test with decodeTimeZone = nil to cover the else branches
oldTimeZone := decodeTimeZone
decodeTimeZone = nil
defer func() { decodeTimeZone = oldTimeZone }()
for _, tc := range internal.TimeIntegerTestcases {
tm := decodeTagData(getReader(tc.Binary))
if string(tm) != "\""+tc.RfcStr+"\"" {
t.Errorf("decodeFloat(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.Binary)), tm, tc.RfcStr)
}
}
for _, tc := range internal.TimeFloatTestcases {
tm := decodeTagData(getReader(tc.Out))
got, _ := time.Parse(string(tm), string(tm))
want, _ := time.Parse(tc.RfcStr, tc.RfcStr)
if got.Sub(want) > time.Microsecond {
t.Errorf("decodeFloat(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.Out)), tm, tc.RfcStr)
}
}
}
func TestDecodeNetworkAddr(t *testing.T) {
for _, tc := range internal.IpAddrTestCases {
d1 := decodeTagData(getReader(tc.Binary))
if string(d1) != tc.Text {
t.Errorf("decodeNetworkAddr(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.Binary)), d1, tc.Text)
}
}
}
func TestDecodeMACAddr(t *testing.T) {
for _, tc := range internal.MacAddrTestCases {
d1 := decodeTagData(getReader(tc.Binary))
if string(d1) != tc.Text {
t.Errorf("decodeNetworkAddr(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.Binary)), d1, tc.Text)
}
}
}
func TestDecodeIPPrefix(t *testing.T) {
for _, tc := range internal.IPPrefixTestCases {
d1 := decodeTagData(getReader(tc.Binary))
if string(d1) != tc.Text {
t.Errorf("decodeIPPrefix(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.Binary)), d1, tc.Text)
}
}
}
var compositeCborTestCases = []struct {
Binary []byte
Json string
}{
{[]byte("\xbf\x64IETF\x20\x65Array\x9f\x20\x00\x18\xc8\x14\xff\xff"), "{\"IETF\":-1,\"Array\":[-1,0,200,20]}\n"},
{[]byte("\xbf\x64IETF\x64YES!\x65Array\x9f\x20\x00\x18\xc8\x14\xff\xff"), "{\"IETF\":\"YES!\",\"Array\":[-1,0,200,20]}\n"},
{[]byte("\xbf\x61a\x01\x61b\x02\x61c\x03\xff"), "{\"a\":1,\"b\":2,\"c\":3}\n"},
{[]byte("\xc1\x1a\x51\x0f\x30\xd8"), "\"2013-02-04T03:54:00Z\"\n"},
}
func TestDecodeCbor2Json(t *testing.T) {
for _, tc := range compositeCborTestCases {
buf := bytes.NewBuffer([]byte{})
err := Cbor2JsonManyObjects(getReader(string(tc.Binary)), buf)
if buf.String() != tc.Json || err != nil {
t.Errorf("cbor2JsonManyObjects(0x%s)=%s, want: %s, err:%s", hex.EncodeToString(tc.Binary), buf.String(), tc.Json, err.Error())
}
}
}
var negativeCborTestCases = []struct {
Binary []byte
errStr string
}{
{[]byte("\xb9\x64IETF\x20\x65Array\x9f\x20\x00\x18\xc8\x14"), "Tried to Read 18 Bytes.. But hit end of file"},
{[]byte("\xbf\x64IETF\x20\x65Array\x9f\x20\x00\x18\xc8\x14"), "EOF"},
{[]byte("\xbf\x14IETF\x20\x65Array\x9f\x20\x00\x18\xc8\x14"), "Tried to Read 40736 Bytes.. But hit end of file"},
{[]byte("\xbf\x64IETF"), "EOF"},
{[]byte("\xbf\x64IETF\x20\x65Array\x9f\x20\x00\x18\xc8\xff\xff\xff"), "Invalid Additional Type: 31 in decodeSimpleFloat"},
{[]byte("\xbf\x64IETF\x20\x65Array"), "EOF"},
{[]byte("\xbf\x64"), "Tried to Read 4 Bytes.. But hit end of file"},
}
func TestDecodeNegativeCbor2Json(t *testing.T) {
for _, tc := range negativeCborTestCases {
buf := bytes.NewBuffer([]byte{})
err := Cbor2JsonManyObjects(getReader(string(tc.Binary)), buf)
if err == nil || err.Error() != tc.errStr {
t.Errorf("Expected error got:%s, want:%s", err, tc.errStr)
}
}
}
func TestBinaryFmt(t *testing.T) {
tests := []struct {
input []byte
want bool
}{
{[]byte{}, false},
{[]byte{0x00}, false},
{[]byte{0x7F}, false},
{[]byte{0x80}, true},
{[]byte{0xFF}, true},
{[]byte{0x00, 0x80}, false}, // Only checks first byte
}
for _, tt := range tests {
got := binaryFmt(tt.input)
if got != tt.want {
t.Errorf("binaryFmt(%v) = %v, want %v", tt.input, got, tt.want)
}
}
}
func TestDecodeIfBinaryToString(t *testing.T) {
tests := []struct {
name string
input []byte
want string
}{
{
name: "non-binary input",
input: []byte(`{"key":"value"}`),
want: `{"key":"value"}`,
},
{
name: "binary input - simple object",
input: []byte("\xbf\x64IETF\x20\xff"), // {"IETF": -1} in indefinite length CBOR
want: "{\"IETF\":-1}\n",
},
{
name: "binary input - multiple objects",
input: []byte("\xbf\x64IETF\x20\xff\xbf\x65Array\x84\x20\x00\x18\xc8\x14\xff"), // Two objects
want: "{\"IETF\":-1}\n{\"Array\":[-1,0,200,20]}\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := DecodeIfBinaryToString(tt.input)
if got != tt.want {
t.Errorf("DecodeIfBinaryToString() = %q, want %q", got, tt.want)
}
})
}
}
func TestDecodeObjectToStr(t *testing.T) {
tests := []struct {
name string
input []byte
want string
}{
{
name: "non-binary input",
input: []byte(`{"key":"value"}`),
want: `{"key":"value"}`,
},
{
name: "binary input - simple object",
input: []byte("\xbf\x64IETF\x20\xff"), // {"IETF": -1} in indefinite length CBOR
want: "{\"IETF\":-1}",
},
{
name: "binary input - array",
input: []byte("\x84\x20\x00\x18\xc8\x14"), // [-1, 0, 200, 20]
want: "[-1,0,200,20]",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := DecodeObjectToStr(tt.input)
if got != tt.want {
t.Errorf("DecodeObjectToStr() = %q, want %q", got, tt.want)
}
})
}
}
func TestDecodeIfBinaryToBytes(t *testing.T) {
tests := []struct {
name string
input []byte
want []byte
}{
{
name: "non-binary input",
input: []byte(`{"key":"value"}`),
want: []byte(`{"key":"value"}`),
},
{
name: "binary input - simple object",
input: []byte("\xbf\x64IETF\x20\xff"), // {"IETF": -1} in indefinite length CBOR
want: []byte("{\"IETF\":-1}\n"),
},
{
name: "binary input - multiple objects",
input: []byte("\xbf\x64IETF\x20\xff\xbf\x65Array\x84\x20\x00\x18\xc8\x14\xff"), // Two objects
want: []byte("{\"IETF\":-1}\n{\"Array\":[-1,0,200,20]}\n"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := DecodeIfBinaryToBytes(tt.input)
if !bytes.Equal(got, tt.want) {
t.Errorf("DecodeIfBinaryToBytes() = %q, want %q", string(got), string(tt.want))
}
})
}
}
func TestDecodeEmbeddedCBOR(t *testing.T) {
// Test embedded CBOR tag: 0xD8 0x3F (tag 63) followed by byte string
// 0xD8 = major type 6 (tags) + additional type 24 (uint8 follows)
// 0x3F = 63 (additionalTypeEmbeddedCBOR)
// 0x43 = major type 2 (byte string) + length 3
// 0x01 0x02 0x03 = the embedded CBOR data
embeddedCBOR := []byte("\xd8\x3f\x43\x01\x02\x03")
expected := "\"data:application/cbor;base64,AQID\""
got := decodeTagData(getReader(string(embeddedCBOR)))
if string(got) != expected {
t.Errorf("decodeTagData(embedded CBOR) = %q, want %q", string(got), expected)
}
}
func TestDecodeEmbeddedJSON(t *testing.T) {
t.Run("valid embedded JSON", func(t *testing.T) {
// Test embedded JSON tag: 0xD9 0x01 0x06 (tag 262) followed by byte string.
// 0xD9 = major type 6 (tags) + additional type 25 (uint16 follows)
// 0x01 0x06 = 262 (additionalTypeEmbeddedJSON)
// 0x47 = major type 2 (byte string) + length 7
// {"a":1} = embedded JSON payload (no surrounding quotes expected)
embeddedJSON := []byte("\xd9\x01\x06\x47{\"a\":1}")
expected := "{\"a\":1}"
got := decodeTagData(getReader(string(embeddedJSON)))
if string(got) != expected {
t.Errorf("decodeTagData(embedded JSON) = %q, want %q", string(got), expected)
}
})
t.Run("unsupported embedded type panics", func(t *testing.T) {
// Same embedded JSON tag, but followed by a UTF-8 string instead of a byte string.
// This should hit the "Unsupported embedded Type" panic branch.
bad := []byte("\xd9\x01\x06\x61x")
defer func() {
if r := recover(); r == nil {
t.Fatalf("expected panic, got none")
}
}()
_ = decodeTagData(getReader(string(bad)))
})
}
func TestDecodeHexString(t *testing.T) {
// Test hex string tag: 0xD9 0x01 0x07 (tag 263) followed by byte string
// 0xD9 = major type 6 (tags) + additional type 25 (uint16 follows)
// 0x01 0x07 = 263 (additionalTypeTagHexString)
// 0x43 = major type 2 (byte string) + length 3
// 0x01 0x02 0x03 = the byte data to hex encode
hexString := []byte("\xd9\x01\x07\x43\x01\x02\x03")
expected := "\"010203\""
got := decodeTagData(getReader(string(hexString)))
if string(got) != expected {
t.Errorf("decodeTagData(hex string) = %q, want %q", string(got), expected)
}
}
func TestDecodeSimpleFloat(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
// Boolean and null cases (already covered)
{"true", "\xf5", "true"},
{"false", "\xf4", "false"},
{"null", "\xf6", "null"},
// Float32 cases
{"float32 1.0", "\xfa\x3f\x80\x00\x00", "1"},
{"float32 1.5", "\xfa\x3f\xc0\x00\x00", "1.5"},
{"float32 +Inf", "\xfa\x7f\x80\x00\x00", "\"+Inf\""},
{"float32 -Inf", "\xfa\xff\x80\x00\x00", "\"-Inf\""},
{"float32 NaN", "\xfa\x7f\xc0\x00\x00", "\"NaN\""},
// Float64 cases
{"float64 1.0", "\xfb\x3f\xf0\x00\x00\x00\x00\x00\x00", "1"},
{"float64 +Inf", "\xfb\x7f\xf0\x00\x00\x00\x00\x00\x00", "\"+Inf\""},
{"float64 -Inf", "\xfb\xff\xf0\x00\x00\x00\x00\x00\x00", "\"-Inf\""},
{"float64 NaN", "\xfb\x7f\xf8\x00\x00\x00\x00\x00\x00", "\"NaN\""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := decodeSimpleFloat(getReader(tt.input))
if string(got) != tt.want {
t.Errorf("decodeSimpleFloat(%q) = %q, want %q", tt.input, string(got), tt.want)
}
})
}
}