From d850b9e804bc9b4b04a6cdc343811da2fb339bdb Mon Sep 17 00:00:00 2001 From: shloktech Date: Sat, 27 Dec 2025 16:38:05 +0530 Subject: [PATCH 1/6] Increasing unit test coverage --- tests/test_core.py | 144 ++++++++++++++++----------------------------- 1 file changed, 52 insertions(+), 92 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 7c1c2b3..3887439 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,8 +1,5 @@ import sys import os -import struct -from unittest.mock import patch - sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) import pytest @@ -10,7 +7,6 @@ from src.keyedstablehash.canonical import canonicalize_to_bytes from src.keyedstablehash.siphash import siphash24 from src.keyedstablehash.stable import stable_keyed_hash -import src.keyedstablehash.canonical as canonical_module SIPHASH_VECTORS = { 0: "310e0edd47db6f72", @@ -59,49 +55,20 @@ def test_canonicalization_handles_sets_and_lists(): def test_rejects_unsupported_type(): - class ExampleSlots: + class Example: __slots__ = ("value",) def __init__(self, value: int): self.value = value - # Slots classes do not have __dict__ and are not automatically supported - with pytest.raises(TypeError) as excinfo: - stable_keyed_hash(ExampleSlots(1), key=b"\x00" * 16) - assert "Unsupported type" in str(excinfo.value) - - -def test_canonical_custom_object_with_dict(): - """Test that objects with __dict__ are canonicalized by class name and vars.""" - - class ExampleObj: - def __init__(self, value): - self.value = value - self.ignore = None # Just to have multiple fields - - obj = ExampleObj(42) - encoded = canonicalize_to_bytes(obj) - - # Check tag for Object - assert encoded.startswith(b"O") - # Check that class name is encoded - assert b"ExampleObj" in encoded - # Check that the internal value 42 is present (encoded as int) - assert canonicalize_to_bytes(42) in encoded + with pytest.raises(TypeError): + stable_keyed_hash(Example(1), key=b"\x00" * 16) def test_encode_length_and_int(): - # Zero - assert canonicalize_to_bytes(0) == b"I" + struct.pack(" 10 + assert canonicalize_to_bytes(0) == canonicalize_to_bytes(0) def test_feed_canonical_dict_order(): @@ -116,67 +83,12 @@ def test_feed_canonical_list_vs_tuple(): lst = [1, 2, 3] t = (1, 2, 3) assert canonicalize_to_bytes(lst) != canonicalize_to_bytes(t) - assert canonicalize_to_bytes(lst).startswith(b"L") - assert canonicalize_to_bytes(t).startswith(b"T") def test_feed_canonical_set_order(): s1 = {1, 2, 3} s2 = {3, 2, 1} assert canonicalize_to_bytes(s1) == canonicalize_to_bytes(s2) - assert canonicalize_to_bytes(s1).startswith(b"E") - - -def test_canonical_frozenset(): - fs = frozenset([3, 2, 1]) - s = {1, 2, 3} - # Frozenset and Set should produce the same content encoding if logic allows, - # or at least be supported. In canonical.py both use _handle_set. - assert canonicalize_to_bytes(fs) == canonicalize_to_bytes(s) - - -def test_canonical_primitives_full_coverage(): - # None - assert canonicalize_to_bytes(None) == b"N" - - # Bool - assert canonicalize_to_bytes(True) == b"B\x01" - assert canonicalize_to_bytes(False) == b"B\x00" - - # Float - f_val = 1.234 - encoded_f = canonicalize_to_bytes(f_val) - assert encoded_f.startswith(b"F") - assert struct.pack(" Date: Sat, 27 Dec 2025 17:21:22 +0530 Subject: [PATCH 2/6] Updating pytest command --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index aa064ae..5fe7adc 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -52,7 +52,7 @@ jobs: - name: Test with pytest run: | # Generates an XML report required by Codecov - pytest --cov=keyedstablehash --cov-report=xml --cov-report=term + pytest --cov=src/keyedstablehash --cov-report=term --cov-report=xml # 6. Upload Coverage: Send report to Codecov.io - name: Upload coverage to Codecov From c9432917c8387be0873f397ef7a60e9568e92000 Mon Sep 17 00:00:00 2001 From: shloktech Date: Sat, 27 Dec 2025 17:29:37 +0530 Subject: [PATCH 3/6] Test coverage of cannonical to 100% --- cobertura-coverage.xml | 301 +++++++++++++++++++++++++++++++++++++++++ tests/test_core.py | 97 +++++++++++-- 2 files changed, 388 insertions(+), 10 deletions(-) create mode 100644 cobertura-coverage.xml diff --git a/cobertura-coverage.xml b/cobertura-coverage.xml new file mode 100644 index 0000000..5816e4c --- /dev/null +++ b/cobertura-coverage.xml @@ -0,0 +1,301 @@ + + + + + + C:\Programming\Open Source\keyedstablehash\src + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_core.py b/tests/test_core.py index 3887439..ea8477e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,9 +1,13 @@ import sys import os +import struct +from unittest.mock import patch + sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) import pytest +import src.keyedstablehash.canonical as canonical_module from src.keyedstablehash.canonical import canonicalize_to_bytes from src.keyedstablehash.siphash import siphash24 from src.keyedstablehash.stable import stable_keyed_hash @@ -83,6 +87,8 @@ def test_feed_canonical_list_vs_tuple(): lst = [1, 2, 3] t = (1, 2, 3) assert canonicalize_to_bytes(lst) != canonicalize_to_bytes(t) + assert canonicalize_to_bytes(lst).startswith(b"L") + assert canonicalize_to_bytes(t).startswith(b"T") def test_feed_canonical_set_order(): @@ -136,47 +142,118 @@ def test_vectorized_import_errors(): def test_encode_int_edge_cases(): actual_0 = canonicalize_to_bytes(0) - expected_0 = b'I\x01\x00\x00\x00\x00\x00\x00\x00\x00' + expected_0 = b"I\x01\x00\x00\x00\x00\x00\x00\x00\x00" assert actual_0 == expected_0 actual_1 = canonicalize_to_bytes(1) - expected_1 = b'I\x01\x00\x00\x00\x00\x00\x00\x00\x01' + expected_1 = b"I\x01\x00\x00\x00\x00\x00\x00\x00\x01" assert actual_1 == expected_1 actual_minus_1 = canonicalize_to_bytes(-1) - expected_minus_1 = b'I\x01\x00\x00\x00\x00\x00\x00\x00\xff' + expected_minus_1 = b"I\x01\x00\x00\x00\x00\x00\x00\x00\xff" assert actual_minus_1 == expected_minus_1 actual_max_int = canonicalize_to_bytes(2**63 - 1) - expected_max_int = b'I\x08\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xff\xff\xff' + expected_max_int = ( + b"I\x08\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xff\xff\xff" + ) assert actual_max_int == expected_max_int - actual_min_int = canonicalize_to_bytes(-2**63) - expected_min_int = b'I\x08\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00' + actual_min_int = canonicalize_to_bytes(-(2**63)) + expected_min_int = ( + b"I\x08\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00" + ) assert actual_min_int == expected_min_int def test_normalize_scalar_numpy_generic(): try: import numpy as np + val = np.int64(10) assert canonicalize_to_bytes(val) == canonicalize_to_bytes(10) except ImportError: pytest.skip("Numpy not installed") +def test_numpy_mock_normalization(): + """Test numpy normalization even if numpy is not installed on the system.""" + + class FakeGeneric: + def item(self): + return 99 + + # Patch _NUMPY_GENERIC to include our fake class + # If numpy is missing, _NUMPY_GENERIC is (), so we make it (FakeGeneric,) + # If numpy is present, we add FakeGeneric to it. + with patch.object(canonical_module, "_NUMPY_GENERIC", (FakeGeneric,)): + fake_val = FakeGeneric() + # Should normalize to 99 -> int encoding + assert canonicalize_to_bytes(fake_val) == canonicalize_to_bytes(99) + + def test_handle_object_with_dict(): class MyClass: def __init__(self, a, b): self.a = a self.b = b + obj = MyClass(1, "test") # Dynamically get the type name as it would be canonicalized - type_name = ( - f"{obj.__class__.__module__}." - f"{obj.__class__.__qualname__}".encode("utf-8") + type_name = f"{obj.__class__.__module__}." f"{obj.__class__.__qualname__}".encode( + "utf-8" ) canonical_vars = canonicalize_to_bytes({"a": 1, "b": "test"}) - expected_bytes_with_type = b'O' + len(type_name).to_bytes(8, 'little') + type_name + canonical_vars + expected_bytes_with_type = ( + b"O" + len(type_name).to_bytes(8, "little") + type_name + canonical_vars + ) actual_obj_bytes = canonicalize_to_bytes(obj) assert actual_obj_bytes == expected_bytes_with_type + + +def test_canonical_none(): + """Test coverage for _handle_none.""" + assert canonicalize_to_bytes(None) == b"N" + + +def test_canonical_bool(): + """Test coverage for _handle_bool.""" + # True -> b"B\x01" + assert canonicalize_to_bytes(True) == b"B\x01" + # False -> b"B\x00" + assert canonicalize_to_bytes(False) == b"B\x00" + + +def test_canonical_float(): + """Test coverage for _handle_float.""" + val = 123.456 + expected = b"F" + struct.pack(" Date: Sat, 27 Dec 2025 17:33:58 +0530 Subject: [PATCH 4/6] Increasing code coverage of stable and siphash --- tests/test_core.py | 245 +++++++++++++++++++++++++++------------------ 1 file changed, 149 insertions(+), 96 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index ea8477e..ec1e864 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -12,6 +12,10 @@ from src.keyedstablehash.siphash import siphash24 from src.keyedstablehash.stable import stable_keyed_hash +# ------------------------------------------------------------------------- +# Test Vectors & Reference Checks +# ------------------------------------------------------------------------- + SIPHASH_VECTORS = { 0: "310e0edd47db6f72", 1: "fd67dc93c539f874", @@ -38,6 +42,43 @@ def test_siphash_vectors_match_reference(): assert hasher.hexdigest() == expected_hex +# ------------------------------------------------------------------------- +# Stable Hash & KeyedStableHash Class Tests (stable.py Coverage) +# ------------------------------------------------------------------------- + + +def test_keyed_stable_hash_output_formats(): + """ + Test hexdigest() and intdigest() methods of KeyedStableHash. + Ensures 100% coverage of the KeyedStableHash dataclass methods. + """ + key = b"\x00" * 16 + payload = "test_data" + result = stable_keyed_hash(payload, key=key) + + # 1. Test digest() + assert isinstance(result.digest(), bytes) + assert len(result.digest()) == 8 # SipHash-2-4 returns 64 bits (8 bytes) + + # 2. Test hexdigest() + hex_val = result.hexdigest() + assert isinstance(hex_val, str) + assert hex_val == result.digest().hex() + + # 3. Test intdigest() + int_val = result.intdigest() + assert isinstance(int_val, int) + # stable.py uses little endian, unsigned + expected_int = int.from_bytes(result.digest(), byteorder="little", signed=False) + assert int_val == expected_int + + +def test_stable_keyed_hash_algo_error(): + """Test invalid algorithm selection raises ValueError.""" + with pytest.raises(ValueError, match="Unsupported algorithm"): + stable_keyed_hash(123, key=b"0" * 16, algo="unknown_algo") + + def test_stable_hash_dict_is_order_independent(): key = b"\x01" * 16 first = stable_keyed_hash({"b": [2, 3], "a": 1}, key=key) @@ -52,70 +93,58 @@ def test_stable_hash_respects_key(): assert digest_a != digest_b -def test_canonicalization_handles_sets_and_lists(): - left = canonicalize_to_bytes({"items": {1, 2, 3}}) - right = canonicalize_to_bytes({"items": {3, 2, 1}}) - assert left == right +# ------------------------------------------------------------------------- +# SipHash Implementation Tests (siphash.py Coverage) +# ------------------------------------------------------------------------- -def test_rejects_unsupported_type(): - class Example: - __slots__ = ("value",) +def test_siphash_intdigest(): + """Test that siphash24.intdigest returns the correct integer.""" + key = b"\x00" * 16 + hasher = siphash24(key) + hasher.update(b"test") - def __init__(self, value: int): - self.value = value + int_val = hasher.intdigest() + digest_bytes = hasher.digest() - with pytest.raises(TypeError): - stable_keyed_hash(Example(1), key=b"\x00" * 16) + # SipHash returns a 64-bit little-endian integer + expected_int = struct.unpack(" int encoding assert canonicalize_to_bytes(fake_val) == canonicalize_to_bytes(99) @@ -199,42 +243,34 @@ def __init__(self, a, b): self.b = b obj = MyClass(1, "test") - # Dynamically get the type name as it would be canonicalized + type_name = f"{obj.__class__.__module__}." f"{obj.__class__.__qualname__}".encode( "utf-8" ) + # Objects are encoded as 'O' + len(typename) + typename + canonical(vars(obj)) canonical_vars = canonicalize_to_bytes({"a": 1, "b": "test"}) - expected_bytes_with_type = ( - b"O" + len(type_name).to_bytes(8, "little") + type_name + canonical_vars - ) - actual_obj_bytes = canonicalize_to_bytes(obj) - assert actual_obj_bytes == expected_bytes_with_type + expected = b"O" + len(type_name).to_bytes(8, "little") + type_name + canonical_vars + + assert canonicalize_to_bytes(obj) == expected def test_canonical_none(): - """Test coverage for _handle_none.""" assert canonicalize_to_bytes(None) == b"N" def test_canonical_bool(): - """Test coverage for _handle_bool.""" - # True -> b"B\x01" assert canonicalize_to_bytes(True) == b"B\x01" - # False -> b"B\x00" assert canonicalize_to_bytes(False) == b"B\x00" def test_canonical_float(): - """Test coverage for _handle_float.""" val = 123.456 expected = b"F" + struct.pack(" Date: Sat, 27 Dec 2025 17:36:02 +0530 Subject: [PATCH 5/6] Adding unit tests for vectorized --- tests/test_core.py | 202 ++++++++++++++++++++++++++++++++------------- 1 file changed, 144 insertions(+), 58 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index ec1e864..a32ee57 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,7 +1,7 @@ -import sys import os import struct -from unittest.mock import patch +import sys +from unittest.mock import patch, MagicMock sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) @@ -43,22 +43,19 @@ def test_siphash_vectors_match_reference(): # ------------------------------------------------------------------------- -# Stable Hash & KeyedStableHash Class Tests (stable.py Coverage) +# Stable Hash & KeyedStableHash Class Tests # ------------------------------------------------------------------------- def test_keyed_stable_hash_output_formats(): - """ - Test hexdigest() and intdigest() methods of KeyedStableHash. - Ensures 100% coverage of the KeyedStableHash dataclass methods. - """ + """Test hexdigest() and intdigest() methods of KeyedStableHash.""" key = b"\x00" * 16 payload = "test_data" result = stable_keyed_hash(payload, key=key) # 1. Test digest() assert isinstance(result.digest(), bytes) - assert len(result.digest()) == 8 # SipHash-2-4 returns 64 bits (8 bytes) + assert len(result.digest()) == 8 # 2. Test hexdigest() hex_val = result.hexdigest() @@ -68,13 +65,11 @@ def test_keyed_stable_hash_output_formats(): # 3. Test intdigest() int_val = result.intdigest() assert isinstance(int_val, int) - # stable.py uses little endian, unsigned expected_int = int.from_bytes(result.digest(), byteorder="little", signed=False) assert int_val == expected_int def test_stable_keyed_hash_algo_error(): - """Test invalid algorithm selection raises ValueError.""" with pytest.raises(ValueError, match="Unsupported algorithm"): stable_keyed_hash(123, key=b"0" * 16, algo="unknown_algo") @@ -94,57 +89,42 @@ def test_stable_hash_respects_key(): # ------------------------------------------------------------------------- -# SipHash Implementation Tests (siphash.py Coverage) +# SipHash Implementation Tests # ------------------------------------------------------------------------- def test_siphash_intdigest(): - """Test that siphash24.intdigest returns the correct integer.""" key = b"\x00" * 16 hasher = siphash24(key) hasher.update(b"test") - int_val = hasher.intdigest() digest_bytes = hasher.digest() - - # SipHash returns a 64-bit little-endian integer expected_int = struct.unpack(" Date: Sat, 27 Dec 2025 17:38:05 +0530 Subject: [PATCH 6/6] Removing cobertura xml --- cobertura-coverage.xml | 301 ----------------------------------------- 1 file changed, 301 deletions(-) delete mode 100644 cobertura-coverage.xml diff --git a/cobertura-coverage.xml b/cobertura-coverage.xml deleted file mode 100644 index 5816e4c..0000000 --- a/cobertura-coverage.xml +++ /dev/null @@ -1,301 +0,0 @@ - - - - - - C:\Programming\Open Source\keyedstablehash\src - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -