From 76e60731a632713cc2f3dc8bdcff3a6c6a4e52f8 Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Tue, 27 Aug 2024 17:39:46 +0800 Subject: [PATCH] move away from Ginkgo --- .github/workflows/test.yml | 18 +- README.md | 2 +- decoder_test.go | 433 +++++++++--------- encoder_test.go | 296 ++++++------ go.mod | 12 +- go.sum | 36 +- header_field_test.go | 24 +- .../interop/interop_suite_test.go | 137 +++++- integrationtests/interop/interop_test.go | 93 ---- integrationtests/self/integration_test.go | 359 ++++++++------- integrationtests/self/self_suite_test.go | 31 -- qpack_suite_test.go | 13 - static_table_test.go | 47 +- tools.go | 5 - 14 files changed, 723 insertions(+), 783 deletions(-) delete mode 100644 integrationtests/interop/interop_test.go delete mode 100644 integrationtests/self/self_suite_test.go delete mode 100644 qpack_suite_test.go delete mode 100644 tools.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 29a8fef..d80bf86 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,16 +13,12 @@ jobs: with: go-version: ${{ matrix.go }} - run: go version - - name: Install dependencies - run: go build - name: Run tests - run: go run github.com/onsi/ginkgo/v2/ginkgo -r -v -cover -randomize-all -randomize-suites -trace -skip-package integrationtests + run: go test -v -cover -race -shuffle=on . - name: Run tests (32 bit) env: GOARCH: 386 - run: go run github.com/onsi/ginkgo/v2/ginkgo -r -v -cover -coverprofile coverage.txt -output-dir . -randomize-all -randomize-suites -trace -skip-package integrationtests - - name: Run tests with race detector - run: go run github.com/onsi/ginkgo/v2/ginkgo -r -v -race -randomize-all -randomize-suites -trace -skip-package integrationtests + run: go test -v -cover -coverprofile=coverage.txt -shuffle=on . - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: @@ -37,15 +33,15 @@ jobs: name: Integration tests (Go ${{ matrix.go }}) steps: - uses: actions/checkout@v4 + submodules: 'recursive' - uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - run: go version - - name: Install dependencies - run: go build - - name: Run tests + - name: Run interop tests + run: go test -v -race -shuffle=on ./integrationtests/interop + - name: Run integration tests run: | for i in {1..25}; do - go run github.com/onsi/ginkgo/v2/ginkgo -race -v -randomize-all -trace integrationtests/self; + go test -v -race -shuffle=on ./integrationtests/self done - diff --git a/README.md b/README.md index 0beba29..70fa448 100644 --- a/README.md +++ b/README.md @@ -16,5 +16,5 @@ git submodule update --init --recursive Then run the tests: ```bash -ginkgo -r integrationtests +go test -v ./integrationtests/interop/ ``` diff --git a/decoder_test.go b/decoder_test.go index f56f588..85980a9 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -2,225 +2,244 @@ package qpack import ( "bytes" + "testing" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" + "github.com/stretchr/testify/require" "golang.org/x/net/http2/hpack" ) -var _ = Describe("Decoder", func() { - var ( - decoder *Decoder - headerFields []HeaderField - ) +func insertPrefix(data []byte) []byte { + prefix := appendVarInt(nil, 8, 0) + prefix = appendVarInt(prefix, 7, 0) + return append(prefix, data...) +} - BeforeEach(func() { - headerFields = nil - decoder = NewDecoder(func(hf HeaderField) { - headerFields = append(headerFields, hf) - }) +func TestDecoderIndexedHeaderField(t *testing.T) { + var headerFields []HeaderField + decoder := NewDecoder(func(hf HeaderField) { + headerFields = append(headerFields, hf) }) - - insertPrefix := func(data []byte) []byte { - prefix := appendVarInt(nil, 8, 0) - prefix = appendVarInt(prefix, 7, 0) - return append(prefix, data...) + data := appendVarInt(nil, 6, 20) + data[0] ^= 0x80 | 0x40 + _, err := decoder.Write(insertPrefix(data)) + require.NoError(t, err) + require.Len(t, headerFields, 1) + require.Equal(t, staticTableEntries[20], headerFields[0]) +} + +func TestDecoderRejections(t *testing.T) { + tests := []struct { + name string + setup func() (*Decoder, []byte) + expected string + }{ + { + name: "non-zero required insert count", + setup: func() (*Decoder, []byte) { + decoder := NewDecoder(nil) + prefix := appendVarInt(nil, 8, 1) + prefix = appendVarInt(prefix, 7, 0) + return decoder, prefix + }, + expected: "decoding error: expected Required Insert Count to be zero", + }, + { + name: "non-zero delta base", + setup: func() (*Decoder, []byte) { + decoder := NewDecoder(nil) + prefix := appendVarInt(nil, 8, 0) + prefix = appendVarInt(prefix, 7, 1) + return decoder, prefix + }, + expected: "decoding error: expected Base to be zero", + }, + { + name: "unknown type byte", + setup: func() (*Decoder, []byte) { + decoder := NewDecoder(nil) + return decoder, insertPrefix([]byte{0x10}) + }, + expected: "unexpected type byte: 0x10", + }, + { + name: "indexed header field that referencing the dynamic table", + setup: func() (*Decoder, []byte) { + decoder := NewDecoder(nil) + data := appendVarInt(nil, 6, 20) + data[0] ^= 0x80 // don't set the static flag (0x40) + return decoder, insertPrefix(data) + }, + expected: errNoDynamicTable.Error(), + }, + { + name: "non-existent static table entry", + setup: func() (*Decoder, []byte) { + var headerFields []HeaderField + decoder := NewDecoder(func(hf HeaderField) { + headerFields = append(headerFields, hf) + }) + data := appendVarInt(nil, 6, 10000) + data[0] ^= 0x80 | 0x40 + return decoder, insertPrefix(data) + }, + expected: "decoding error: invalid indexed representation index 10000", + }, } - doPartialWrites := func(data []byte) { - for i := 0; i < len(data)-1; i++ { - n, err := decoder.Write([]byte{data[i]}) - Expect(err).ToNot(HaveOccurred()) - Expect(n).To(Equal(1)) - Expect(headerFields).To(BeEmpty()) - } - n, err := decoder.Write([]byte{data[len(data)-1]}) - Expect(err).ToNot(HaveOccurred()) - Expect(n).To(Equal(1)) - Expect(headerFields).To(HaveLen(1)) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + decoder, data := tt.setup() + _, err := decoder.Write(data) + require.EqualError(t, err, tt.expected) + }) } +} - It("rejects a non-zero Required Insert Count", func() { - prefix := appendVarInt(nil, 8, 1) - prefix = appendVarInt(prefix, 7, 0) - _, err := decoder.Write(prefix) - Expect(err).To(MatchError("decoding error: expected Required Insert Count to be zero")) +func TestDecoderHandlesPartialWritesForIndexedHeaderField(t *testing.T) { + var headerFields []HeaderField + decoder := NewDecoder(func(hf HeaderField) { + headerFields = append(headerFields, hf) }) - - It("rejects a non-zero Delta Base", func() { - prefix := appendVarInt(nil, 8, 0) - prefix = appendVarInt(prefix, 7, 1) - _, err := decoder.Write(prefix) - Expect(err).To(MatchError("decoding error: expected Base to be zero")) + data := appendVarInt(nil, 6, 20) + data[0] ^= 0x80 | 0x40 + data = insertPrefix(data) + + for i := 0; i < len(data)-1; i++ { + n, err := decoder.Write([]byte{data[i]}) + require.NoError(t, err) + require.Equal(t, 1, n) + require.Empty(t, headerFields) + } + n, err := decoder.Write([]byte{data[len(data)-1]}) + require.NoError(t, err) + require.Equal(t, 1, n) + require.Len(t, headerFields, 1) +} + +func TestDecoderParsesLiteralHeaderFieldWithNameReference(t *testing.T) { + var headerFields []HeaderField + decoder := NewDecoder(func(hf HeaderField) { + headerFields = append(headerFields, hf) }) - - It("rejects unknown type bytes", func() { - _, err := decoder.Write(insertPrefix([]byte{0x10})) - Expect(err).To(MatchError("unexpected type byte: 0x10")) + data := appendVarInt(nil, 4, 49) + data[0] ^= 0x40 | 0x10 + data = appendVarInt(data, 7, 6) + data = append(data, []byte("foobar")...) + _, err := decoder.Write(insertPrefix(data)) + require.NoError(t, err) + require.Len(t, headerFields, 1) + require.Equal(t, "content-type", headerFields[0].Name) + require.Equal(t, "foobar", headerFields[0].Value) +} + +func TestDecoderParsesLiteralHeaderFieldWithNameReferenceAndHuffmanEncoding(t *testing.T) { + var headerFields []HeaderField + decoder := NewDecoder(func(hf HeaderField) { + headerFields = append(headerFields, hf) }) - - Context("indexed header field", func() { - It("parses an indexed header field", func() { - data := appendVarInt(nil, 6, 20) - data[0] ^= 0x80 | 0x40 - _, err := decoder.Write(insertPrefix(data)) - Expect(err).ToNot(HaveOccurred()) - Expect(headerFields).To(HaveLen(1)) - Expect(headerFields[0]).To(Equal(staticTableEntries[20])) - }) - - It("rejects an indexed header field that references the dynamic table", func() { - data := appendVarInt(nil, 6, 20) - data[0] ^= 0x80 // don't set the static flag (0x40) - _, err := decoder.Write(insertPrefix(data)) - Expect(err).To(MatchError(errNoDynamicTable)) - }) - - It("errors when a non-existent static table entry is referenced", func() { - data := appendVarInt(nil, 6, 10000) - data[0] ^= 0x80 | 0x40 - _, err := decoder.Write(insertPrefix(data)) - Expect(err).To(MatchError("decoding error: invalid indexed representation index 10000")) - Expect(headerFields).To(BeEmpty()) - }) - - It("handles partial writes", func() { - data := appendVarInt(nil, 6, 20) - data[0] ^= 0x80 | 0x40 - data = insertPrefix(data) - - doPartialWrites(data) - }) + data := appendVarInt(nil, 4, 49) + data[0] ^= 0x40 | 0x10 + data2 := appendVarInt(nil, 7, hpack.HuffmanEncodeLength("foobar")) + data2[0] ^= 0x80 + data = hpack.AppendHuffmanString(append(data, data2...), "foobar") + _, err := decoder.Write(insertPrefix(data)) + require.NoError(t, err) + require.Len(t, headerFields, 1) + require.Equal(t, "content-type", headerFields[0].Name) + require.Equal(t, "foobar", headerFields[0].Value) +} + +func TestDecoderRejectsLiteralHeaderFieldWithNameReferenceToNonStaticTable(t *testing.T) { + decoder := NewDecoder(nil) + data := appendVarInt(nil, 4, 49) + data[0] ^= 0x40 // don't set the static flag (0x10) + data = appendVarInt(data, 7, 6) + data = append(data, []byte("foobar")...) + _, err := decoder.Write(insertPrefix(data)) + require.Equal(t, errNoDynamicTable, err) +} + +func TestDecoderParsesLiteralHeaderFieldWithoutNameReference(t *testing.T) { + var headerFields []HeaderField + decoder := NewDecoder(func(hf HeaderField) { + headerFields = append(headerFields, hf) }) - - Context("header field with name reference", func() { - It("parses a literal header field with name reference", func() { - data := appendVarInt(nil, 4, 49) - data[0] ^= 0x40 | 0x10 - data = appendVarInt(data, 7, 6) - data = append(data, []byte("foobar")...) - _, err := decoder.Write(insertPrefix(data)) - Expect(err).ToNot(HaveOccurred()) - Expect(headerFields).To(HaveLen(1)) - Expect(headerFields[0].Name).To(Equal("content-type")) - Expect(headerFields[0].Value).To(Equal("foobar")) - }) - - It("parses a literal header field with name reference, with Huffman encoding", func() { - data := appendVarInt(nil, 4, 49) - data[0] ^= 0x40 | 0x10 - data2 := appendVarInt(nil, 7, hpack.HuffmanEncodeLength("foobar")) - data2[0] ^= 0x80 - data = hpack.AppendHuffmanString(append(data, data2...), "foobar") - _, err := decoder.Write(insertPrefix(data)) - Expect(err).ToNot(HaveOccurred()) - Expect(headerFields).To(HaveLen(1)) - Expect(headerFields[0].Name).To(Equal("content-type")) - Expect(headerFields[0].Value).To(Equal("foobar")) - }) - - It("rejects a literal header field with name reference that references the dynamic table", func() { - data := appendVarInt(nil, 4, 49) - data[0] ^= 0x40 // don't set the static flag (0x10) - data = appendVarInt(data, 7, 6) - data = append(data, []byte("foobar")...) - _, err := decoder.Write(insertPrefix(data)) - Expect(err).To(MatchError(errNoDynamicTable)) - }) - - It("handles partial writes", func() { - data := appendVarInt(nil, 4, 49) - data[0] ^= 0x40 | 0x10 - data = appendVarInt(data, 7, 6) - data = append(data, []byte("foobar")...) - data = insertPrefix(data) - - doPartialWrites(data) - }) - - It("handles partial writes, when using Huffman encoding", func() { - data := appendVarInt(nil, 4, 49) - data[0] ^= 0x40 | 0x10 - data2 := appendVarInt(nil, 7, hpack.HuffmanEncodeLength("foobar")) - data2[0] ^= 0x80 - data = hpack.AppendHuffmanString(append(data, data2...), "foobar") - data = insertPrefix(data) - - doPartialWrites(data) - }) - }) - - Context("header field without name reference", func() { - It("parses a literal header field without name reference", func() { - data := appendVarInt(nil, 3, 3) - data[0] ^= 0x20 - data = append(data, []byte("foo")...) - data2 := appendVarInt(nil, 7, 3) - data2 = append(data2, []byte("bar")...) - data = append(data, data2...) - _, err := decoder.Write(insertPrefix(data)) - Expect(err).ToNot(HaveOccurred()) - Expect(headerFields).To(HaveLen(1)) - Expect(headerFields[0].Name).To(Equal("foo")) - Expect(headerFields[0].Value).To(Equal("bar")) - }) - - It("handles partial writes", func() { - data := appendVarInt(nil, 3, 3) - data[0] ^= 0x20 - data = append(data, []byte("foo")...) - data2 := appendVarInt(nil, 7, 3) - data2 = append(data2, []byte("bar")...) - data = append(data, data2...) - data = insertPrefix(data) - - doPartialWrites(data) - }) + data := appendVarInt(nil, 3, 3) + data[0] ^= 0x20 + data = append(data, []byte("foo")...) + data2 := appendVarInt(nil, 7, 3) + data2 = append(data2, []byte("bar")...) + data = append(data, data2...) + _, err := decoder.Write(insertPrefix(data)) + require.NoError(t, err) + require.Len(t, headerFields, 1) + require.Equal(t, "foo", headerFields[0].Name) + require.Equal(t, "bar", headerFields[0].Value) +} + +func TestDecoderHandlesPartialWritesForLiteralHeaderFieldWithoutNameReference(t *testing.T) { + var headerFields []HeaderField + decoder := NewDecoder(func(hf HeaderField) { + headerFields = append(headerFields, hf) }) - - Context("using DecodeFull", func() { - It("decodes nothing", func() { - data, err := NewDecoder(nil).DecodeFull([]byte{}) - Expect(err).ToNot(HaveOccurred()) - Expect(data).To(BeEmpty()) - }) - - It("decodes multiple entries", func() { - buf := &bytes.Buffer{} - enc := NewEncoder(buf) - Expect(enc.WriteField(HeaderField{Name: "foo", Value: "bar"})).To(Succeed()) - Expect(enc.WriteField(HeaderField{Name: "lorem", Value: "ipsum"})).To(Succeed()) - data, err := NewDecoder(nil).DecodeFull(buf.Bytes()) - Expect(err).ToNot(HaveOccurred()) - Expect(data).To(Equal([]HeaderField{ - {Name: "foo", Value: "bar"}, - {Name: "lorem", Value: "ipsum"}, - })) - }) - - It("returns an error if the data is incomplete", func() { - buf := &bytes.Buffer{} - enc := NewEncoder(buf) - Expect(enc.WriteField(HeaderField{Name: "foo", Value: "bar"})).To(Succeed()) - _, err := NewDecoder(nil).DecodeFull(buf.Bytes()[:buf.Len()-2]) - Expect(err).To(MatchError("decoding error: truncated headers")) - }) - - It("restores the emitFunc afterwards", func() { - var emitFuncCalled bool - emitFunc := func(HeaderField) { - emitFuncCalled = true - } - decoder := NewDecoder(emitFunc) - buf := &bytes.Buffer{} - enc := NewEncoder(buf) - Expect(enc.WriteField(HeaderField{Name: "foo", Value: "bar"})).To(Succeed()) - _, err := decoder.DecodeFull(buf.Bytes()) - Expect(err).ToNot(HaveOccurred()) - Expect(emitFuncCalled).To(BeFalse()) - _, err = decoder.Write(buf.Bytes()) - Expect(err).ToNot(HaveOccurred()) - Expect(emitFuncCalled).To(BeTrue()) - }) - }) -}) + data := appendVarInt(nil, 3, 3) + data[0] ^= 0x20 + data = append(data, []byte("foo")...) + data2 := appendVarInt(nil, 7, 3) + data2 = append(data2, []byte("bar")...) + data = append(data, data2...) + data = insertPrefix(data) + + for i := 0; i < len(data)-1; i++ { + n, err := decoder.Write([]byte{data[i]}) + require.NoError(t, err) + require.Equal(t, 1, n) + require.Empty(t, headerFields) + } + n, err := decoder.Write([]byte{data[len(data)-1]}) + require.NoError(t, err) + require.Equal(t, 1, n) + require.Len(t, headerFields, 1) +} + +func TestDecodeFullEmpty(t *testing.T) { + data, err := NewDecoder(nil).DecodeFull([]byte{}) + require.NoError(t, err) + require.Empty(t, data) +} + +func TestDecodeFullMultipleEntries(t *testing.T) { + buf := &bytes.Buffer{} + enc := NewEncoder(buf) + require.NoError(t, enc.WriteField(HeaderField{Name: "foo", Value: "bar"})) + require.NoError(t, enc.WriteField(HeaderField{Name: "lorem", Value: "ipsum"})) + data, err := NewDecoder(nil).DecodeFull(buf.Bytes()) + require.NoError(t, err) + require.Equal(t, []HeaderField{ + {Name: "foo", Value: "bar"}, + {Name: "lorem", Value: "ipsum"}, + }, data) +} + +func TestDecodeFullIncompleteData(t *testing.T) { + buf := &bytes.Buffer{} + enc := NewEncoder(buf) + require.NoError(t, enc.WriteField(HeaderField{Name: "foo", Value: "bar"})) + _, err := NewDecoder(nil).DecodeFull(buf.Bytes()[:buf.Len()-2]) + require.EqualError(t, err, "decoding error: truncated headers") +} + +func TestDecodeFullRestoresEmitFunc(t *testing.T) { + var emitFuncCalled bool + emitFunc := func(HeaderField) { emitFuncCalled = true } + decoder := NewDecoder(emitFunc) + buf := &bytes.Buffer{} + enc := NewEncoder(buf) + require.NoError(t, enc.WriteField(HeaderField{Name: "foo", Value: "bar"})) + _, err := decoder.DecodeFull(buf.Bytes()) + require.NoError(t, err) + require.False(t, emitFuncCalled) + _, err = decoder.Write(buf.Bytes()) + require.NoError(t, err) + require.True(t, emitFuncCalled) +} diff --git a/encoder_test.go b/encoder_test.go index 0629588..df7c579 100644 --- a/encoder_test.go +++ b/encoder_test.go @@ -3,11 +3,11 @@ package qpack import ( "bytes" "io" + "testing" "golang.org/x/net/http2/hpack" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" + "github.com/stretchr/testify/require" ) // errWriter wraps bytes.Buffer and optionally fails on every write @@ -24,150 +24,160 @@ func (ew *errWriter) Write(b []byte) (int, error) { return ew.Buffer.Write(b) } -var _ = Describe("Encoder", func() { - var ( - encoder *Encoder - output *errWriter - ) - - BeforeEach(func() { - output = &errWriter{} - encoder = NewEncoder(output) - }) - - readPrefix := func(data []byte) (rest []byte, requiredInsertCount uint64, deltaBase uint64) { - var err error - requiredInsertCount, rest, err = readVarInt(8, data) - Expect(err).ToNot(HaveOccurred()) - deltaBase, rest, err = readVarInt(7, rest) - Expect(err).ToNot(HaveOccurred()) - return - } +func readPrefix(t *testing.T, data []byte) (rest []byte, requiredInsertCount uint64, deltaBase uint64) { + var err error + requiredInsertCount, rest, err = readVarInt(8, data) + require.NoError(t, err) + deltaBase, rest, err = readVarInt(7, rest) + require.NoError(t, err) + return +} - checkHeaderField := func(data []byte, hf HeaderField) []byte { - Expect(data[0] & (0x80 ^ 0x40 ^ 0x20)).To(Equal(uint8(0x20))) // 001xxxxx - Expect(data[0] & 0x8).ToNot(BeZero()) // Huffman encoding - nameLen, data, err := readVarInt(3, data) - Expect(err).ToNot(HaveOccurred()) - l := hpack.HuffmanEncodeLength(hf.Name) - Expect(nameLen).To(BeEquivalentTo(l)) - Expect(hpack.HuffmanDecodeToString(data[:l])).To(Equal(hf.Name)) - valueLen, data, err := readVarInt(7, data[l:]) - Expect(err).ToNot(HaveOccurred()) - l = hpack.HuffmanEncodeLength(hf.Value) - Expect(valueLen).To(BeEquivalentTo(l)) - Expect(hpack.HuffmanDecodeToString(data[:l])).To(Equal(hf.Value)) - return data[l:] - } +func checkHeaderField(t *testing.T, data []byte, hf HeaderField) []byte { + require.Equal(t, uint8(0x20), data[0]&(0x80^0x40^0x20)) // 001xxxxx + require.NotZero(t, data[0]&0x8) // Huffman encoding + nameLen, data, err := readVarInt(3, data) + require.NoError(t, err) + l := hpack.HuffmanEncodeLength(hf.Name) + require.Equal(t, uint64(l), nameLen) + decodedName, err := hpack.HuffmanDecodeToString(data[:l]) + require.NoError(t, err) + require.Equal(t, hf.Name, decodedName) + valueLen, data, err := readVarInt(7, data[l:]) + require.NoError(t, err) + l = hpack.HuffmanEncodeLength(hf.Value) + require.Equal(t, uint64(l), valueLen) + decodedValue, err := hpack.HuffmanDecodeToString(data[:l]) + require.NoError(t, err) + require.Equal(t, hf.Value, decodedValue) + return data[l:] +} + +// Reads one indexed field line representation from data and verifies it matches hf. +// Returns the leftover bytes from data. +func checkIndexedHeaderField(t *testing.T, data []byte, hf HeaderField) []byte { + require.Equal(t, uint8(1), data[0]>>7) // 1Txxxxxx + index, data, err := readVarInt(6, data) + require.NoError(t, err) + require.Equal(t, hf, staticTableEntries[index]) + return data +} + +func checkHeaderFieldWithNameRef(t *testing.T, data []byte, hf HeaderField) []byte { + // read name reference + require.Equal(t, uint8(1), data[0]>>6) // 01NTxxxx + index, data, err := readVarInt(4, data) + require.NoError(t, err) + require.Equal(t, hf.Name, staticTableEntries[index].Name) + // read literal value + valueLen, data, err := readVarInt(7, data) + require.NoError(t, err) + l := hpack.HuffmanEncodeLength(hf.Value) + require.Equal(t, uint64(l), valueLen) + decodedValue, err := hpack.HuffmanDecodeToString(data[:l]) + require.NoError(t, err) + require.Equal(t, hf.Value, decodedValue) + return data[l:] +} + +func TestEncoderEncodesSingleField(t *testing.T) { + output := &errWriter{} + encoder := NewEncoder(output) + + hf := HeaderField{Name: "foobar", Value: "lorem ipsum"} + require.NoError(t, encoder.WriteField(hf)) + + data, requiredInsertCount, deltaBase := readPrefix(t, output.Bytes()) + require.Zero(t, requiredInsertCount) + require.Zero(t, deltaBase) + + data = checkHeaderField(t, data, hf) + require.Empty(t, data) +} + +func TestEncoderFailsToEncodeWhenWriterErrs(t *testing.T) { + output := &errWriter{fail: true} + encoder := NewEncoder(output) - // Reads one indexed field line representation from data and verifies it matches hf. - // Returns the leftover bytes from data. - checkIndexedHeaderField := func(data []byte, hf HeaderField) []byte { - Expect(data[0] >> 7).To(Equal(uint8(1))) // 1Txxxxxx - index, data, err := readVarInt(6, data) - Expect(err).ToNot(HaveOccurred()) - Expect(staticTableEntries[index]).To(Equal(hf)) - return data + hf := HeaderField{Name: "foobar", Value: "lorem ipsum"} + err := encoder.WriteField(hf) + require.EqualError(t, err, "io: read/write on closed pipe") +} + +func TestEncoderEncodesMultipleFields(t *testing.T) { + output := &errWriter{} + encoder := NewEncoder(output) + + hf1 := HeaderField{Name: "foobar", Value: "lorem ipsum"} + hf2 := HeaderField{Name: "raboof", Value: "dolor sit amet"} + require.NoError(t, encoder.WriteField(hf1)) + require.NoError(t, encoder.WriteField(hf2)) + + data, requiredInsertCount, deltaBase := readPrefix(t, output.Bytes()) + require.Zero(t, requiredInsertCount) + require.Zero(t, deltaBase) + + data = checkHeaderField(t, data, hf1) + data = checkHeaderField(t, data, hf2) + require.Empty(t, data) +} + +func TestEncoderEncodesAllFieldsOfStaticTable(t *testing.T) { + output := &errWriter{} + encoder := NewEncoder(output) + + for _, hf := range staticTableEntries { + require.NoError(t, encoder.WriteField(hf)) } - checkHeaderFieldWithNameRef := func(data []byte, hf HeaderField) []byte { - // read name reference - Expect(data[0] >> 6).To(Equal(uint8(1))) // 01NTxxxx - index, data, err := readVarInt(4, data) - Expect(err).ToNot(HaveOccurred()) - Expect(staticTableEntries[index].Name).To(Equal(hf.Name)) - // read literal value - valueLen, data, err := readVarInt(7, data) - Expect(err).ToNot(HaveOccurred()) - l := hpack.HuffmanEncodeLength(hf.Value) - Expect(valueLen).To(BeEquivalentTo(l)) - Expect(hpack.HuffmanDecodeToString(data[:l])).To(Equal(hf.Value)) - return data[l:] + data, requiredInsertCount, deltaBase := readPrefix(t, output.Bytes()) + require.Zero(t, requiredInsertCount) + require.Zero(t, deltaBase) + + for _, hf := range staticTableEntries { + data = checkIndexedHeaderField(t, data, hf) } + require.Empty(t, data) +} - It("encodes a single field", func() { - hf := HeaderField{Name: "foobar", Value: "lorem ipsum"} - Expect(encoder.WriteField(hf)).To(Succeed()) - - data, requiredInsertCount, deltaBase := readPrefix(output.Bytes()) - Expect(requiredInsertCount).To(BeZero()) - Expect(deltaBase).To(BeZero()) - - data = checkHeaderField(data, hf) - Expect(data).To(BeEmpty()) - }) - - It("encodes fails to encode when writer errs", func() { - hf := HeaderField{Name: "foobar", Value: "lorem ipsum"} - output.fail = true - Expect(encoder.WriteField(hf)).To(MatchError("io: read/write on closed pipe")) - }) - - It("encodes multiple fields", func() { - hf1 := HeaderField{Name: "foobar", Value: "lorem ipsum"} - hf2 := HeaderField{Name: "raboof", Value: "dolor sit amet"} - Expect(encoder.WriteField(hf1)).To(Succeed()) - Expect(encoder.WriteField(hf2)).To(Succeed()) - - data, requiredInsertCount, deltaBase := readPrefix(output.Bytes()) - Expect(requiredInsertCount).To(BeZero()) - Expect(deltaBase).To(BeZero()) - - data = checkHeaderField(data, hf1) - data = checkHeaderField(data, hf2) - Expect(data).To(BeEmpty()) - }) - - It("encodes all the fields of the static table", func() { - for _, hf := range staticTableEntries { - Expect(encoder.WriteField(hf)).To(Succeed()) - } - - data, requiredInsertCount, deltaBase := readPrefix(output.Bytes()) - Expect(requiredInsertCount).To(BeZero()) - Expect(deltaBase).To(BeZero()) - - for _, hf := range staticTableEntries { - data = checkIndexedHeaderField(data, hf) - } - Expect(data).To(BeEmpty()) - }) - - It("encodes fields with name reference in the static table", func() { - hf1 := HeaderField{Name: ":status", Value: "666"} - hf2 := HeaderField{Name: "server", Value: "lorem ipsum"} - hf3 := HeaderField{Name: ":method", Value: ""} - Expect(encoder.WriteField(hf1)).To(Succeed()) - Expect(encoder.WriteField(hf2)).To(Succeed()) - Expect(encoder.WriteField(hf3)).To(Succeed()) - - data, requiredInsertCount, deltaBase := readPrefix(output.Bytes()) - Expect(requiredInsertCount).To(BeZero()) - Expect(deltaBase).To(BeZero()) - - data = checkHeaderFieldWithNameRef(data, hf1) - data = checkHeaderFieldWithNameRef(data, hf2) - data = checkHeaderFieldWithNameRef(data, hf3) - Expect(data).To(BeEmpty()) - }) - - It("encodes multiple requests", func() { - hf1 := HeaderField{Name: "foobar", Value: "lorem ipsum"} - Expect(encoder.WriteField(hf1)).To(Succeed()) - data, requiredInsertCount, deltaBase := readPrefix(output.Bytes()) - Expect(requiredInsertCount).To(BeZero()) - Expect(deltaBase).To(BeZero()) - data = checkHeaderField(data, hf1) - Expect(data).To(BeEmpty()) - - output.Reset() - Expect(encoder.Close()) - hf2 := HeaderField{Name: "raboof", Value: "dolor sit amet"} - Expect(encoder.WriteField(hf2)).To(Succeed()) - data, requiredInsertCount, deltaBase = readPrefix(output.Bytes()) - Expect(requiredInsertCount).To(BeZero()) - Expect(deltaBase).To(BeZero()) - data = checkHeaderField(data, hf2) - Expect(data).To(BeEmpty()) - }) -}) +func TestEncodeFieldsWithNameReferenceInStaticTable(t *testing.T) { + output := &errWriter{} + encoder := NewEncoder(output) + + hf1 := HeaderField{Name: ":status", Value: "666"} + hf2 := HeaderField{Name: "server", Value: "lorem ipsum"} + hf3 := HeaderField{Name: ":method", Value: ""} + require.NoError(t, encoder.WriteField(hf1)) + require.NoError(t, encoder.WriteField(hf2)) + require.NoError(t, encoder.WriteField(hf3)) + + data, requiredInsertCount, deltaBase := readPrefix(t, output.Bytes()) + require.Zero(t, requiredInsertCount) + require.Zero(t, deltaBase) + + data = checkHeaderFieldWithNameRef(t, data, hf1) + data = checkHeaderFieldWithNameRef(t, data, hf2) + data = checkHeaderFieldWithNameRef(t, data, hf3) + require.Empty(t, data) +} + +func TestEncodeMultipleRequests(t *testing.T) { + output := &errWriter{} + encoder := NewEncoder(output) + + hf1 := HeaderField{Name: "foobar", Value: "lorem ipsum"} + require.NoError(t, encoder.WriteField(hf1)) + data, requiredInsertCount, deltaBase := readPrefix(t, output.Bytes()) + require.Zero(t, requiredInsertCount) + require.Zero(t, deltaBase) + require.Empty(t, checkHeaderField(t, data, hf1)) + + output.Reset() + require.NoError(t, encoder.Close()) + hf2 := HeaderField{Name: "raboof", Value: "dolor sit amet"} + require.NoError(t, encoder.WriteField(hf2)) + data, requiredInsertCount, deltaBase = readPrefix(t, output.Bytes()) + require.Zero(t, requiredInsertCount) + require.Zero(t, deltaBase) + require.Empty(t, checkHeaderField(t, data, hf2)) +} diff --git a/go.mod b/go.mod index 854304d..26ad83c 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,12 @@ module github.com/quic-go/qpack go 1.22 require ( - github.com/onsi/ginkgo/v2 v2.2.0 - github.com/onsi/gomega v1.20.1 - golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 + github.com/stretchr/testify v1.9.0 golang.org/x/net v0.22.0 ) require ( - github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect - github.com/google/go-cmp v0.5.8 // indirect - github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.19.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index eac0be3..08bdd7e 100644 --- a/go.sum +++ b/go.sum @@ -1,44 +1,12 @@ -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= -github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= -github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= -github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= -golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= -golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/header_field_test.go b/header_field_test.go index bc7d134..94141d1 100644 --- a/header_field_test.go +++ b/header_field_test.go @@ -1,16 +1,20 @@ package qpack import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" + "testing" + + "github.com/stretchr/testify/require" ) -var _ = Describe("Header Field", func() { - It("says if it is pseudo", func() { - Expect((HeaderField{Name: ":status"}).IsPseudo()).To(BeTrue()) - Expect((HeaderField{Name: ":authority"}).IsPseudo()).To(BeTrue()) - Expect((HeaderField{Name: ":foobar"}).IsPseudo()).To(BeTrue()) - Expect((HeaderField{Name: "status"}).IsPseudo()).To(BeFalse()) - Expect((HeaderField{Name: "foobar"}).IsPseudo()).To(BeFalse()) +func TestHeaderFieldIsPseudo(t *testing.T) { + t.Run("Pseudo headers", func(t *testing.T) { + require.True(t, (HeaderField{Name: ":status"}).IsPseudo()) + require.True(t, (HeaderField{Name: ":authority"}).IsPseudo()) + require.True(t, (HeaderField{Name: ":foobar"}).IsPseudo()) + }) + + t.Run("Non-pseudo headers", func(t *testing.T) { + require.False(t, (HeaderField{Name: "status"}).IsPseudo()) + require.False(t, (HeaderField{Name: "foobar"}).IsPseudo()) }) -}) +} diff --git a/integrationtests/interop/interop_suite_test.go b/integrationtests/interop/interop_suite_test.go index 252172e..0ffd21d 100644 --- a/integrationtests/interop/interop_suite_test.go +++ b/integrationtests/interop/interop_suite_test.go @@ -2,23 +2,21 @@ package interop import ( "bufio" + "encoding/binary" + "fmt" "io" + "log" "os" + "path" "path/filepath" + "runtime" "strings" "testing" "github.com/quic-go/qpack" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" + "github.com/stretchr/testify/require" ) -func TestInterop(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Interop Suite") -} - type request struct { headers []qpack.HeaderField } @@ -29,10 +27,14 @@ type qif struct { var qifs map[string]qif +func init() { + qifs = make(map[string]qif) + readQIFs() +} + func readQIFs() { qifDir := currentDir() + "/qifs/qifs" - Expect(qifDir).To(BeADirectory()) - filepath.Walk(qifDir, func(path string, info os.FileInfo, err error) error { + if err := filepath.Walk(qifDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } @@ -43,11 +45,16 @@ func readQIFs() { ext := filepath.Ext(filename) name := filename[:len(filename)-len(ext)] file, err := os.Open(path) - Expect(err).ToNot(HaveOccurred()) + if err != nil { + return err + } + defer file.Close() requests := parseRequests(file) qifs[name] = qif{requests: requests} return nil - }) + }); err != nil { + log.Fatal(err) + } } func parseRequests(r io.Reader) []request { @@ -63,22 +70,26 @@ func parseRequests(r io.Reader) []request { return reqs } -// Done means that we reached the end of the file -// The headers returned with this call will be empty and should be ignored. -func parseRequest(lr *bufio.Reader) ([]qpack.HeaderField, bool /* done reading */) { +func parseRequest(lr *bufio.Reader) ([]qpack.HeaderField, bool) { var headers []qpack.HeaderField for { line, isPrefix, err := lr.ReadLine() if err == io.EOF { return headers, true } - Expect(err).ToNot(HaveOccurred()) - Expect(isPrefix).To(BeFalse()) + if err != nil { + return nil, true + } + if isPrefix { + return nil, true + } if len(line) == 0 { break } split := strings.Split(string(line), "\t") - Expect(split).To(Or(HaveLen(1), HaveLen(2))) + if len(split) != 1 && len(split) != 2 { + return nil, true + } name := split[0] var val string if len(split) == 2 { @@ -89,7 +100,89 @@ func parseRequest(lr *bufio.Reader) ([]qpack.HeaderField, bool /* done reading * return headers, false } -var _ = BeforeSuite(func() { - qifs = make(map[string]qif) - readQIFs() -}) +func currentDir() string { + _, filename, _, ok := runtime.Caller(0) + if !ok { + panic("Failed to get current frame") + } + return path.Dir(filename) +} + +func findFiles() []string { + var files []string + encodedDir := currentDir() + "/qifs/encoded/qpack-06/" + filepath.Walk(encodedDir, func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + return nil + } + _, file := filepath.Split(path) + split := strings.Split(file, ".") + tableSize := split[len(split)-3] + if tableSize == "0" { + files = append(files, path) + } + return nil + }) + return files +} + +func parseInput(r io.Reader) (uint64, []byte) { + prefix := make([]byte, 12) + _, err := io.ReadFull(r, prefix) + if err != nil { + return 0, nil + } + streamID := binary.BigEndian.Uint64(prefix[:8]) + length := binary.BigEndian.Uint32(prefix[8:12]) + if length > (1 << 15) { + return 0, nil + } + data := make([]byte, int(length)) + _, err = io.ReadFull(r, data) + if err != nil { + return 0, nil + } + return streamID, data +} + +func TestInteropDecodingEncodedFiles(t *testing.T) { + filenames := findFiles() + for _, path := range filenames { + fpath, filename := filepath.Split(path) + prettyPath := path[len(filepath.Dir(filepath.Dir(filepath.Dir(fpath))))+1:] + + t.Run(fmt.Sprintf("Decoding_%s", prettyPath), func(t *testing.T) { + qif, ok := qifs[strings.Split(filename, ".")[0]] + require.True(t, ok) + + file, err := os.Open(path) + require.NoError(t, err) + defer file.Close() + + var headers []qpack.HeaderField + decoder := qpack.NewDecoder(func(hf qpack.HeaderField) { + headers = append(headers, hf) + }) + + var numRequests, numHeaderFields int + require.NotEmpty(t, qif.requests) + + for _, req := range qif.requests { + _, data := parseInput(file) + require.NotNil(t, data) + + _, err = decoder.Write(data) + require.NoError(t, err) + + require.Equal(t, req.headers, headers) + + numRequests++ + numHeaderFields += len(headers) + headers = nil + decoder.Close() + } + + t.Logf("Decoded %d requests containing %d header fields.", len(qif.requests), numHeaderFields) + }) + } +} diff --git a/integrationtests/interop/interop_test.go b/integrationtests/interop/interop_test.go deleted file mode 100644 index b8e3646..0000000 --- a/integrationtests/interop/interop_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package interop - -import ( - "encoding/binary" - "fmt" - "io" - "os" - "path" - "path/filepath" - "runtime" - "strings" - - "github.com/quic-go/qpack" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func currentDir() string { - _, filename, _, ok := runtime.Caller(0) - if !ok { - panic("Failed to get current frame") - } - return path.Dir(filename) -} - -var _ = Describe("Interop", func() { - // find all encoded files with a dynamic table size of 0 - findFiles := func() []string { - var files []string - encodedDir := currentDir() + "/qifs/encoded/qpack-06/" - filepath.Walk(encodedDir, func(path string, info os.FileInfo, err error) error { - if info.IsDir() { - return nil - } - _, file := filepath.Split(path) - split := strings.Split(file, ".") - tableSize := split[len(split)-3] - if tableSize == "0" { - files = append(files, path) - } - return nil - }) - return files - } - - parseInput := func(r io.Reader) (uint64 /* stream ID */, []byte) { - prefix := make([]byte, 12) - _, err := io.ReadFull(r, prefix) - Expect(err).ToNot(HaveOccurred()) - streamID := binary.BigEndian.Uint64(prefix[:8]) - length := binary.BigEndian.Uint32(prefix[8:12]) - if length > (1 << 15) { // DoS prevention - Fail("input too long") - } - data := make([]byte, int(length)) - _, err = io.ReadFull(r, data) - Expect(err).ToNot(HaveOccurred()) - return streamID, data - } - - filenames := findFiles() - for i := range filenames { - path := filenames[i] - fpath, filename := filepath.Split(path) - prettyPath := path[len(filepath.Dir(filepath.Dir(filepath.Dir(fpath))))+1:] - - It(fmt.Sprintf("using %s", prettyPath), func() { - qif, ok := qifs[strings.Split(filename, ".")[0]] - Expect(ok).To(BeTrue()) - - file, err := os.Open(path) - var headers []qpack.HeaderField - decoder := qpack.NewDecoder(func(hf qpack.HeaderField) { - headers = append(headers, hf) - }) - var numRequests, numHeaderFields int - Expect(qif.requests).ToNot(BeEmpty()) - for _, req := range qif.requests { - Expect(err).ToNot(HaveOccurred()) - _, data := parseInput(file) - _, err = decoder.Write(data) - Expect(err).ToNot(HaveOccurred()) - Expect(headers).To(Equal(req.headers)) - numRequests++ - numHeaderFields += len(headers) - headers = nil - decoder.Close() - } - fmt.Fprintf(GinkgoWriter, "Decoded %d requests containing %d header fields.\n", len(qif.requests), numHeaderFields) - }) - } -}) diff --git a/integrationtests/self/integration_test.go b/integrationtests/self/integration_test.go index 7acf3b5..3515b89 100644 --- a/integrationtests/self/integration_test.go +++ b/integrationtests/self/integration_test.go @@ -2,209 +2,208 @@ package self import ( "bytes" - "fmt" - - "golang.org/x/exp/rand" + "math/rand/v2" + "testing" + _ "unsafe" // for go:linkname "github.com/quic-go/qpack" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" + "github.com/stretchr/testify/require" ) +var staticTable []qpack.HeaderField + +//go:linkname getStaticTable github.com/quic-go/qpack.getStaticTable +func getStaticTable() []qpack.HeaderField + +func init() { + staticTable = getStaticTable() +} + func randomString(l int) string { const charset = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" s := make([]byte, l) for i := range s { - s[i] = charset[rand.Intn(len(charset))] + s[i] = charset[rand.IntN(len(charset))] } return string(s) } -var _ = Describe("Self Tests", func() { - getEncoder := func() (*qpack.Encoder, *bytes.Buffer) { - output := &bytes.Buffer{} - return qpack.NewEncoder(output), output +func getEncoder() (*qpack.Encoder, *bytes.Buffer) { + output := &bytes.Buffer{} + return qpack.NewEncoder(output), output +} + +func TestEncodingAndDecodingSingleHeaderField(t *testing.T) { + hf := qpack.HeaderField{ + Name: randomString(15), + Value: randomString(15), } + encoder, output := getEncoder() + require.NoError(t, encoder.WriteField(hf)) + headerFields, err := qpack.NewDecoder(nil).DecodeFull(output.Bytes()) + require.NoError(t, err) + require.Equal(t, []qpack.HeaderField{hf}, headerFields) +} - It("encodes and decodes a single header field", func() { - hf := qpack.HeaderField{ - Name: randomString(15), - Value: randomString(15), - } - encoder, output := getEncoder() - Expect(encoder.WriteField(hf)).To(Succeed()) - headerFields, err := qpack.NewDecoder(nil).DecodeFull(output.Bytes()) - Expect(err).ToNot(HaveOccurred()) - Expect(headerFields).To(Equal([]qpack.HeaderField{hf})) - }) - - It("encodes and decodes multiple header fields", func() { - hfs := []qpack.HeaderField{ - {Name: "foo", Value: "bar"}, - {Name: "lorem", Value: "ipsum"}, - {Name: randomString(15), Value: randomString(20)}, - } - encoder, output := getEncoder() - for _, hf := range hfs { - Expect(encoder.WriteField(hf)).To(Succeed()) - } - headerFields, err := qpack.NewDecoder(nil).DecodeFull(output.Bytes()) - Expect(err).ToNot(HaveOccurred()) - Expect(headerFields).To(Equal(hfs)) - }) - - It("encodes and decodes multiple requests", func() { - hfs1 := []qpack.HeaderField{{Name: "foo", Value: "bar"}} - hfs2 := []qpack.HeaderField{ - {Name: "lorem", Value: "ipsum"}, - {Name: randomString(15), Value: randomString(20)}, - } - encoder, output := getEncoder() - for _, hf := range hfs1 { - Expect(encoder.WriteField(hf)).To(Succeed()) - } - req1 := append([]byte{}, output.Bytes()...) - output.Reset() - for _, hf := range hfs2 { - Expect(encoder.WriteField(hf)).To(Succeed()) - } - req2 := append([]byte{}, output.Bytes()...) - - var headerFields []qpack.HeaderField - decoder := qpack.NewDecoder(func(hf qpack.HeaderField) { headerFields = append(headerFields, hf) }) - _, err := decoder.Write(req1) - Expect(err).ToNot(HaveOccurred()) - Expect(headerFields).To(Equal(hfs1)) - headerFields = nil - _, err = decoder.Write(req2) - Expect(err).ToNot(HaveOccurred()) - Expect(headerFields).To(Equal(hfs2)) - }) - - // replace one character by a random character at a random position - replaceRandomCharacter := func(s string) string { - pos := rand.Intn(len(s)) - new := s[:pos] - for { - if c := randomString(1); c != string(s[pos]) { - new += c - break - } - } - new += s[pos+1:] - return new +func TestEncodingAndDecodingMultipleHeaderFields(t *testing.T) { + hfs := []qpack.HeaderField{ + {Name: "foo", Value: "bar"}, + {Name: "lorem", Value: "ipsum"}, + {Name: randomString(15), Value: randomString(20)}, + } + encoder, output := getEncoder() + for _, hf := range hfs { + require.NoError(t, encoder.WriteField(hf)) + } + headerFields, err := qpack.NewDecoder(nil).DecodeFull(output.Bytes()) + require.NoError(t, err) + require.Equal(t, hfs, headerFields) +} + +func TestEncodingAndDecodingMultipleRequests(t *testing.T) { + hfs1 := []qpack.HeaderField{{Name: "foo", Value: "bar"}} + hfs2 := []qpack.HeaderField{ + {Name: "lorem", Value: "ipsum"}, + {Name: randomString(15), Value: randomString(20)}, + } + encoder, output := getEncoder() + for _, hf := range hfs1 { + require.NoError(t, encoder.WriteField(hf)) + } + req1 := append([]byte{}, output.Bytes()...) + output.Reset() + for _, hf := range hfs2 { + require.NoError(t, encoder.WriteField(hf)) } + req2 := append([]byte{}, output.Bytes()...) + + var headerFields []qpack.HeaderField + decoder := qpack.NewDecoder(func(hf qpack.HeaderField) { headerFields = append(headerFields, hf) }) + _, err := decoder.Write(req1) + require.NoError(t, err) + require.Equal(t, hfs1, headerFields) + headerFields = nil + _, err = decoder.Write(req2) + require.NoError(t, err) + require.Equal(t, hfs2, headerFields) +} - check := func(encoded []byte, hf qpack.HeaderField) { - headerFields, err := qpack.NewDecoder(nil).DecodeFull(encoded) - ExpectWithOffset(1, err).ToNot(HaveOccurred()) - ExpectWithOffset(1, headerFields).To(HaveLen(1)) - ExpectWithOffset(1, headerFields[0]).To(Equal(hf)) +// replace one character by a random character at a random position +func replaceRandomCharacter(s string) string { + pos := rand.IntN(len(s)) + new := s[:pos] + for { + if c := randomString(1); c != string(s[pos]) { + new += c + break + } } + new += s[pos+1:] + return new +} - // use an entry with a value, for example "set-cookie" - It("uses the static table for field names, for fields without values", func() { - var hf qpack.HeaderField - for { - if entry := staticTable[rand.Intn(len(staticTable))]; len(entry.Value) == 0 { - hf = qpack.HeaderField{Name: entry.Name} - break - } +func check(t *testing.T, encoded []byte, hf qpack.HeaderField) { + t.Helper() + headerFields, err := qpack.NewDecoder(nil).DecodeFull(encoded) + require.NoError(t, err) + require.Len(t, headerFields, 1) + require.Equal(t, hf, headerFields[0]) +} + +func TestUsingStaticTableForFieldNamesWithoutValues(t *testing.T) { + var hf qpack.HeaderField + for { + if entry := staticTable[rand.IntN(len(staticTable))]; len(entry.Value) == 0 { + hf = qpack.HeaderField{Name: entry.Name} + break } - encoder, output := getEncoder() - Expect(encoder.WriteField(hf)).To(Succeed()) - encodedLen := output.Len() - check(output.Bytes(), hf) - encoder, output = getEncoder() - oldName := hf.Name - hf.Name = replaceRandomCharacter(hf.Name) - Expect(encoder.WriteField(hf)).To(Succeed()) - fmt.Fprintf(GinkgoWriter, "Encoding field name:\n\t%s: %d bytes\n\t%s: %d bytes\n", oldName, encodedLen, hf.Name, output.Len()) - Expect(output.Len()).To(BeNumerically(">", encodedLen)) - }) - - // use an entry with a value, for example "set-cookie", - // but now use a custom value - It("uses the static table for field names, for fields without values", func() { - var hf qpack.HeaderField - for { - if entry := staticTable[rand.Intn(len(staticTable))]; len(entry.Value) == 0 { - hf = qpack.HeaderField{ - Name: entry.Name, - Value: randomString(5), - } - break + } + encoder, output := getEncoder() + require.NoError(t, encoder.WriteField(hf)) + encodedLen := output.Len() + check(t, output.Bytes(), hf) + encoder, output = getEncoder() + oldName := hf.Name + hf.Name = replaceRandomCharacter(hf.Name) + require.NoError(t, encoder.WriteField(hf)) + t.Logf("Encoding field name:\n\t%s: %d bytes\n\t%s: %d bytes\n", oldName, encodedLen, hf.Name, output.Len()) + require.Greater(t, output.Len(), encodedLen) +} + +func TestUsingStaticTableForFieldNamesWithCustomValues(t *testing.T) { + var hf qpack.HeaderField + for { + if entry := staticTable[rand.IntN(len(staticTable))]; len(entry.Value) == 0 { + hf = qpack.HeaderField{ + Name: entry.Name, + Value: randomString(5), } + break } - encoder, output := getEncoder() - Expect(encoder.WriteField(hf)).To(Succeed()) - encodedLen := output.Len() - check(output.Bytes(), hf) - encoder, output = getEncoder() - oldName := hf.Name - hf.Name = replaceRandomCharacter(hf.Name) - Expect(encoder.WriteField(hf)).To(Succeed()) - fmt.Fprintf(GinkgoWriter, "Encoding field name:\n\t%s: %d bytes\n\t%s: %d bytes\n", oldName, encodedLen, hf.Name, output.Len()) - Expect(output.Len()).To(BeNumerically(">", encodedLen)) - }) - - // use an entry with a value, for example - // cache-control -> Value: "max-age=0" - // but encode a different value - // cache-control -> xyz - It("uses the static table for field names, for fields with values", func() { - var hf qpack.HeaderField - for { - // Only use values with at least 2 characters. - // This makes sure that Huffman enocding doesn't compress them as much as encoding it using the static table would. - if entry := staticTable[rand.Intn(len(staticTable))]; len(entry.Value) > 1 { - hf = qpack.HeaderField{ - Name: entry.Name, - Value: randomString(20), - } - break + } + encoder, output := getEncoder() + require.NoError(t, encoder.WriteField(hf)) + encodedLen := output.Len() + check(t, output.Bytes(), hf) + encoder, output = getEncoder() + oldName := hf.Name + hf.Name = replaceRandomCharacter(hf.Name) + require.NoError(t, encoder.WriteField(hf)) + t.Logf("Encoding field name:\n\t%s: %d bytes\n\t%s: %d bytes", oldName, encodedLen, hf.Name, output.Len()) + require.Greater(t, output.Len(), encodedLen) +} + +func TestStaticTableForFieldNamesWithValues(t *testing.T) { + var hf qpack.HeaderField + for { + // Only use values with at least 2 characters. + // This makes sure that Huffman encoding doesn't compress them as much as encoding it using the static table would. + if entry := staticTable[rand.IntN(len(staticTable))]; len(entry.Value) > 1 { + hf = qpack.HeaderField{ + Name: entry.Name, + Value: randomString(20), } + break } - encoder, output := getEncoder() - Expect(encoder.WriteField(hf)).To(Succeed()) - encodedLen := output.Len() - check(output.Bytes(), hf) - encoder, output = getEncoder() - oldName := hf.Name - hf.Name = replaceRandomCharacter(hf.Name) - Expect(encoder.WriteField(hf)).To(Succeed()) - fmt.Fprintf(GinkgoWriter, "Encoding field name:\n\t%s: %d bytes\n\t%s: %d bytes\n", oldName, encodedLen, hf.Name, output.Len()) - Expect(output.Len()).To(BeNumerically(">", encodedLen)) - }) - - It("uses the static table for field values", func() { - var hf qpack.HeaderField - for { - // Only use values with at least 2 characters. - // This makes sure that Huffman enocding doesn't compress them as much as encoding it using the static table would. - if entry := staticTable[rand.Intn(len(staticTable))]; len(entry.Value) > 1 { - hf = qpack.HeaderField{ - Name: entry.Name, - Value: entry.Value, - } - break + } + encoder, output := getEncoder() + require.NoError(t, encoder.WriteField(hf)) + encodedLen := output.Len() + check(t, output.Bytes(), hf) + encoder, output = getEncoder() + oldName := hf.Name + hf.Name = replaceRandomCharacter(hf.Name) + require.NoError(t, encoder.WriteField(hf)) + t.Logf("Encoding field name:\n\t%s: %d bytes\n\t%s: %d bytes", oldName, encodedLen, hf.Name, output.Len()) + require.Greater(t, output.Len(), encodedLen) +} + +func TestStaticTableForFieldValues(t *testing.T) { + var hf qpack.HeaderField + for { + // Only use values with at least 2 characters. + // This makes sure that Huffman encoding doesn't compress them as much as encoding it using the static table would. + if entry := staticTable[rand.IntN(len(staticTable))]; len(entry.Value) > 1 { + hf = qpack.HeaderField{ + Name: entry.Name, + Value: entry.Value, } + break } - encoder, output := getEncoder() - Expect(encoder.WriteField(hf)).To(Succeed()) - encodedLen := output.Len() - check(output.Bytes(), hf) - encoder, output = getEncoder() - oldValue := hf.Value - hf.Value = replaceRandomCharacter(hf.Value) - Expect(encoder.WriteField(hf)).To(Succeed()) - fmt.Fprintf(GinkgoWriter, - "Encoding field value:\n\t%s: %s -> %d bytes\n\t%s: %s -> %d bytes\n", - hf.Name, oldValue, encodedLen, - hf.Name, hf.Value, output.Len(), - ) - Expect(output.Len()).To(BeNumerically(">", encodedLen)) - }) -}) + } + encoder, output := getEncoder() + require.NoError(t, encoder.WriteField(hf)) + encodedLen := output.Len() + check(t, output.Bytes(), hf) + encoder, output = getEncoder() + oldValue := hf.Value + hf.Value = replaceRandomCharacter(hf.Value) + require.NoError(t, encoder.WriteField(hf)) + t.Logf( + "Encoding field value:\n\t%s: %s -> %d bytes\n\t%s: %s -> %d bytes", + hf.Name, oldValue, encodedLen, + hf.Name, hf.Value, output.Len(), + ) + require.Greater(t, output.Len(), encodedLen) +} diff --git a/integrationtests/self/self_suite_test.go b/integrationtests/self/self_suite_test.go deleted file mode 100644 index 36474e5..0000000 --- a/integrationtests/self/self_suite_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package self - -import ( - "testing" - _ "unsafe" - - "golang.org/x/exp/rand" - - "github.com/quic-go/qpack" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestSelf(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Self Suite") -} - -var _ = BeforeSuite(func() { - rand.Seed(uint64(GinkgoRandomSeed())) -}) - -var staticTable []qpack.HeaderField - -//go:linkname getStaticTable github.com/quic-go/qpack.getStaticTable -func getStaticTable() []qpack.HeaderField - -func init() { - staticTable = getStaticTable() -} diff --git a/qpack_suite_test.go b/qpack_suite_test.go deleted file mode 100644 index 736c0a3..0000000 --- a/qpack_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package qpack_test - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestQpack(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "QPACK Suite") -} diff --git a/static_table_test.go b/static_table_test.go index d1298e4..8413634 100644 --- a/static_table_test.go +++ b/static_table_test.go @@ -1,33 +1,32 @@ package qpack import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" + "testing" + + "github.com/stretchr/testify/require" ) -var _ = Describe("StaticTable", func() { - It("verifies that encoderMap has a value for every staticTableEntries entry", func() { - for idx, hf := range staticTableEntries { - if len(hf.Value) == 0 { - Expect(encoderMap[hf.Name].idx).To(Equal(uint8(idx))) - } else { - Expect(encoderMap[hf.Name].values[hf.Value]).To(Equal(uint8(idx))) - } +func TestEncoderMapHasValueForEveryStaticTableEntry(t *testing.T) { + for idx, hf := range staticTableEntries { + if len(hf.Value) == 0 { + require.Equal(t, uint8(idx), encoderMap[hf.Name].idx) + } else { + require.Equal(t, uint8(idx), encoderMap[hf.Name].values[hf.Value]) } - }) + } +} - It("verifies that staticTableEntries has a value for every encoderMap entry", func() { - for name, indexAndVal := range encoderMap { - if len(indexAndVal.values) == 0 { - id := indexAndVal.idx - Expect(staticTableEntries[id].Name).To(Equal(name)) - Expect(staticTableEntries[id].Value).To(BeEmpty()) - } else { - for value, id := range indexAndVal.values { - Expect(staticTableEntries[id].Name).To(Equal(name)) - Expect(staticTableEntries[id].Value).To(Equal(value)) - } +func TestStaticTableasValueForEveryEncoderMapEntry(t *testing.T) { + for name, indexAndVal := range encoderMap { + if len(indexAndVal.values) == 0 { + id := indexAndVal.idx + require.Equal(t, name, staticTableEntries[id].Name) + require.Empty(t, staticTableEntries[id].Value) + } else { + for value, id := range indexAndVal.values { + require.Equal(t, name, staticTableEntries[id].Name) + require.Equal(t, value, staticTableEntries[id].Value) } } - }) -}) + } +} diff --git a/tools.go b/tools.go deleted file mode 100644 index 8f71eea..0000000 --- a/tools.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build tools - -package qpack - -import _ "github.com/onsi/ginkgo/v2/ginkgo"