|
| 1 | +from pathlib import Path |
| 2 | +from unittest.mock import sentinel |
| 3 | + |
| 4 | +import pytest |
| 5 | + |
| 6 | +from pytest_mpl.kernels import (DEFAULT_HAMMING_TOLERANCE, DEFAULT_HASH_SIZE, |
| 7 | + DEFAULT_HIGH_FREQUENCY_FACTOR, KERNEL_PHASH, KERNEL_SHA256, Kernel, |
| 8 | + KernelPHash, KernelSHA256, kernel_factory) |
| 9 | + |
| 10 | +HASH_SIZE = sentinel.hash_size |
| 11 | +HAMMING_TOLERANCE = sentinel.hamming_tolerance |
| 12 | +HIGH_FREQUENCY_FACTOR = sentinel.high_freq_factor |
| 13 | + |
| 14 | +#: baseline hash (64-bit) |
| 15 | +HASH_BASE = "0123456789abcdef" |
| 16 | + |
| 17 | +#: 2-bit baseline delta (64-bit) |
| 18 | +# ---X------------ |
| 19 | +HASH_2BIT = "0120456789abcdef" |
| 20 | + |
| 21 | +#: 4-bit baseline delta (64-bit) |
| 22 | +# --XX-----------X |
| 23 | +HASH_4BIT = "0100456789abcdee" |
| 24 | + |
| 25 | + |
| 26 | +#: Absolute path to test baseline image |
| 27 | +baseline_image = Path(__file__).parent / "baseline" / "2.0.x" / "test_base_style.png" |
| 28 | + |
| 29 | +#: Verify availabilty of test baseline image |
| 30 | +baseline_unavailable = not baseline_image.is_file() |
| 31 | + |
| 32 | +#: Convenience skipif reason |
| 33 | +baseline_missing = f"missing baseline image {str(baseline_image)!r}" |
| 34 | + |
| 35 | + |
| 36 | +class DummyMarker: |
| 37 | + def __init__(self, hamming_tolerance=None): |
| 38 | + self.kwargs = dict(hamming_tolerance=hamming_tolerance) |
| 39 | + |
| 40 | + |
| 41 | +class DummyPlugin: |
| 42 | + def __init__(self, hash_size=None, hamming_tolerance=None, high_freq_factor=None): |
| 43 | + self.hash_size = hash_size |
| 44 | + self.hamming_tolerance = hamming_tolerance |
| 45 | + self.high_freq_factor = high_freq_factor |
| 46 | + |
| 47 | + |
| 48 | +def test_kernel_abc(): |
| 49 | + emsg = "Can't instantiate abstract class Kernel" |
| 50 | + with pytest.raises(TypeError, match=emsg): |
| 51 | + Kernel(None) |
| 52 | + |
| 53 | + |
| 54 | +def test_phash_name(): |
| 55 | + assert KernelPHash.name == KERNEL_PHASH |
| 56 | + |
| 57 | + |
| 58 | +def test_phash_init__set(): |
| 59 | + plugin = DummyPlugin( |
| 60 | + hash_size=HASH_SIZE, |
| 61 | + hamming_tolerance=HAMMING_TOLERANCE, |
| 62 | + high_freq_factor=HIGH_FREQUENCY_FACTOR, |
| 63 | + ) |
| 64 | + kernel = KernelPHash(plugin) |
| 65 | + assert kernel.hash_size == HASH_SIZE |
| 66 | + assert kernel.hamming_tolerance == HAMMING_TOLERANCE |
| 67 | + assert kernel.high_freq_factor == HIGH_FREQUENCY_FACTOR |
| 68 | + assert kernel.equivalent is None |
| 69 | + assert kernel.hamming_distance is None |
| 70 | + |
| 71 | + |
| 72 | +def test_phash_init__default(): |
| 73 | + plugin = DummyPlugin() |
| 74 | + kernel = KernelPHash(plugin) |
| 75 | + assert kernel.hash_size == DEFAULT_HASH_SIZE |
| 76 | + assert kernel.hamming_tolerance == DEFAULT_HAMMING_TOLERANCE |
| 77 | + assert kernel.high_freq_factor == DEFAULT_HIGH_FREQUENCY_FACTOR |
| 78 | + assert kernel.equivalent is None |
| 79 | + assert kernel.hamming_distance is None |
| 80 | + |
| 81 | + |
| 82 | +def test_phash_option(): |
| 83 | + assert KernelPHash(DummyPlugin()).option == "hamming_tolerance" |
| 84 | + |
| 85 | + |
| 86 | +@pytest.mark.parametrize( |
| 87 | + "baseline,equivalent,distance", |
| 88 | + [(HASH_BASE, True, 0), (HASH_2BIT, True, 2), (HASH_4BIT, False, 4)], |
| 89 | +) |
| 90 | +def test_phash_equivalent(baseline, equivalent, distance): |
| 91 | + kernel = KernelPHash(DummyPlugin()) |
| 92 | + assert kernel.equivalent_hash(HASH_BASE, baseline) is equivalent |
| 93 | + assert kernel.equivalent is equivalent |
| 94 | + assert kernel.hamming_distance == distance |
| 95 | + |
| 96 | + |
| 97 | +def test_phash_equivalent__tolerance(): |
| 98 | + hamming_tolerance = 10 |
| 99 | + plugin = DummyPlugin(hamming_tolerance=hamming_tolerance) |
| 100 | + kernel = KernelPHash(plugin) |
| 101 | + assert kernel.equivalent_hash(HASH_BASE, HASH_4BIT) |
| 102 | + assert kernel.equivalent is True |
| 103 | + assert kernel.hamming_tolerance == hamming_tolerance |
| 104 | + assert kernel.hamming_distance == 4 |
| 105 | + |
| 106 | + |
| 107 | +@pytest.mark.parametrize( |
| 108 | + "tolerance,equivalent", |
| 109 | + [(10, True), (3, False)], |
| 110 | +) |
| 111 | +def test_phash_equivalent__marker(tolerance, equivalent): |
| 112 | + marker = DummyMarker(hamming_tolerance=tolerance) |
| 113 | + kernel = KernelPHash(DummyPlugin()) |
| 114 | + assert kernel.hamming_tolerance == DEFAULT_HAMMING_TOLERANCE |
| 115 | + assert kernel.equivalent_hash(HASH_BASE, HASH_4BIT, marker=marker) is equivalent |
| 116 | + assert kernel.equivalent is equivalent |
| 117 | + assert kernel.hamming_tolerance == tolerance |
| 118 | + assert kernel.hamming_distance == 4 |
| 119 | + |
| 120 | + |
| 121 | +@pytest.mark.skipif(baseline_unavailable, reason=baseline_missing) |
| 122 | +@pytest.mark.parametrize( |
| 123 | + "hash_size,hff,expected", |
| 124 | + [ |
| 125 | + ( |
| 126 | + DEFAULT_HASH_SIZE, |
| 127 | + DEFAULT_HIGH_FREQUENCY_FACTOR, |
| 128 | + "800bc0555feab05f67ea8d1779fa83537e7ec0d17f9f003517ef200985532856", |
| 129 | + ), |
| 130 | + ( |
| 131 | + DEFAULT_HASH_SIZE, |
| 132 | + 8, |
| 133 | + "800fc0155fe8b05f67ea8d1779fa83537e7ec0d57f9f003517ef200985532856", |
| 134 | + ), |
| 135 | + (8, DEFAULT_HIGH_FREQUENCY_FACTOR, "80c05fb1778d79c3"), |
| 136 | + ( |
| 137 | + DEFAULT_HASH_SIZE, |
| 138 | + 16, |
| 139 | + "800bc0155feab05f67ea8d1779fa83537e7ec0d57f9f003517ef200985532856", |
| 140 | + ), |
| 141 | + ], |
| 142 | +) |
| 143 | +def test_phash_generate_hash(hash_size, hff, expected): |
| 144 | + plugin = DummyPlugin(hash_size=hash_size, high_freq_factor=hff) |
| 145 | + kernel = KernelPHash(plugin) |
| 146 | + fh = open(baseline_image, "rb") |
| 147 | + actual = kernel.generate_hash(fh) |
| 148 | + assert actual == expected |
| 149 | + |
| 150 | + |
| 151 | +@pytest.mark.parametrize("message", (None, "", "one")) |
| 152 | +@pytest.mark.parametrize("equivalent", (None, True)) |
| 153 | +def test_phash_update_status__none(message, equivalent): |
| 154 | + kernel = KernelPHash(DummyPlugin()) |
| 155 | + kernel.equivalent = equivalent |
| 156 | + result = kernel.update_status(message) |
| 157 | + assert isinstance(result, str) |
| 158 | + expected = 0 if message is None else len(message) |
| 159 | + assert len(result) == expected |
| 160 | + |
| 161 | + |
| 162 | +@pytest.mark.parametrize("message", ("", "one")) |
| 163 | +@pytest.mark.parametrize("distance", (10, 20)) |
| 164 | +@pytest.mark.parametrize("tolerance", (1, 2)) |
| 165 | +def test_phash_update_status__equivalent(message, distance, tolerance): |
| 166 | + plugin = DummyPlugin(hamming_tolerance=tolerance) |
| 167 | + kernel = KernelPHash(plugin) |
| 168 | + kernel.equivalent = False |
| 169 | + kernel.hamming_distance = distance |
| 170 | + result = kernel.update_status(message) |
| 171 | + assert isinstance(result, str) |
| 172 | + template = "Hash hamming distance of {} bits > hamming tolerance of {} bits." |
| 173 | + status = template.format(distance, tolerance) |
| 174 | + expected = f"{message} {status}" if message else status |
| 175 | + assert result == expected |
| 176 | + |
| 177 | + |
| 178 | +@pytest.mark.parametrize( |
| 179 | + "summary,distance,tolerance,count", |
| 180 | + [({}, None, DEFAULT_HAMMING_TOLERANCE, 3), (dict(one=1), 2, 3, 4)], |
| 181 | +) |
| 182 | +def test_phash_update_summary(summary, distance, tolerance, count): |
| 183 | + plugin = DummyPlugin(hamming_tolerance=tolerance) |
| 184 | + kernel = KernelPHash(plugin) |
| 185 | + kernel.hamming_distance = distance |
| 186 | + kernel.update_summary(summary) |
| 187 | + assert summary["kernel"] == KernelPHash.name |
| 188 | + assert summary["hamming_distance"] == distance |
| 189 | + assert summary["hamming_tolerance"] == tolerance |
| 190 | + assert len(summary) == count |
0 commit comments