github.com/hoveychen/kafka-go@v0.4.42/message_test.go (about)

     1  package kafka
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"encoding/hex"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"math/rand"
    11  	"os"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/hoveychen/kafka-go/compress/gzip"
    16  	"github.com/hoveychen/kafka-go/compress/lz4"
    17  	"github.com/hoveychen/kafka-go/compress/snappy"
    18  	"github.com/hoveychen/kafka-go/compress/zstd"
    19  	"github.com/stretchr/testify/require"
    20  )
    21  
    22  // This regression test covers reading messages using offsets that
    23  // are at the beginning and in the middle of compressed and uncompressed
    24  // v1 message sets.
    25  func TestV1BatchOffsets(t *testing.T) {
    26  	const highWatermark = 5000
    27  	const topic = "test-topic"
    28  	var (
    29  		msg0 = Message{
    30  			Offset: 0,
    31  			Key:    []byte("msg-0"),
    32  			Value:  []byte("key-0"),
    33  		}
    34  		msg1 = Message{
    35  			Offset: 1,
    36  			Key:    []byte("msg-1"),
    37  			Value:  []byte("key-1"),
    38  		}
    39  		msg2 = Message{
    40  			Offset: 2,
    41  			Key:    []byte("msg-2"),
    42  			Value:  []byte("key-2"),
    43  		}
    44  	)
    45  
    46  	for _, tc := range []struct {
    47  		name     string
    48  		builder  fetchResponseBuilder
    49  		offset   int64
    50  		expected []Message
    51  		debug    bool
    52  	}{
    53  		{
    54  			name:   "num=1 off=0",
    55  			offset: 0,
    56  			builder: fetchResponseBuilder{
    57  				header: fetchResponseHeader{
    58  					highWatermarkOffset: highWatermark,
    59  					lastStableOffset:    highWatermark,
    60  					topic:               topic,
    61  				},
    62  				msgSets: []messageSetBuilder{
    63  					v1MessageSetBuilder{
    64  						msgs: []Message{msg0},
    65  					},
    66  				},
    67  			},
    68  			expected: []Message{msg0},
    69  		},
    70  		{
    71  			name:   "num=1 off=0 compressed",
    72  			offset: 0,
    73  			builder: fetchResponseBuilder{
    74  				header: fetchResponseHeader{
    75  					highWatermarkOffset: highWatermark,
    76  					lastStableOffset:    highWatermark,
    77  					topic:               topic,
    78  				},
    79  				msgSets: []messageSetBuilder{
    80  					v1MessageSetBuilder{
    81  						codec: new(gzip.Codec),
    82  						msgs:  []Message{msg0},
    83  					},
    84  				},
    85  			},
    86  			expected: []Message{msg0},
    87  		},
    88  		{
    89  			name:   "num=1 off=1",
    90  			offset: 1,
    91  			builder: fetchResponseBuilder{
    92  				header: fetchResponseHeader{
    93  					highWatermarkOffset: highWatermark,
    94  					lastStableOffset:    highWatermark,
    95  					topic:               topic,
    96  				},
    97  				msgSets: []messageSetBuilder{
    98  					v1MessageSetBuilder{
    99  						msgs: []Message{msg1},
   100  					},
   101  				},
   102  			},
   103  			expected: []Message{msg1},
   104  		},
   105  		{
   106  			name:   "num=1 off=1 compressed",
   107  			offset: 1,
   108  			builder: fetchResponseBuilder{
   109  				header: fetchResponseHeader{
   110  					highWatermarkOffset: highWatermark,
   111  					lastStableOffset:    highWatermark,
   112  					topic:               topic,
   113  				},
   114  				msgSets: []messageSetBuilder{
   115  					v1MessageSetBuilder{
   116  						codec: new(gzip.Codec),
   117  						msgs:  []Message{msg1},
   118  					},
   119  				},
   120  			},
   121  			expected: []Message{msg1},
   122  		},
   123  		{
   124  			name:   "num=3 off=0",
   125  			offset: 0,
   126  			builder: fetchResponseBuilder{
   127  				header: fetchResponseHeader{
   128  					highWatermarkOffset: highWatermark,
   129  					lastStableOffset:    highWatermark,
   130  					topic:               topic,
   131  				},
   132  				msgSets: []messageSetBuilder{
   133  					v1MessageSetBuilder{
   134  						msgs: []Message{msg0, msg1, msg2},
   135  					},
   136  				},
   137  			},
   138  			expected: []Message{msg0, msg1, msg2},
   139  		},
   140  		{
   141  			name:   "num=3 off=0 compressed",
   142  			offset: 0,
   143  			builder: fetchResponseBuilder{
   144  				header: fetchResponseHeader{
   145  					highWatermarkOffset: highWatermark,
   146  					lastStableOffset:    highWatermark,
   147  					topic:               topic,
   148  				},
   149  				msgSets: []messageSetBuilder{
   150  					v1MessageSetBuilder{
   151  						codec: new(gzip.Codec),
   152  						msgs:  []Message{msg0, msg1, msg2},
   153  					},
   154  				},
   155  			},
   156  			expected: []Message{msg0, msg1, msg2},
   157  		},
   158  		{
   159  			name:   "num=3 off=1",
   160  			offset: 1,
   161  			debug:  true,
   162  			builder: fetchResponseBuilder{
   163  				header: fetchResponseHeader{
   164  					highWatermarkOffset: highWatermark,
   165  					lastStableOffset:    highWatermark,
   166  					topic:               topic,
   167  				},
   168  				msgSets: []messageSetBuilder{
   169  					v1MessageSetBuilder{
   170  						msgs: []Message{msg0, msg1, msg2},
   171  					},
   172  				},
   173  			},
   174  			expected: []Message{msg1, msg2},
   175  		},
   176  		{
   177  			name:   "num=3 off=1 compressed",
   178  			offset: 1,
   179  			debug:  true,
   180  			builder: fetchResponseBuilder{
   181  				header: fetchResponseHeader{
   182  					highWatermarkOffset: highWatermark,
   183  					lastStableOffset:    highWatermark,
   184  					topic:               topic,
   185  				},
   186  				msgSets: []messageSetBuilder{
   187  					v1MessageSetBuilder{
   188  						codec: new(gzip.Codec),
   189  						msgs:  []Message{msg0, msg1, msg2},
   190  					},
   191  				},
   192  			},
   193  			expected: []Message{msg1, msg2},
   194  		},
   195  		{
   196  			name:   "num=3 off=2 compressed",
   197  			offset: 2,
   198  			debug:  true,
   199  			builder: fetchResponseBuilder{
   200  				header: fetchResponseHeader{
   201  					highWatermarkOffset: highWatermark,
   202  					lastStableOffset:    highWatermark,
   203  					topic:               topic,
   204  				},
   205  				msgSets: []messageSetBuilder{
   206  					v1MessageSetBuilder{
   207  						codec: new(gzip.Codec),
   208  						msgs:  []Message{msg0, msg1, msg2},
   209  					},
   210  				},
   211  			},
   212  			expected: []Message{msg2},
   213  		},
   214  	} {
   215  		t.Run(tc.name, func(t *testing.T) {
   216  			bs := tc.builder.bytes()
   217  			r, err := newReaderHelper(t, bs)
   218  			require.NoError(t, err)
   219  			r.offset = tc.offset
   220  			r.debug = tc.debug
   221  			filter := func(msg Message) (res Message) {
   222  				res.Offset = msg.Offset
   223  				res.Key = msg.Key
   224  				res.Value = msg.Value
   225  				return res
   226  			}
   227  			for _, expected := range tc.expected {
   228  				msg := filter(r.readMessage())
   229  				require.EqualValues(t, expected, msg)
   230  			}
   231  			// finally, verify no more bytes remain
   232  			require.EqualValues(t, 0, r.remain)
   233  			_, err = r.readMessageErr()
   234  			require.EqualError(t, err, errShortRead.Error())
   235  		})
   236  	}
   237  }
   238  
   239  func TestMessageSetReader(t *testing.T) {
   240  	const startOffset = 1000
   241  	const highWatermark = 5000
   242  	const topic = "test-topic"
   243  	msgs := make([]Message, 100)
   244  	for i := 0; i < 100; i++ {
   245  		msgs[i] = Message{
   246  			Time:   time.Now(),
   247  			Offset: int64(i + startOffset),
   248  			Key:    []byte(fmt.Sprintf("key-%d", i)),
   249  			Value:  []byte(fmt.Sprintf("val-%d", i)),
   250  			Headers: []Header{
   251  				{
   252  					Key:   fmt.Sprintf("header-key-%d", i),
   253  					Value: []byte(fmt.Sprintf("header-value-%d", i)),
   254  				},
   255  			},
   256  		}
   257  	}
   258  	defaultHeader := fetchResponseHeader{
   259  		highWatermarkOffset: highWatermark,
   260  		lastStableOffset:    highWatermark,
   261  		topic:               topic,
   262  	}
   263  	for _, tc := range []struct {
   264  		name    string
   265  		builder fetchResponseBuilder
   266  		err     error
   267  		debug   bool
   268  	}{
   269  		{
   270  			name: "empty",
   271  			builder: fetchResponseBuilder{
   272  				header: defaultHeader,
   273  			},
   274  			err: errShortRead,
   275  		},
   276  		{
   277  			name: "v0",
   278  			builder: fetchResponseBuilder{
   279  				header: defaultHeader,
   280  				msgSets: []messageSetBuilder{
   281  					v0MessageSetBuilder{
   282  						msgs: []Message{msgs[0]},
   283  					},
   284  				},
   285  			},
   286  		},
   287  		{
   288  			name: "v0 compressed",
   289  			builder: fetchResponseBuilder{
   290  				header: defaultHeader,
   291  				msgSets: []messageSetBuilder{
   292  					v0MessageSetBuilder{
   293  						codec: new(gzip.Codec),
   294  						msgs:  []Message{msgs[0]},
   295  					},
   296  				},
   297  			},
   298  		},
   299  		{
   300  			name: "v1",
   301  			builder: fetchResponseBuilder{
   302  				header: defaultHeader,
   303  				msgSets: []messageSetBuilder{
   304  					v1MessageSetBuilder{
   305  						msgs: []Message{msgs[0]},
   306  					},
   307  				},
   308  			},
   309  		},
   310  		{
   311  			name: "v1 compressed",
   312  			builder: fetchResponseBuilder{
   313  				header: defaultHeader,
   314  				msgSets: []messageSetBuilder{
   315  					v1MessageSetBuilder{
   316  						codec: new(gzip.Codec),
   317  						msgs:  []Message{msgs[0]},
   318  					},
   319  				},
   320  			},
   321  		},
   322  		{
   323  			name: "v2",
   324  			builder: fetchResponseBuilder{
   325  				header: defaultHeader,
   326  				msgSets: []messageSetBuilder{
   327  					v2MessageSetBuilder{
   328  						msgs: []Message{msgs[0]},
   329  					},
   330  				},
   331  			},
   332  		},
   333  		{
   334  			name: "v2 compressed",
   335  			builder: fetchResponseBuilder{
   336  				header: defaultHeader,
   337  				msgSets: []messageSetBuilder{
   338  					v2MessageSetBuilder{
   339  						codec: new(zstd.Codec),
   340  						msgs:  []Message{msgs[0]},
   341  					},
   342  				},
   343  			},
   344  		},
   345  		{
   346  			name: "v2 multiple messages",
   347  			builder: fetchResponseBuilder{
   348  				header: defaultHeader,
   349  				msgSets: []messageSetBuilder{
   350  					v2MessageSetBuilder{
   351  						msgs: []Message{msgs[0], msgs[1], msgs[2], msgs[3], msgs[4]},
   352  					},
   353  				},
   354  			},
   355  		},
   356  		{
   357  			name: "v2 multiple messages compressed",
   358  			builder: fetchResponseBuilder{
   359  				header: defaultHeader,
   360  				msgSets: []messageSetBuilder{
   361  					v2MessageSetBuilder{
   362  						codec: new(snappy.Codec),
   363  						msgs:  []Message{msgs[0], msgs[1], msgs[2], msgs[3], msgs[4]},
   364  					},
   365  				},
   366  			},
   367  		},
   368  		{
   369  			name: "v2 mix of compressed and uncompressed message sets",
   370  			builder: fetchResponseBuilder{
   371  				header: defaultHeader,
   372  				msgSets: []messageSetBuilder{
   373  					v2MessageSetBuilder{
   374  						codec: new(snappy.Codec),
   375  						msgs:  []Message{msgs[0], msgs[1], msgs[2], msgs[3], msgs[4]},
   376  					},
   377  					v2MessageSetBuilder{
   378  						msgs: []Message{msgs[5], msgs[6], msgs[7], msgs[8], msgs[9]},
   379  					},
   380  					v2MessageSetBuilder{
   381  						codec: new(snappy.Codec),
   382  						msgs:  []Message{msgs[10], msgs[11], msgs[12], msgs[13], msgs[14]},
   383  					},
   384  					v2MessageSetBuilder{
   385  						msgs: []Message{msgs[15], msgs[16], msgs[17], msgs[18], msgs[19]},
   386  					},
   387  				},
   388  			},
   389  		},
   390  		{
   391  			name: "v0 v2 v1 v2 v1 v1 v0 v2",
   392  			builder: fetchResponseBuilder{
   393  				header: defaultHeader,
   394  				msgSets: []messageSetBuilder{
   395  					v0MessageSetBuilder{
   396  						msgs: []Message{msgs[0]},
   397  					},
   398  					v2MessageSetBuilder{
   399  						msgs: []Message{msgs[1], msgs[2]},
   400  					},
   401  					v1MessageSetBuilder{
   402  						msgs: []Message{msgs[3]},
   403  					},
   404  					v2MessageSetBuilder{
   405  						msgs: []Message{msgs[4], msgs[5]},
   406  					},
   407  					v1MessageSetBuilder{
   408  						msgs: []Message{msgs[6]},
   409  					},
   410  					v1MessageSetBuilder{
   411  						msgs: []Message{msgs[7]},
   412  					},
   413  					v0MessageSetBuilder{
   414  						msgs: []Message{msgs[8]},
   415  					},
   416  					v2MessageSetBuilder{
   417  						msgs: []Message{msgs[9], msgs[10]},
   418  					},
   419  				},
   420  			},
   421  		},
   422  		{
   423  			name: "v0 v2 v1 v2 v1 v1 v0 v2 mixed compression",
   424  			builder: fetchResponseBuilder{
   425  				header: defaultHeader,
   426  				msgSets: []messageSetBuilder{
   427  					v0MessageSetBuilder{
   428  						codec: new(gzip.Codec),
   429  						msgs:  []Message{msgs[0]},
   430  					},
   431  					v2MessageSetBuilder{
   432  						codec: new(zstd.Codec),
   433  						msgs:  []Message{msgs[1], msgs[2]},
   434  					},
   435  					v1MessageSetBuilder{
   436  						codec: new(snappy.Codec),
   437  						msgs:  []Message{msgs[3]},
   438  					},
   439  					v2MessageSetBuilder{
   440  						codec: new(lz4.Codec),
   441  						msgs:  []Message{msgs[4], msgs[5]},
   442  					},
   443  					v1MessageSetBuilder{
   444  						codec: new(gzip.Codec),
   445  						msgs:  []Message{msgs[6]},
   446  					},
   447  					v1MessageSetBuilder{
   448  						codec: new(zstd.Codec),
   449  						msgs:  []Message{msgs[7]},
   450  					},
   451  					v0MessageSetBuilder{
   452  						codec: new(snappy.Codec),
   453  						msgs:  []Message{msgs[8]},
   454  					},
   455  					v2MessageSetBuilder{
   456  						codec: new(lz4.Codec),
   457  						msgs:  []Message{msgs[9], msgs[10]},
   458  					},
   459  				},
   460  			},
   461  		},
   462  		{
   463  			name: "v0 v2 v1 v2 v1 v1 v0 v2 mixed compression with non-compressed",
   464  			builder: fetchResponseBuilder{
   465  				header: defaultHeader,
   466  				msgSets: []messageSetBuilder{
   467  					v0MessageSetBuilder{
   468  						codec: new(gzip.Codec),
   469  						msgs:  []Message{msgs[0]},
   470  					},
   471  					v2MessageSetBuilder{
   472  						msgs: []Message{msgs[1], msgs[2]},
   473  					},
   474  					v1MessageSetBuilder{
   475  						codec: new(snappy.Codec),
   476  						msgs:  []Message{msgs[3]},
   477  					},
   478  					v2MessageSetBuilder{
   479  						msgs: []Message{msgs[4], msgs[5]},
   480  					},
   481  					v1MessageSetBuilder{
   482  						msgs: []Message{msgs[6]},
   483  					},
   484  					v1MessageSetBuilder{
   485  						codec: new(zstd.Codec),
   486  						msgs:  []Message{msgs[7]},
   487  					},
   488  					v0MessageSetBuilder{
   489  						msgs: []Message{msgs[8]},
   490  					},
   491  					v2MessageSetBuilder{
   492  						codec: new(lz4.Codec),
   493  						msgs:  []Message{msgs[9], msgs[10]},
   494  					},
   495  				},
   496  			},
   497  		},
   498  	} {
   499  		t.Run(tc.name, func(t *testing.T) {
   500  			rh, err := newReaderHelper(t, tc.builder.bytes())
   501  			require.Equal(t, tc.err, err)
   502  			if tc.err != nil {
   503  				return
   504  			}
   505  			rh.debug = tc.debug
   506  			for _, messageSet := range tc.builder.msgSets {
   507  				for _, expected := range messageSet.messages() {
   508  					msg := rh.readMessage()
   509  					require.Equal(t, expected.Offset, msg.Offset)
   510  					require.Equal(t, string(expected.Key), string(msg.Key))
   511  					require.Equal(t, string(expected.Value), string(msg.Value))
   512  					switch messageSet.(type) {
   513  					case v0MessageSetBuilder, v1MessageSetBuilder:
   514  						// v0 and v1 message sets do not have headers
   515  						require.Len(t, msg.Headers, 0)
   516  					case v2MessageSetBuilder:
   517  						// v2 message sets can have headers
   518  						require.EqualValues(t, expected.Headers, msg.Headers)
   519  					default:
   520  						t.Fatalf("unknown builder: %T", messageSet)
   521  					}
   522  					require.Equal(t, expected.Offset, msg.Offset)
   523  				}
   524  			}
   525  			// verify the reader stack is empty
   526  			require.EqualValues(t, 0, rh.remain)
   527  			require.EqualValues(t, 0, rh.count)
   528  			require.EqualValues(t, 0, rh.remaining())
   529  			require.Nil(t, rh.readerStack.parent)
   530  			// any further message is a short read
   531  			_, err = rh.readMessageErr()
   532  			require.EqualError(t, err, errShortRead.Error())
   533  		})
   534  	}
   535  
   536  }
   537  
   538  func TestMessageSetReaderEmpty(t *testing.T) {
   539  	m := messageSetReader{empty: true}
   540  
   541  	noop := func(*bufio.Reader, int, int) (int, error) {
   542  		return 0, nil
   543  	}
   544  
   545  	offset, _, timestamp, headers, err := m.readMessage(0, noop, noop)
   546  	if offset != 0 {
   547  		t.Errorf("expected offset of 0, get %d", offset)
   548  	}
   549  	if timestamp != 0 {
   550  		t.Errorf("expected timestamp of 0, get %d", timestamp)
   551  	}
   552  	if headers != nil {
   553  		t.Errorf("expected nil headers, got %v", headers)
   554  	}
   555  	if !errors.Is(err, RequestTimedOut) {
   556  		t.Errorf("expected RequestTimedOut, got %v", err)
   557  	}
   558  
   559  	if m.remaining() != 0 {
   560  		t.Errorf("expected 0 remaining, got %d", m.remaining())
   561  	}
   562  
   563  	if m.discard() != nil {
   564  		t.Errorf("unexpected error from discard(): %v", m.discard())
   565  	}
   566  }
   567  
   568  func TestMessageFixtures(t *testing.T) {
   569  	type fixtureMessage struct {
   570  		key   string
   571  		value string
   572  	}
   573  	var fixtureMessages = map[string]fixtureMessage{
   574  		"a": {key: "alpha", value: `{"count":0,"filler":"aaaaaaaaaa"}`},
   575  		"b": {key: "beta", value: `{"count":0,"filler":"bbbbbbbbbb"}`},
   576  		"c": {key: "gamma", value: `{"count":0,"filler":"cccccccccc"}`},
   577  		"d": {key: "delta", value: `{"count":0,"filler":"dddddddddd"}`},
   578  		"e": {key: "epsilon", value: `{"count":0,"filler":"eeeeeeeeee"}`},
   579  		"f": {key: "zeta", value: `{"count":0,"filler":"ffffffffff"}`},
   580  		"g": {key: "eta", value: `{"count":0,"filler":"gggggggggg"}`},
   581  		"h": {key: "theta", value: `{"count":0,"filler":"hhhhhhhhhh"}`},
   582  	}
   583  
   584  	for _, tc := range []struct {
   585  		name     string
   586  		file     string
   587  		messages []string
   588  	}{
   589  		{
   590  			name:     "v2 followed by v1",
   591  			file:     "fixtures/v2b-v1.hex",
   592  			messages: []string{"a", "b", "a", "b"},
   593  		},
   594  		{
   595  			name:     "v2 compressed followed by v1 compressed",
   596  			file:     "fixtures/v2bc-v1c.hex",
   597  			messages: []string{"a", "b", "a", "b"},
   598  		},
   599  		{
   600  			name:     "v2 compressed followed by v1 uncompressed",
   601  			file:     "fixtures/v2bc-v1.hex",
   602  			messages: []string{"a", "b", "c", "d"},
   603  		},
   604  		{
   605  			name:     "v2 compressed followed by v1 uncompressed then v1 compressed",
   606  			file:     "fixtures/v2bc-v1-v1c.hex",
   607  			messages: []string{"a", "b", "c", "d", "e", "f"},
   608  		},
   609  		{
   610  			name:     "v2 compressed followed by v1 uncompressed then v1 compressed",
   611  			file:     "fixtures/v2bc-v1-v1c.hex",
   612  			messages: []string{"a", "b", "c", "d", "e", "f"},
   613  		},
   614  		{
   615  			name:     "v1 followed by v1",
   616  			file:     "fixtures/v1-v1.hex",
   617  			messages: []string{"a", "b", "c", "d"},
   618  		},
   619  		{
   620  			name:     "v1 compressed followed by v1 compressed",
   621  			file:     "fixtures/v1c-v1c.hex",
   622  			messages: []string{"a", "b", "c", "d"},
   623  		},
   624  		{
   625  			name:     "v1 compressed followed by v1 uncompressed then v1 compressed",
   626  			file:     "fixtures/v1c-v1-v1c.hex",
   627  			messages: []string{"a", "b", "c", "d", "e", "f"},
   628  		},
   629  		{
   630  			name:     "v2 followed by v2",
   631  			file:     "fixtures/v2-v2.hex",
   632  			messages: []string{"a", "b", "c", "d"},
   633  		},
   634  		{
   635  			name:     "v2 compressed followed by v2 compressed",
   636  			file:     "fixtures/v2c-v2c.hex",
   637  			messages: []string{"a", "b", "c", "d"},
   638  		},
   639  		{
   640  			name:     "v2 compressed followed by v2 uncompressed then v2 compressed",
   641  			file:     "fixtures/v2c-v2-v2c.hex",
   642  			messages: []string{"a", "b", "c", "d", "e", "f"},
   643  		},
   644  		{
   645  			name:     "v1 followed by v2 followed by v1 with mixture of compressed and uncompressed",
   646  			file:     "fixtures/v1-v1c-v2-v2c-v2b-v2b-v2b-v2bc-v1b-v1bc.hex",
   647  			messages: []string{"a", "b", "a", "b", "c", "d", "c", "d", "e", "f", "e", "f", "g", "h", "g", "h", "g", "h", "g", "h"},
   648  		},
   649  	} {
   650  		t.Run(tc.name, func(t *testing.T) {
   651  			bs, err := os.ReadFile(tc.file)
   652  			require.NoError(t, err)
   653  			buf := new(bytes.Buffer)
   654  			_, err = io.Copy(buf, hex.NewDecoder(bytes.NewReader(bs)))
   655  			require.NoError(t, err)
   656  
   657  			// discard 4 byte len and 4 byte correlation id
   658  			bs = make([]byte, 8)
   659  			buf.Read(bs)
   660  
   661  			rh, err := newReaderHelper(t, buf.Bytes())
   662  			require.NoError(t, err)
   663  			messageCount := 0
   664  			expectedMessageCount := len(tc.messages)
   665  			for _, expectedMessageId := range tc.messages {
   666  				expectedMessage := fixtureMessages[expectedMessageId]
   667  				msg := rh.readMessage()
   668  				messageCount++
   669  				require.Equal(t, expectedMessage.key, string(msg.Key))
   670  				require.Equal(t, expectedMessage.value, string(msg.Value))
   671  				t.Logf("Message %d key & value are what we expected: %s -> %s\n",
   672  					messageCount, string(msg.Key), string(msg.Value))
   673  			}
   674  			require.Equal(t, expectedMessageCount, messageCount)
   675  		})
   676  	}
   677  }
   678  
   679  func TestMessageSize(t *testing.T) {
   680  	rand.Seed(time.Now().UnixNano())
   681  	for i := 0; i < 20; i++ {
   682  		t.Run("Run", func(t *testing.T) {
   683  			msg := Message{
   684  				Key:   make([]byte, rand.Intn(200)),
   685  				Value: make([]byte, rand.Intn(200)),
   686  				Time:  randate(),
   687  			}
   688  			expSize := msg.message(nil).size()
   689  			gotSize := msg.size()
   690  			if expSize != gotSize {
   691  				t.Errorf("Expected size %d, but got size %d", expSize, gotSize)
   692  			}
   693  		})
   694  	}
   695  
   696  }
   697  
   698  // https://stackoverflow.com/questions/43495745/how-to-generate-random-date-in-go-lang/43497333#43497333
   699  func randate() time.Time {
   700  	min := time.Date(1970, 1, 0, 0, 0, 0, 0, time.UTC).Unix()
   701  	max := time.Date(2070, 1, 0, 0, 0, 0, 0, time.UTC).Unix()
   702  	delta := max - min
   703  
   704  	sec := rand.Int63n(delta) + min
   705  	return time.Unix(sec, 0)
   706  }
   707  
   708  // readerHelper composes a messageSetReader to provide convenience methods to read
   709  // messages.
   710  type readerHelper struct {
   711  	t *testing.T
   712  	*messageSetReader
   713  	offset int64
   714  }
   715  
   716  func newReaderHelper(t *testing.T, bs []byte) (r *readerHelper, err error) {
   717  	bufReader := bufio.NewReader(bytes.NewReader(bs))
   718  	_, _, remain, err := readFetchResponseHeaderV10(bufReader, len(bs))
   719  	require.NoError(t, err)
   720  	var msgs *messageSetReader
   721  	msgs, err = newMessageSetReader(bufReader, remain)
   722  	if err != nil {
   723  		return
   724  	}
   725  	r = &readerHelper{t: t, messageSetReader: msgs}
   726  	require.Truef(t, msgs.remaining() > 0, "remaining should be > 0 but was %d", msgs.remaining())
   727  	return
   728  }
   729  
   730  func (r *readerHelper) readMessageErr() (msg Message, err error) {
   731  	keyFunc := func(r *bufio.Reader, size int, nbytes int) (remain int, err error) {
   732  		msg.Key, remain, err = readNewBytes(r, size, nbytes)
   733  		return
   734  	}
   735  	valueFunc := func(r *bufio.Reader, size int, nbytes int) (remain int, err error) {
   736  		msg.Value, remain, err = readNewBytes(r, size, nbytes)
   737  		return
   738  	}
   739  	var timestamp int64
   740  	var headers []Header
   741  	r.offset, _, timestamp, headers, err = r.messageSetReader.readMessage(r.offset, keyFunc, valueFunc)
   742  	if err != nil {
   743  		return
   744  	}
   745  	msg.Offset = r.offset
   746  	msg.Time = time.Unix(timestamp/1000, (timestamp%1000)*1000000)
   747  	msg.Headers = headers
   748  	return
   749  }
   750  
   751  func (r *readerHelper) readMessage() (msg Message) {
   752  	var err error
   753  	msg, err = r.readMessageErr()
   754  	require.NoError(r.t, err)
   755  	return
   756  }