Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-hpack for openSUSE:Factory checked in at 2025-02-05 12:39:54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-hpack (Old) and /work/SRC/openSUSE:Factory/.python-hpack.new.2316 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-hpack" Wed Feb 5 12:39:54 2025 rev:9 rq:1243134 version:4.1.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-hpack/python-hpack.changes 2023-04-22 22:01:31.677599388 +0200 +++ /work/SRC/openSUSE:Factory/.python-hpack.new.2316/python-hpack.changes 2025-02-05 12:40:05.759733354 +0100 @@ -1,0 +2,27 @@ +Tue Feb 4 11:51:51 UTC 2025 - John Paul Adrian Glaubitz <adrian.glaub...@suse.com> + +- Update to 4.1.0 + * API Changes (Backward Incompatible)** + - Support for Python 3.6 has been removed. + - Support for Python 3.7 has been removed. + - Support for Python 3.8 has been removed. + - Renamed `InvalidTableIndex` exception to `InvalidTableIndexError`. + * API Changes (Backward Compatible)** + - Support for Python 3.9 has been added. + - Support for Python 3.10 has been added. + - Support for Python 3.11 has been added. + - Support for Python 3.12 has been added. + - Support for Python 3.13 has been added. + - Optimized bytes encoding of headers. + - Updated packaging and testing infrastructure. + - Code cleanup and linting. + - Added type hints. +- Refresh healthcheck.patch +- Refresh test_fixtures.tar.xz +- Switch build system from setuptools to pyproject.toml + * Add python-pip and python-wheel to BuildRequires + * Replace %python_build with %pyproject_wheel + * Replace %python_install with %pyproject_install + * Update name for dist directory in %files section + +------------------------------------------------------------------- Old: ---- hpack-4.0.0.tar.gz New: ---- hpack-4.1.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-hpack.spec ++++++ --- /var/tmp/diff_new_pack.1FJrYO/_old 2025-02-05 12:40:07.323797903 +0100 +++ /var/tmp/diff_new_pack.1FJrYO/_new 2025-02-05 12:40:07.327798068 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-hpack # -# Copyright (c) 2023 SUSE LLC +# Copyright (c) 2025 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %{?sle15_python_module_pythons} Name: python-hpack -Version: 4.0.0 +Version: 4.1.0 Release: 0 Summary: Pure-Python HPACK header compression License: MIT @@ -29,8 +29,10 @@ Source1: test_fixtures.tar.xz Patch0: healthcheck.patch BuildRequires: %{python_module hypothesis} +BuildRequires: %{python_module pip} BuildRequires: %{python_module pytest} BuildRequires: %{python_module setuptools} +BuildRequires: %{python_module wheel} BuildRequires: fdupes BuildRequires: python-rpm-macros BuildArch: noarch @@ -46,11 +48,11 @@ %build export LC_ALL="en_US.UTF-8" -%python_build +%pyproject_wheel %install export LC_ALL="en_US.UTF-8" -%python_install +%pyproject_install %python_expand %fdupes %{buildroot}%{$python_sitelib} %check @@ -60,5 +62,5 @@ %license LICENSE %doc CHANGELOG.rst CONTRIBUTORS.rst README.rst %{python_sitelib}/hpack -%{python_sitelib}/hpack-%{version}-py%{python_version}.egg-info +%{python_sitelib}/hpack-%{version}.dist-info ++++++ healthcheck.patch ++++++ --- /var/tmp/diff_new_pack.1FJrYO/_old 2025-02-05 12:40:07.347798893 +0100 +++ /var/tmp/diff_new_pack.1FJrYO/_new 2025-02-05 12:40:07.351799059 +0100 @@ -1,6 +1,7 @@ ---- a/test/test_hpack.py -+++ b/test/test_hpack.py -@@ -2,7 +2,7 @@ +diff -Nru hpack-4.1.0.orig/tests/test_hpack.py hpack-4.1.0/tests/test_hpack.py +--- hpack-4.1.0.orig/tests/test_hpack.py 2024-11-23 09:35:36.000000000 +0100 ++++ hpack-4.1.0/tests/test_hpack.py 2025-02-04 12:22:54.679696648 +0100 +@@ -1,7 +1,7 @@ import itertools import pytest @@ -9,7 +10,7 @@ from hypothesis.strategies import text, binary, sets, one_of from hpack import ( -@@ -760,6 +760,7 @@ class TestDictToIterable: +@@ -767,6 +767,7 @@ binary().filter(lambda k: k and not k.startswith(b':')) ) @@ -17,7 +18,7 @@ @given( special_keys=sets(keys), boring_keys=sets(keys), -@@ -797,6 +798,7 @@ class TestDictToIterable: +@@ -804,6 +805,7 @@ assert special_keys == received_special assert boring_keys == received_boring ++++++ hpack-4.0.0.tar.gz -> hpack-4.1.0.tar.gz ++++++ ++++ 4662 lines of diff (skipped) ++++++ test_fixtures.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/test/conftest.py new/tests/conftest.py --- old/test/conftest.py 1970-01-01 01:00:00.000000000 +0100 +++ new/tests/conftest.py 2025-02-04 12:38:50.885994211 +0100 @@ -0,0 +1,43 @@ +import pytest +import os +import json + +from hypothesis.strategies import text + +# We need to grab one text example from hypothesis to prime its cache. +text().example() + +# This pair of generator expressions are pretty lame, but building lists is a +# bad idea as I plan to have a substantial number of tests here. +story_directories = ( + os.path.join('tests/test_fixtures', d) + for d in os.listdir('tests/test_fixtures') +) +story_files = ( + os.path.join(storydir, name) + for storydir in story_directories + for name in os.listdir(storydir) + if 'raw-data' not in storydir +) +raw_story_files = ( + os.path.join('tests/test_fixtures/raw-data', name) + for name in os.listdir('tests/test_fixtures/raw-data') +) + + +@pytest.fixture(scope='class', params=story_files) +def story(request): + """ + Provides a detailed HPACK story to test with. + """ + with open(request.param, 'r', encoding='utf-8') as f: + return json.load(f) + + +@pytest.fixture(scope='class', params=raw_story_files) +def raw_story(request): + """ + Provides a detailed HPACK story to test the encoder with. + """ + with open(request.param, 'r', encoding='utf-8') as f: + return json.load(f) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/test/test_encode_decode.py new/tests/test_encode_decode.py --- old/test/test_encode_decode.py 1970-01-01 01:00:00.000000000 +0100 +++ new/tests/test_encode_decode.py 2025-02-04 12:38:50.886931637 +0100 @@ -0,0 +1,140 @@ +""" +Test for the integer encoding/decoding functionality in the HPACK library. +""" +import pytest + +from hypothesis import given +from hypothesis.strategies import integers, binary, one_of + +from hpack import HPACKDecodingError +from hpack.hpack import encode_integer, decode_integer + + +class TestIntegerEncoding: + # These tests are stolen from the HPACK spec. + def test_encoding_10_with_5_bit_prefix(self): + val = encode_integer(10, 5) + assert len(val) == 1 + assert val == bytearray(b'\x0a') + + def test_encoding_1337_with_5_bit_prefix(self): + val = encode_integer(1337, 5) + assert len(val) == 3 + assert val == bytearray(b'\x1f\x9a\x0a') + + def test_encoding_42_with_8_bit_prefix(self): + val = encode_integer(42, 8) + assert len(val) == 1 + assert val == bytearray(b'\x2a') + + +class TestIntegerDecoding: + # These tests are stolen from the HPACK spec. + def test_decoding_10_with_5_bit_prefix(self): + val = decode_integer(b'\x0a', 5) + assert val == (10, 1) + + def test_encoding_1337_with_5_bit_prefix(self): + val = decode_integer(b'\x1f\x9a\x0a', 5) + assert val == (1337, 3) + + def test_encoding_42_with_8_bit_prefix(self): + val = decode_integer(b'\x2a', 8) + assert val == (42, 1) + + def test_decode_empty_string_fails(self): + with pytest.raises(HPACKDecodingError): + decode_integer(b'', 8) + + def test_decode_insufficient_data_fails(self): + with pytest.raises(HPACKDecodingError): + decode_integer(b'\x1f', 5) + + +class TestEncodingProperties: + """ + Property-based tests for our integer encoder and decoder. + """ + @given( + integer=integers(min_value=0), + prefix_bits=integers(min_value=1, max_value=8) + ) + def test_encode_positive_integer_always_valid(self, integer, prefix_bits): + """ + So long as the prefix bits are between 1 and 8, any positive integer + can be represented. + """ + result = encode_integer(integer, prefix_bits) + assert isinstance(result, bytearray) + assert len(result) > 0 + + @given( + integer=integers(max_value=-1), + prefix_bits=integers(min_value=1, max_value=8) + ) + def test_encode_fails_for_negative_integers(self, integer, prefix_bits): + """ + If the integer to encode is negative, the encoder fails. + """ + with pytest.raises(ValueError): + encode_integer(integer, prefix_bits) + + @given( + integer=integers(min_value=0), + prefix_bits=one_of( + integers(max_value=0), + integers(min_value=9) + ) + ) + def test_encode_fails_for_invalid_prefixes(self, integer, prefix_bits): + """ + If the prefix is out of the range [1,8], the encoder fails. + """ + with pytest.raises(ValueError): + encode_integer(integer, prefix_bits) + + @given( + prefix_bits=one_of( + integers(max_value=0), + integers(min_value=9) + ) + ) + def test_decode_fails_for_invalid_prefixes(self, prefix_bits): + """ + If the prefix is out of the range [1,8], the encoder fails. + """ + with pytest.raises(ValueError): + decode_integer(b'\x00', prefix_bits) + + @given( + data=binary(), + prefix_bits=integers(min_value=1, max_value=8) + ) + def test_decode_either_succeeds_or_raises_error(self, data, prefix_bits): + """ + Attempting to decode data either returns a positive integer or throws a + HPACKDecodingError. + """ + try: + result, consumed = decode_integer(data, prefix_bits) + except HPACKDecodingError: + pass + else: + assert isinstance(result, int) + assert result >= 0 + assert consumed > 0 + + @given( + integer=integers(min_value=0), + prefix_bits=integers(min_value=1, max_value=8) + ) + def test_encode_decode_round_trips(self, integer, prefix_bits): + """ + Given valid data, the encoder and decoder can round trip. + """ + encoded_result = encode_integer(integer, prefix_bits) + decoded_integer, consumed = decode_integer( + bytes(encoded_result), prefix_bits + ) + assert integer == decoded_integer + assert consumed > 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/test/test_hpack.py new/tests/test_hpack.py --- old/test/test_hpack.py 1970-01-01 01:00:00.000000000 +0100 +++ new/tests/test_hpack.py 2025-02-04 12:38:51.013997144 +0100 @@ -0,0 +1,846 @@ +import itertools +import pytest + +from hypothesis import given +from hypothesis.strategies import text, binary, sets, one_of + +from hpack import ( + Encoder, + Decoder, + HeaderTuple, + NeverIndexedHeaderTuple, + HPACKDecodingError, + InvalidTableIndex, + OversizedHeaderListError, + InvalidTableSizeError, +) +from hpack.hpack import _dict_to_iterable, _to_bytes + + +def test_to_bytes(): + assert _to_bytes(b"foobar") == b"foobar" + assert _to_bytes("foobar") == b"foobar" + assert _to_bytes(0xABADBABE) == b'2880289470' + assert _to_bytes(True) == b'True' + assert _to_bytes(Encoder()).startswith(b"<hpack.hpack.Encoder") + + +class TestHPACKEncoder: + # These tests are stolen entirely from the IETF specification examples. + def test_literal_header_field_with_indexing(self): + """ + The header field representation uses a literal name and a literal + value. + """ + e = Encoder() + header_set = {'custom-key': 'custom-header'} + result = b'\x40\x0acustom-key\x0dcustom-header' + + assert e.encode(header_set, huffman=False) == result + assert list(e.header_table.dynamic_entries) == [ + (n.encode('utf-8'), v.encode('utf-8')) + for n, v in header_set.items() + ] + + def test_sensitive_headers(self): + """ + Test encoding header values + """ + e = Encoder() + result = (b'\x82\x14\x88\x63\xa1\xa9' + + b'\x32\x08\x73\xd0\xc7\x10' + + b'\x87\x25\xa8\x49\xe9\xea' + + b'\x5f\x5f\x89\x41\x6a\x41' + + b'\x92\x6e\xe5\x35\x52\x9f') + header_set = [ + (':method', 'GET', True), + (':path', '/jimiscool/', True), + ('customkey', 'sensitiveinfo', True), + ] + assert e.encode(header_set, huffman=True) == result + + def test_non_sensitive_headers_with_header_tuples(self): + """ + A header field stored in a HeaderTuple emits a representation that + allows indexing. + """ + e = Encoder() + result = (b'\x82\x44\x88\x63\xa1\xa9' + + b'\x32\x08\x73\xd0\xc7\x40' + + b'\x87\x25\xa8\x49\xe9\xea' + + b'\x5f\x5f\x89\x41\x6a\x41' + + b'\x92\x6e\xe5\x35\x52\x9f') + header_set = [ + HeaderTuple(':method', 'GET'), + HeaderTuple(':path', '/jimiscool/'), + HeaderTuple('customkey', 'sensitiveinfo'), + ] + assert e.encode(header_set, huffman=True) == result + + def test_sensitive_headers_with_header_tuples(self): + """ + A header field stored in a NeverIndexedHeaderTuple emits a + representation that forbids indexing. + """ + e = Encoder() + result = (b'\x82\x14\x88\x63\xa1\xa9' + + b'\x32\x08\x73\xd0\xc7\x10' + + b'\x87\x25\xa8\x49\xe9\xea' + + b'\x5f\x5f\x89\x41\x6a\x41' + + b'\x92\x6e\xe5\x35\x52\x9f') + header_set = [ + NeverIndexedHeaderTuple(':method', 'GET'), + NeverIndexedHeaderTuple(':path', '/jimiscool/'), + NeverIndexedHeaderTuple('customkey', 'sensitiveinfo'), + ] + assert e.encode(header_set, huffman=True) == result + + def test_header_table_size_getter(self): + e = Encoder() + assert e.header_table_size == 4096 + + def test_indexed_literal_header_field_with_indexing(self): + """ + The header field representation uses an indexed name and a literal + value and performs incremental indexing. + """ + e = Encoder() + header_set = {':path': '/sample/path'} + result = b'\x44\x0c/sample/path' + + assert e.encode(header_set, huffman=False) == result + assert list(e.header_table.dynamic_entries) == [ + (n.encode('utf-8'), v.encode('utf-8')) + for n, v in header_set.items() + ] + + def test_indexed_header_field(self): + """ + The header field representation uses an indexed header field, from + the static table. + """ + e = Encoder() + header_set = {':method': 'GET'} + result = b'\x82' + + assert e.encode(header_set, huffman=False) == result + assert list(e.header_table.dynamic_entries) == [] + + def test_indexed_header_field_from_static_table(self): + e = Encoder() + e.header_table_size = 0 + header_set = {':method': 'GET'} + result = b'\x82' + + # Make sure we don't emit an encoding context update. + e.header_table.resized = False + + assert e.encode(header_set, huffman=False) == result + assert list(e.header_table.dynamic_entries) == [] + + def test_request_examples_without_huffman(self): + """ + This section shows several consecutive header sets, corresponding to + HTTP requests, on the same connection. + """ + e = Encoder() + first_header_set = [ + (':method', 'GET',), + (':scheme', 'http',), + (':path', '/',), + (':authority', 'www.example.com'), + ] + # We should have :authority in first_header_table since we index it + first_header_table = [(':authority', 'www.example.com')] + first_result = b'\x82\x86\x84\x41\x0fwww.example.com' + + assert e.encode(first_header_set, huffman=False) == first_result + assert list(e.header_table.dynamic_entries) == [ + (n.encode('utf-8'), v.encode('utf-8')) + for n, v in first_header_table + ] + + second_header_set = [ + (':method', 'GET',), + (':scheme', 'http',), + (':path', '/',), + (':authority', 'www.example.com',), + ('cache-control', 'no-cache'), + ] + second_header_table = [ + ('cache-control', 'no-cache'), + (':authority', 'www.example.com') + ] + second_result = b'\x82\x86\x84\xbeX\x08no-cache' + + assert e.encode(second_header_set, huffman=False) == second_result + assert list(e.header_table.dynamic_entries) == [ + (n.encode('utf-8'), v.encode('utf-8')) + for n, v in second_header_table + ] + + third_header_set = [ + (':method', 'GET',), + (':scheme', 'https',), + (':path', '/index.html',), + (':authority', 'www.example.com',), + ('custom-key', 'custom-value'), + ] + third_result = ( + b'\x82\x87\x85\xbf@\ncustom-key\x0ccustom-value' + ) + + assert e.encode(third_header_set, huffman=False) == third_result + # Don't check the header table here, it's just too complex to be + # reliable. Check its length though. + assert len(e.header_table.dynamic_entries) == 3 + + def test_request_examples_with_huffman(self): + """ + This section shows the same examples as the previous section, but + using Huffman encoding for the literal values. + """ + e = Encoder() + first_header_set = [ + (':method', 'GET',), + (':scheme', 'http',), + (':path', '/',), + (':authority', 'www.example.com'), + ] + first_header_table = [(':authority', 'www.example.com')] + first_result = ( + b'\x82\x86\x84\x41\x8c\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff' + ) + + assert e.encode(first_header_set, huffman=True) == first_result + assert list(e.header_table.dynamic_entries) == [ + (n.encode('utf-8'), v.encode('utf-8')) + for n, v in first_header_table + ] + + second_header_table = [ + ('cache-control', 'no-cache'), + (':authority', 'www.example.com') + ] + second_header_set = [ + (':method', 'GET',), + (':scheme', 'http',), + (':path', '/',), + (':authority', 'www.example.com',), + ('cache-control', 'no-cache'), + ] + second_result = b'\x82\x86\x84\xbeX\x86\xa8\xeb\x10d\x9c\xbf' + + assert e.encode(second_header_set, huffman=True) == second_result + assert list(e.header_table.dynamic_entries) == [ + (n.encode('utf-8'), v.encode('utf-8')) + for n, v in second_header_table + ] + + third_header_set = [ + (':method', 'GET',), + (':scheme', 'https',), + (':path', '/index.html',), + (':authority', 'www.example.com',), + ('custom-key', 'custom-value'), + ] + third_result = ( + b'\x82\x87\x85\xbf' + b'@\x88%\xa8I\xe9[\xa9}\x7f\x89%\xa8I\xe9[\xb8\xe8\xb4\xbf' + ) + + assert e.encode(third_header_set, huffman=True) == third_result + assert len(e.header_table.dynamic_entries) == 3 + + # These tests are custom, for hyper. + def test_resizing_header_table(self): + # We need to encode a substantial number of headers, to populate the + # header table. + e = Encoder() + header_set = [ + (':method', 'GET'), + (':scheme', 'https'), + (':path', '/some/path'), + (':authority', 'www.example.com'), + ('custom-key', 'custom-value'), + ( + "user-agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) " + "Gecko/20100101 Firefox/16.0", + ), + ( + "accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;" + "q=0.8", + ), + ('X-Lukasa-Test', '88989'), + ] + e.encode(header_set, huffman=True) + + # Resize the header table to a size so small that nothing can be in it. + e.header_table_size = 40 + assert len(e.header_table.dynamic_entries) == 0 + + def test_resizing_header_table_sends_multiple_updates(self): + e = Encoder() + + e.header_table_size = 40 + e.header_table_size = 100 + e.header_table_size = 40 + + header_set = [(':method', 'GET')] + out = e.encode(header_set, huffman=True) + assert out == b'\x3F\x09\x3F\x45\x3F\x09\x82' + + def test_resizing_header_table_to_same_size_ignored(self): + e = Encoder() + + # These size changes should be ignored + e.header_table_size = 4096 + e.header_table_size = 4096 + e.header_table_size = 4096 + + # These size changes should be encoded + e.header_table_size = 40 + e.header_table_size = 100 + e.header_table_size = 40 + + header_set = [(':method', 'GET')] + out = e.encode(header_set, huffman=True) + assert out == b'\x3F\x09\x3F\x45\x3F\x09\x82' + + def test_resizing_header_table_sends_context_update(self): + e = Encoder() + + # Resize the header table to a size so small that nothing can be in it. + e.header_table_size = 40 + + # Now, encode a header set. Just a small one, with a well-defined + # output. + header_set = [(':method', 'GET')] + out = e.encode(header_set, huffman=True) + + assert out == b'?\t\x82' + + def test_setting_table_size_to_the_same_does_nothing(self): + e = Encoder() + + # Set the header table size to the default. + e.header_table_size = 4096 + + # Now encode a header set. Just a small one, with a well-defined + # output. + header_set = [(':method', 'GET')] + out = e.encode(header_set, huffman=True) + + assert out == b'\x82' + + def test_evicting_header_table_objects(self): + e = Encoder() + + # Set the header table size large enough to include one header. + e.header_table_size = 66 + header_set = [('a', 'b'), ('long-custom-header', 'longish value')] + e.encode(header_set) + + assert len(e.header_table.dynamic_entries) == 1 + + def test_headers_generator(self): + e = Encoder() + + def headers_generator(): + return (("k" + str(i), "v" + str(i)) for i in range(3)) + + header_set = headers_generator() + out = e.encode(header_set) + assert Decoder().decode(out) == list(headers_generator()) + + +class TestHPACKDecoder: + # These tests are stolen entirely from the IETF specification examples. + def test_literal_header_field_with_indexing(self): + """ + The header field representation uses a literal name and a literal + value. + """ + d = Decoder() + header_set = [('custom-key', 'custom-header')] + data = b'\x40\x0acustom-key\x0dcustom-header' + + assert d.decode(data) == header_set + assert list(d.header_table.dynamic_entries) == [ + (n.encode('utf-8'), v.encode('utf-8')) for n, v in header_set + ] + + def test_raw_decoding(self): + """ + The header field representation is decoded as a raw byte string instead + of UTF-8 + """ + d = Decoder() + header_set = [ + (b'\x00\x01\x99\x30\x11\x22\x55\x21\x89\x14', b'custom-header') + ] + data = ( + b'\x40\x0a\x00\x01\x99\x30\x11\x22\x55\x21\x89\x14\x0d' + b'custom-header' + ) + + assert d.decode(data, raw=True) == header_set + + def test_literal_header_field_without_indexing(self): + """ + The header field representation uses an indexed name and a literal + value. + """ + d = Decoder() + header_set = [(':path', '/sample/path')] + data = b'\x04\x0c/sample/path' + + assert d.decode(data) == header_set + assert list(d.header_table.dynamic_entries) == [] + + def test_header_table_size_getter(self): + d = Decoder() + assert d.header_table_size + + def test_indexed_header_field(self): + """ + The header field representation uses an indexed header field, from + the static table. + """ + d = Decoder() + header_set = [(':method', 'GET')] + data = b'\x82' + + assert d.decode(data) == header_set + assert list(d.header_table.dynamic_entries) == [] + + def test_request_examples_without_huffman(self): + """ + This section shows several consecutive header sets, corresponding to + HTTP requests, on the same connection. + """ + d = Decoder() + first_header_set = [ + (':method', 'GET',), + (':scheme', 'http',), + (':path', '/',), + (':authority', 'www.example.com'), + ] + # The first_header_table doesn't contain 'authority' + first_data = b'\x82\x86\x84\x01\x0fwww.example.com' + + assert d.decode(first_data) == first_header_set + assert list(d.header_table.dynamic_entries) == [] + + # This request takes advantage of the differential encoding of header + # sets. + second_header_set = [ + (':method', 'GET',), + (':scheme', 'http',), + (':path', '/',), + (':authority', 'www.example.com',), + ('cache-control', 'no-cache'), + ] + second_data = ( + b'\x82\x86\x84\x01\x0fwww.example.com\x0f\t\x08no-cache' + ) + + assert d.decode(second_data) == second_header_set + assert list(d.header_table.dynamic_entries) == [] + + third_header_set = [ + (':method', 'GET',), + (':scheme', 'https',), + (':path', '/index.html',), + (':authority', 'www.example.com',), + ('custom-key', 'custom-value'), + ] + third_data = ( + b'\x82\x87\x85\x01\x0fwww.example.com@\ncustom-key\x0ccustom-value' + ) + + assert d.decode(third_data) == third_header_set + # Don't check the header table here, it's just too complex to be + # reliable. Check its length though. + assert len(d.header_table.dynamic_entries) == 1 + + def test_request_examples_with_huffman(self): + """ + This section shows the same examples as the previous section, but + using Huffman encoding for the literal values. + """ + d = Decoder() + + first_header_set = [ + (':method', 'GET',), + (':scheme', 'http',), + (':path', '/',), + (':authority', 'www.example.com'), + ] + first_data = ( + b'\x82\x86\x84\x01\x8c\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff' + ) + + assert d.decode(first_data) == first_header_set + assert list(d.header_table.dynamic_entries) == [] + + second_header_set = [ + (':method', 'GET',), + (':scheme', 'http',), + (':path', '/',), + (':authority', 'www.example.com',), + ('cache-control', 'no-cache'), + ] + second_data = ( + b'\x82\x86\x84\x01\x8c\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff' + b'\x0f\t\x86\xa8\xeb\x10d\x9c\xbf' + ) + + assert d.decode(second_data) == second_header_set + assert list(d.header_table.dynamic_entries) == [] + + third_header_set = [ + (':method', 'GET',), + (':scheme', 'https',), + (':path', '/index.html',), + (':authority', 'www.example.com',), + ('custom-key', 'custom-value'), + ] + third_data = ( + b'\x82\x87\x85\x01\x8c\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff@' + b'\x88%\xa8I\xe9[\xa9}\x7f\x89%\xa8I\xe9[\xb8\xe8\xb4\xbf' + ) + + assert d.decode(third_data) == third_header_set + assert len(d.header_table.dynamic_entries) == 1 + + # These tests are custom, for hyper. + def test_resizing_header_table(self): + # We need to decode a substantial number of headers, to populate the + # header table. This string isn't magic: it's the output from the + # equivalent test for the Encoder. + d = Decoder() + data = ( + b'\x82\x87D\x87a\x07\xa4\xacV4\xcfA\x8c\xf1\xe3\xc2\xe5\xf2:k\xa0' + b'\xab\x90\xf4\xff@\x88%\xa8I\xe9[\xa9}\x7f\x89%\xa8I\xe9[\xb8\xe8' + b'\xb4\xbfz\xbc\xd0\x7ff\xa2\x81\xb0\xda\xe0S\xfa\xd02\x1a\xa4\x9d' + b'\x13\xfd\xa9\x92\xa4\x96\x854\x0c\x8aj\xdc\xa7\xe2\x81\x02\xef}' + b'\xa9g{\x81qp\x7fjb):\x9d\x81\x00 \x00@\x150\x9a\xc2\xca\x7f,\x05' + b'\xc5\xc1S\xb0I|\xa5\x89\xd3M\x1fC\xae\xba\x0cA\xa4\xc7\xa9\x8f3' + b'\xa6\x9a?\xdf\x9ah\xfa\x1du\xd0b\r&=Ly\xa6\x8f\xbe\xd0\x01w\xfe' + b'\xbeX\xf9\xfb\xed\x00\x17{@\x8a\xfc[=\xbdF\x81\xad\xbc\xa8O\x84y' + b'\xe7\xde\x7f' + ) + d.decode(data) + + # Resize the header table to a size so small that nothing can be in it. + d.header_table_size = 40 + assert len(d.header_table.dynamic_entries) == 0 + + def test_apache_trafficserver(self): + # This test reproduces the bug in #110, using exactly the same header + # data. + d = Decoder() + data = ( + b'\x10\x07:status\x03200@\x06server\tATS/6.0.0' + b'@\x04date\x1dTue, 31 Mar 2015 08:09:51 GMT' + b'@\x0ccontent-type\ttext/html@\x0econtent-length\x0542468' + b'@\rlast-modified\x1dTue, 31 Mar 2015 01:55:51 GMT' + b'@\x04vary\x0fAccept-Encoding@\x04etag\x0f"5519fea7-a5e4"' + b'@\x08x-served\x05Nginx@\x14x-subdomain-tryfiles\x04True' + b'@\x07x-deity\thydra-lts@\raccept-ranges\x05bytes@\x03age\x010' + b'@\x19strict-transport-security\rmax-age=86400' + b'@\x03via2https/1.1 ATS (ApacheTrafficServer/6.0.0 [cSsNfU])' + ) + expect = [ + (':status', '200'), + ('server', 'ATS/6.0.0'), + ('date', 'Tue, 31 Mar 2015 08:09:51 GMT'), + ('content-type', 'text/html'), + ('content-length', '42468'), + ('last-modified', 'Tue, 31 Mar 2015 01:55:51 GMT'), + ('vary', 'Accept-Encoding'), + ('etag', '"5519fea7-a5e4"'), + ('x-served', 'Nginx'), + ('x-subdomain-tryfiles', 'True'), + ('x-deity', 'hydra-lts'), + ('accept-ranges', 'bytes'), + ('age', '0'), + ('strict-transport-security', 'max-age=86400'), + ('via', 'https/1.1 ATS (ApacheTrafficServer/6.0.0 [cSsNfU])'), + ] + + result = d.decode(data) + + assert result == expect + # The status header shouldn't be indexed. + assert len(d.header_table.dynamic_entries) == len(expect) - 1 + + def test_utf8_errors_raise_hpack_decoding_error(self): + d = Decoder() + + # Invalid UTF-8 data. + data = b'\x82\x86\x84\x01\x10www.\x07\xaa\xd7\x95\xd7\xa8\xd7\x94.com' + + with pytest.raises(HPACKDecodingError): + d.decode(data) + + def test_invalid_indexed_literal(self): + d = Decoder() + + # Refer to an index that is too large. + data = b'\x82\x86\x84\x7f\x0a\x0fwww.example.com' + with pytest.raises(InvalidTableIndex): + d.decode(data) + + def test_invalid_indexed_header(self): + d = Decoder() + + # Refer to an indexed header that is too large. + data = b'\xBE\x86\x84\x01\x0fwww.example.com' + with pytest.raises(InvalidTableIndex): + d.decode(data) + + def test_literal_header_field_with_indexing_emits_headertuple(self): + """ + A header field with indexing emits a HeaderTuple. + """ + d = Decoder() + data = b'\x00\x0acustom-key\x0dcustom-header' + + headers = d.decode(data) + assert len(headers) == 1 + + header = headers[0] + assert isinstance(header, HeaderTuple) + assert not isinstance(header, NeverIndexedHeaderTuple) + + def test_literal_never_indexed_emits_neverindexedheadertuple(self): + """ + A literal header field that must never be indexed emits a + NeverIndexedHeaderTuple. + """ + d = Decoder() + data = b'\x10\x0acustom-key\x0dcustom-header' + + headers = d.decode(data) + assert len(headers) == 1 + + header = headers[0] + assert isinstance(header, NeverIndexedHeaderTuple) + + def test_indexed_never_indexed_emits_neverindexedheadertuple(self): + """ + A header field with an indexed name that must never be indexed emits a + NeverIndexedHeaderTuple. + """ + d = Decoder() + data = b'\x14\x0c/sample/path' + + headers = d.decode(data) + assert len(headers) == 1 + + header = headers[0] + assert isinstance(header, NeverIndexedHeaderTuple) + + def test_max_header_list_size(self): + """ + If the header block is larger than the max_header_list_size, the HPACK + decoder throws an OversizedHeaderListError. + """ + d = Decoder(max_header_list_size=44) + data = b'\x14\x0c/sample/path' + + with pytest.raises(OversizedHeaderListError): + d.decode(data) + + def test_can_decode_multiple_header_table_size_changes(self): + """ + If multiple header table size changes are sent in at once, they are + successfully decoded. + """ + d = Decoder() + data = b'?a?\xe1\x1f\x82\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99' + expect = [ + (':method', 'GET'), + (':scheme', 'https'), + (':path', '/'), + (':authority', '127.0.0.1:8443') + ] + + assert d.decode(data) == expect + + def test_header_table_size_change_above_maximum(self): + """ + If a header table size change is received that exceeds the maximum + allowed table size, it is rejected. + """ + d = Decoder() + d.max_allowed_table_size = 127 + data = b'?a\x82\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99' + + with pytest.raises(InvalidTableSizeError): + d.decode(data) + + def test_table_size_not_adjusting(self): + """ + If the header table size is shrunk, and then the remote peer doesn't + join in the shrinking, then an error is raised. + """ + d = Decoder() + d.max_allowed_table_size = 128 + data = b'\x82\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99' + + with pytest.raises(InvalidTableSizeError): + d.decode(data) + + def test_table_size_last_rejected(self): + """ + If a header table size change comes last in the header block, it is + forbidden. + """ + d = Decoder() + data = b'\x82\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99?a' + + with pytest.raises(HPACKDecodingError): + d.decode(data) + + def test_table_size_middle_rejected(self): + """ + If a header table size change comes anywhere but first in the header + block, it is forbidden. + """ + d = Decoder() + data = b'\x82?a\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99' + + with pytest.raises(HPACKDecodingError): + d.decode(data) + + def test_truncated_header_name(self): + """ + If a header name is truncated an error is raised. + """ + d = Decoder() + # This is a simple header block that has a bad ending. The interesting + # part begins on the second line. This indicates a string that has + # literal name and value. The name is a 5 character huffman-encoded + # string that is only three bytes long. + data = ( + b'\x82\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99' + b'\x00\x85\xf2\xb2J' + ) + + with pytest.raises(HPACKDecodingError): + d.decode(data) + + def test_truncated_header_value(self): + """ + If a header value is truncated an error is raised. + """ + d = Decoder() + # This is a simple header block that has a bad ending. The interesting + # part begins on the second line. This indicates a string that has + # literal name and value. The name is a 5 character huffman-encoded + # string, but the entire EOS character has been written over the end. + # This causes hpack to see the header value as being supposed to be + # 622462 bytes long, which it clearly is not, and so this must fail. + data = ( + b'\x82\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99' + b'\x00\x85\xf2\xb2J\x87\xff\xff\xff\xfd%B\x7f' + ) + + with pytest.raises(HPACKDecodingError): + d.decode(data) + + +class TestDictToIterable: + """ + The dict_to_iterable function has some subtle requirements: validates that + everything behaves as expected. + + As much as possible this tries to be exhaustive. + """ + keys = one_of( + text().filter(lambda k: k and not k.startswith(':')), + binary().filter(lambda k: k and not k.startswith(b':')) + ) + + @given( + special_keys=sets(keys), + boring_keys=sets(keys), + ) + def test_ordering(self, special_keys, boring_keys): + """ + _dict_to_iterable produces an iterable where all the keys beginning + with a colon are emitted first. + """ + def _prepend_colon(k): + if isinstance(k, str): + return ':' + k + else: + return b':' + k + + special_keys = set(map(_prepend_colon, special_keys)) + input_dict = { + k: b'testval' for k in itertools.chain( + special_keys, + boring_keys + ) + } + filtered = _dict_to_iterable(input_dict) + + received_special = set() + received_boring = set() + + for _ in special_keys: + k, _ = next(filtered) + received_special.add(k) + for _ in boring_keys: + k, _ = next(filtered) + received_boring.add(k) + + assert special_keys == received_special + assert boring_keys == received_boring + + @given( + special_keys=sets(keys), + boring_keys=sets(keys), + ) + def test_ordering_applies_to_encoding(self, special_keys, boring_keys): + """ + When encoding a dictionary the special keys all appear first. + """ + def _prepend_colon(k): + if isinstance(k, str): + return ':' + k + else: + return b':' + k + + special_keys = set(map(_prepend_colon, special_keys)) + input_dict = { + k: b'testval' for k in itertools.chain( + special_keys, + boring_keys + ) + } + e = Encoder() + d = Decoder() + encoded = e.encode(input_dict) + decoded = iter(d.decode(encoded, raw=True)) + + received_special = set() + received_boring = set() + expected_special = set(map(_to_bytes, special_keys)) + expected_boring = set(map(_to_bytes, boring_keys)) + + for _ in special_keys: + k, _ = next(decoded) + received_special.add(k) + for _ in boring_keys: + k, _ = next(decoded) + received_boring.add(k) + + assert expected_special == received_special + assert expected_boring == received_boring diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/test/test_hpack_integration.py new/tests/test_hpack_integration.py --- old/test/test_hpack_integration.py 1970-01-01 01:00:00.000000000 +0100 +++ new/tests/test_hpack_integration.py 2025-02-04 12:38:51.013997144 +0100 @@ -0,0 +1,74 @@ +""" +This module defines substantial HPACK integration tests. These can take a very +long time to run, so they're outside the main test suite, but they need to be +run before every change to HPACK. +""" +from binascii import unhexlify +from pytest import skip + +from hpack import Decoder, Encoder, HeaderTuple + + +class TestHPACKDecoderIntegration: + def test_can_decode_a_story(self, story): + d = Decoder() + + # We test against draft 9 of the HPACK spec. + if story['draft'] != 9: + skip("We test against draft 9, not draft %d" % story['draft']) + + for case in story['cases']: + try: + d.header_table_size = case['header_table_size'] + except KeyError: + pass + decoded_headers = d.decode(unhexlify(case['wire'])) + + # The correct headers are a list of dicts, which is annoying. + correct_headers = [ + (item[0], item[1]) + for header in case['headers'] + for item in header.items() + ] + correct_headers = correct_headers + assert correct_headers == decoded_headers + assert all( + isinstance(header, HeaderTuple) for header in decoded_headers + ) + + def test_can_encode_a_story_no_huffman(self, raw_story): + d = Decoder() + e = Encoder() + + for case in raw_story['cases']: + # The input headers are a list of dicts, which is annoying. + input_headers = [ + (item[0], item[1]) + for header in case['headers'] + for item in header.items() + ] + + encoded = e.encode(input_headers, huffman=False) + decoded_headers = d.decode(encoded) + + assert input_headers == decoded_headers + assert all( + isinstance(header, HeaderTuple) for header in decoded_headers + ) + + def test_can_encode_a_story_with_huffman(self, raw_story): + d = Decoder() + e = Encoder() + + for case in raw_story['cases']: + # The input headers are a list of dicts, which is annoying. + input_headers = [ + (item[0], item[1]) + for header in case['headers'] + for item in header.items() + ] + + encoded = e.encode(input_headers, huffman=True) + decoded_headers = d.decode(encoded) + + assert input_headers == decoded_headers diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/test/test_huffman.py new/tests/test_huffman.py --- old/test/test_huffman.py 1970-01-01 01:00:00.000000000 +0100 +++ new/tests/test_huffman.py 2025-02-04 12:38:51.013997144 +0100 @@ -0,0 +1,54 @@ +from hpack import HPACKDecodingError +from hpack.huffman import HuffmanEncoder +from hpack.huffman_constants import REQUEST_CODES, REQUEST_CODES_LENGTH +from hpack.huffman_table import decode_huffman + +from hypothesis import given, example +from hypothesis.strategies import binary + + +class TestHuffman: + + def test_request_huffman_decoder(self): + assert ( + decode_huffman(b'\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff') == + b"www.example.com" + ) + assert decode_huffman(b'\xa8\xeb\x10d\x9c\xbf') == b"no-cache" + assert decode_huffman(b'%\xa8I\xe9[\xa9}\x7f') == b"custom-key" + assert ( + decode_huffman(b'%\xa8I\xe9[\xb8\xe8\xb4\xbf') == b"custom-value" + ) + + def test_request_huffman_encode(self): + encoder = HuffmanEncoder(REQUEST_CODES, REQUEST_CODES_LENGTH) + assert ( + encoder.encode(b"www.example.com") == + b'\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff' + ) + assert encoder.encode(b"no-cache") == b'\xa8\xeb\x10d\x9c\xbf' + assert encoder.encode(b"custom-key") == b'%\xa8I\xe9[\xa9}\x7f' + assert ( + encoder.encode(b"custom-value") == b'%\xa8I\xe9[\xb8\xe8\xb4\xbf' + ) + + +class TestHuffmanDecoder: + @given(data=binary()) + @example(b'\xff') + @example(b'\x5f\xff\xff\xff\xff') + @example(b'\x00\x3f\xff\xff\xff') + def test_huffman_decoder_properly_handles_all_bytestrings(self, data): + """ + When given random bytestrings, either we get HPACKDecodingError or we + get a bytestring back. + """ + # The examples aren't special, they're just known to hit specific error + # paths through the state machine. Basically, they are strings that are + # definitely invalid. + try: + result = decode_huffman(data) + except HPACKDecodingError: + result = b'' + + assert isinstance(result, bytes) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/test/test_struct.py new/tests/test_struct.py --- old/test/test_struct.py 1970-01-01 01:00:00.000000000 +0100 +++ new/tests/test_struct.py 2025-02-04 12:38:51.013997144 +0100 @@ -0,0 +1,70 @@ +import pytest + +from hpack import HeaderTuple, NeverIndexedHeaderTuple + + +class TestHeaderTuple: + def test_is_tuple(self): + """ + HeaderTuple objects are tuples. + """ + h = HeaderTuple('name', 'value') + assert isinstance(h, tuple) + + def test_unpacks_properly(self): + """ + HeaderTuple objects unpack like tuples. + """ + h = HeaderTuple('name', 'value') + k, v = h + + assert k == 'name' + assert v == 'value' + + def test_header_tuples_are_indexable(self): + """ + HeaderTuple objects can be indexed. + """ + h = HeaderTuple('name', 'value') + assert h.indexable + + def test_never_indexed_tuples_are_not_indexable(self): + """ + NeverIndexedHeaderTuple objects cannot be indexed. + """ + h = NeverIndexedHeaderTuple('name', 'value') + assert not h.indexable + + @pytest.mark.parametrize('cls', (HeaderTuple, NeverIndexedHeaderTuple)) + def test_equal_to_tuples(self, cls): + """ + HeaderTuples and NeverIndexedHeaderTuples are equal to equivalent + tuples. + """ + t1 = ('name', 'value') + t2 = cls('name', 'value') + + assert t1 == t2 + assert t1 is not t2 + + @pytest.mark.parametrize('cls', (HeaderTuple, NeverIndexedHeaderTuple)) + def test_equal_to_self(self, cls): + """ + HeaderTuples and NeverIndexedHeaderTuples are always equal when + compared to the same class. + """ + t1 = cls('name', 'value') + t2 = cls('name', 'value') + + assert t1 == t2 + assert t1 is not t2 + + def test_equal_for_different_indexes(self): + """ + HeaderTuples compare equal to equivalent NeverIndexedHeaderTuples. + """ + t1 = HeaderTuple('name', 'value') + t2 = NeverIndexedHeaderTuple('name', 'value') + + assert t1 == t2 + assert t1 is not t2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/test/test_table.py new/tests/test_table.py --- old/test/test_table.py 1970-01-01 01:00:00.000000000 +0100 +++ new/tests/test_table.py 2025-02-04 12:38:51.013997144 +0100 @@ -0,0 +1,143 @@ +import pytest + +from hpack import InvalidTableIndex +from hpack.table import HeaderTable, table_entry_size + + +class TestPackageFunctions: + def test_table_entry_size(self): + res = table_entry_size(b'TestName', b'TestValue') + assert res == 49 + + +class TestHeaderTable: + def test_get_by_index_dynamic_table(self): + tbl = HeaderTable() + off = len(HeaderTable.STATIC_TABLE) + val = (b'TestName', b'TestValue') + tbl.add(*val) + res = tbl.get_by_index(off + 1) + assert res == val + + def test_get_by_index_static_table(self): + tbl = HeaderTable() + exp = (b':authority', b'') + res = tbl.get_by_index(1) + assert res == exp + idx = len(HeaderTable.STATIC_TABLE) + exp = (b'www-authenticate', b'') + res = tbl.get_by_index(idx) + assert res == exp + + def test_get_by_index_zero_index(self): + tbl = HeaderTable() + with pytest.raises(InvalidTableIndex): + tbl.get_by_index(0) + + def test_get_by_index_out_of_range(self): + tbl = HeaderTable() + off = len(HeaderTable.STATIC_TABLE) + tbl.add(b'TestName', b'TestValue') + with pytest.raises(InvalidTableIndex) as e: + tbl.get_by_index(off + 2) + + assert ( + "Invalid table index %d" % (off + 2) in str(e.value) + ) + + def test_repr(self): + tbl = HeaderTable() + tbl.add(b'TestName1', b'TestValue1') + tbl.add(b'TestName2', b'TestValue2') + tbl.add(b'TestName2', b'TestValue2') + exp = ( + "HeaderTable(4096, False, deque([" + "(b'TestName2', b'TestValue2'), " + "(b'TestName2', b'TestValue2'), " + "(b'TestName1', b'TestValue1')" + "]))" + ) + res = repr(tbl) + assert res == exp + + def test_add_to_large(self): + tbl = HeaderTable() + # Max size to small to hold the value we specify + tbl.maxsize = 1 + tbl.add(b'TestName', b'TestValue') + # Table length should be 0 + assert len(tbl.dynamic_entries) == 0 + + def test_search_in_static_full(self): + tbl = HeaderTable() + itm = (b':authority', b'') + exp = (1, itm[0], itm[1]) + res = tbl.search(itm[0], itm[1]) + assert res == exp + + def test_search_in_static_partial(self): + tbl = HeaderTable() + exp = (1, b':authority', None) + res = tbl.search(b':authority', b'NotInTable') + assert res == exp + + def test_search_in_dynamic_full(self): + tbl = HeaderTable() + idx = len(HeaderTable.STATIC_TABLE) + 1 + tbl.add(b'TestName', b'TestValue') + exp = (idx, b'TestName', b'TestValue') + res = tbl.search(b'TestName', b'TestValue') + assert res == exp + + def test_search_in_dynamic_partial(self): + tbl = HeaderTable() + idx = len(HeaderTable.STATIC_TABLE) + 1 + tbl.add(b'TestName', b'TestValue') + exp = (idx, b'TestName', None) + res = tbl.search(b'TestName', b'NotInTable') + assert res == exp + + def test_search_no_match(self): + tbl = HeaderTable() + tbl.add(b'TestName', b'TestValue') + res = tbl.search(b'NotInTable', b'NotInTable') + assert res is None + + def test_maxsize_prop_getter(self): + tbl = HeaderTable() + assert tbl.maxsize == HeaderTable.DEFAULT_SIZE + + def test_maxsize_prop_setter(self): + tbl = HeaderTable() + exp = int(HeaderTable.DEFAULT_SIZE / 2) + tbl.maxsize = exp + assert tbl.resized is True + assert tbl.maxsize == exp + tbl.resized = False + tbl.maxsize = exp + assert tbl.resized is False + assert tbl.maxsize == exp + + def test_size(self): + tbl = HeaderTable() + for i in range(3): + tbl.add(b'TestName', b'TestValue') + res = tbl._current_size + assert res == 147 + + def test_shrink_maxsize_is_zero(self): + tbl = HeaderTable() + tbl.add(b'TestName', b'TestValue') + assert len(tbl.dynamic_entries) == 1 + tbl.maxsize = 0 + assert len(tbl.dynamic_entries) == 0 + + def test_shrink_maxsize(self): + tbl = HeaderTable() + for i in range(3): + tbl.add(b'TestName', b'TestValue') + + assert tbl._current_size == 147 + tbl.maxsize = 146 + assert len(tbl.dynamic_entries) == 2 + assert tbl._current_size == 98