# # BitTorrent bencode coder test suite (Python) # # Copyright (c) 2026 Project Nayuki. (MIT License) # https://www.nayuki.io/page/bittorrent-bencode-format-tools # # Permission is hereby granted, free of charge, to any person obtaining a copy of # this software and associated documentation files (the "Software"), to deal in # the Software without restriction, including without limitation the rights to # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of # the Software, and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # - The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # - The Software is provided "as is", without warranty of any kind, express or # implied, including but not limited to the warranties of merchantability, # fitness for a particular purpose and noninfringement. In no event shall the # authors or copyright holders be liable for any claim, damages or other # liability, whether in an action of contract, tort or otherwise, arising from, # out of or in connection with the Software or the use or other dealings in the # Software. # import collections, io, unittest import bencode class BencodeTest(unittest.TestCase): # ---- Test the serialization ---- def test_serialize_integer(self) -> None: self._check_serialize([ ("i0e", 0), ("i2e", 2), ("i-1e", -1), ("i3141592e", 3141592), ("i-27182818284e", -27182818284), ("i1208925819614629174706176e", 1 << 80), ]) def test_serialize_byte_string(self) -> None: self._check_serialize([ ("0:", b""), ("1:\u0000", b"\x00"), ("2:\u0004\u0001", b"\x04\x01"), ("3:ben", b"ben"), ("10:ABCDE98765", b"ABCDE98765"), ]) def test_serialize_list(self) -> None: self._check_serialize([ ("le", []), ("li4ee", [4]), ("li7e5:Helloe", [7, b"Hello"]), ("li-88ele1:Xe", [-88, [], b"X"]), ]) def test_serialize_dictionary(self) -> None: self._check_serialize([ ("de", {}), ("d0:lee", {b"":[]}), ("d3:AAA6:-141422:ZZi768ee", {b"ZZ":768, b"AAA":b"-14142"}), ("d1:\u0003le1:\u0008dee", {b"\x03":[], b"\x08":{}}), ]) # Asserts that for each given pair, serializing the bencode value equals the byte string. def _check_serialize(self, cases: collections.abc.Sequence[tuple[str,bencode.BencodeValue]]) -> None: for (expected, obj) in cases: with io.BytesIO() as out: bencode.serialize(obj, out) actual: bytes = out.getvalue() self.assertEqual(expected.encode("UTF-8"), actual) # ---- Test the parsing ---- def test_parse_empty(self) -> None: self._parse_expecting_exception([""], EOFError) def test_parse_invalid(self) -> None: self._parse_expecting_exception([ "i0ei1e", "1:a2:bc3:def", "le0:de", ], ValueError) def test_parse_integer(self) -> None: self._check_parse([ (0, "i0e"), (11, "i11e"), (-749, "i-749e"), (9223372036854775807, "i9223372036854775807e"), (-9223372036854775808, "i-9223372036854775808e"), ]) def test_parse_integer_eof(self) -> None: self._parse_expecting_exception([ "i", "i0", "i1248", "i-", ], EOFError) def test_parse_integer_invalid(self) -> None: self._parse_expecting_exception([ "ie", "i00", "i00e", "i019", "i0199e", "i-e", "i-0", "i-0e", "i-026e", "i-B", "iA", "iAe", "i01Ce", "i+5e", "i4.0e", "i9E9e", ], ValueError) def test_parse_byte_string(self) -> None: self._check_parse([ (b"", "0:"), (b"&", "1:&"), (b"abcdefghijklm", "13:abcdefghijklm"), ]) def test_parse_byte_string_eof(self) -> None: self._parse_expecting_exception([ "0", "1", "843", "1:", "2:", "2:q", "d", "d3:$", ], EOFError) def test_parse_byte_string_invalid(self) -> None: self._parse_expecting_exception([ "00", "01", "00:", "01:", "-", "-0", "-1:", ], ValueError) def test_parse_list(self) -> None: self._check_parse([ ([], "le"), ([-6], "li-6ee"), ([b"00", 55], "l2:00i55ee"), ([[], []], "llelee"), ]) def test_parse_list_eof(self) -> None: self._parse_expecting_exception([ "l", "li0e", "llleleel", ], EOFError) def test_parse_dictionary(self) -> None: self._check_parse([ ({}, "de"), ({b"-":404}, "d1:-i404ee"), ({b"010":b"101", b"yU":[]}, "d3:0103:1012:yUlee"), ]) def test_parse_dictionary_eof(self) -> None: self._parse_expecting_exception([ "d", "d1::", "d2: 0:", "d0:d", ], EOFError) def test_parse_dictionary_invalid(self) -> None: self._parse_expecting_exception([ "d:", "d-", "d1:A0:1:A1:.", "d1:B0:1:A1:.", "d1:B0:1:D0:1:C0:", "d1:E0:1:F0:1:E0:", "d2:gg0:1:g0:", ], ValueError) # Asserts that parsing each given test case will raise the given exception. def _parse_expecting_exception(self, testcases: collections.abc.Sequence[str], expect: type) -> None: for cs in testcases: try: BencodeTest._try_parse(cs) except Exception as e: if isinstance(e, expect): continue # Pass self.fail() # Asserts that for each given pair, parsing the byte string equals the bencode value. def _check_parse(self, cases: collections.abc.Sequence[tuple[bencode.BencodeValue,str]]) -> None: for (expect, s) in cases: actual: bencode.BencodeValue = BencodeTest._try_parse(s) self.assertTrue(BencodeTest._deep_equals(expect, actual)) # Parses the given string into a bencode value. @staticmethod def _try_parse(s: str) -> bencode.BencodeValue: with io.BytesIO(s.encode("UTF-8")) as inp: return bencode.parse(inp) # Tests whether the two given bencode values/structures are equal. @staticmethod def _deep_equals(x: bencode.BencodeValue, y: bencode.BencodeValue) -> bool: if bencode.is_int(x) and bencode.is_int(y): return x == y elif bencode.is_bytes(x) and bencode.is_bytes(y): return x == y elif bencode.is_list(x) and bencode.is_list(y): return (len(x) == len(y)) and all( BencodeTest._deep_equals(xe, ye) for (xe, ye) in zip(x, y)) elif bencode.is_dict(x) and bencode.is_dict(y): return (len(x) == len(y)) and all( bencode.is_bytes(xk) and bencode.is_bytes(yk) and (xk == yk) and BencodeTest._deep_equals(xv, yv) for ((xk, xv), (yk, yv)) in zip(sorted(x.items()), sorted(y.items()))) else: raise AssertionError(f"Unreachable types: {type(x)}, {type(y)}") if __name__ == "__main__": unittest.main()