Rearrange hypothesis tests in the `ssh_agent` module
Marco Ricci

Marco Ricci commited on 2025-01-26 16:34:47
Zeige 1 geänderte Dateien mit 127 Einfügungen und 102 Löschungen.


Arrange the hypothesis tests so that they lie next to the respective
non-hypothesis test, if any, instead of bundling them all in the same
testing class.  By doing this, related tests are closer to each other,
if possible.  Use the hypothesis mark instead of the testing class if
filtering is required.

Additionally, since the helper functions `as_ssh_string`,
`canonicalize1` and `canonicalize2` now are visible in and assigned to
a broader context, give them proper docstrings.
... ...
@@ -31,6 +31,51 @@ if TYPE_CHECKING:
31 31
 class TestStaticFunctionality:
32 32
     """Test the static functionality of the `ssh_agent` module."""
33 33
 
34
+    @staticmethod
35
+    def as_ssh_string(bytestring: bytes) -> bytes:
36
+        """Return an encoded SSH string from a bytestring.
37
+
38
+        This is a helper function for hypothesis data generation.
39
+
40
+        """
41
+        return int.to_bytes(len(bytestring), 4, 'big') + bytestring
42
+
43
+    @staticmethod
44
+    def canonicalize1(data: bytes) -> bytes:
45
+        """Return an encoded SSH string from a bytestring.
46
+
47
+        This is a helper function for hypothesis testing.
48
+
49
+        References:
50
+
51
+          * [David R. MacIver: Another invariant to test for
52
+            encoders][DECODE_ENCODE]
53
+
54
+        [DECODE_ENCODE]: https://hypothesis.works/articles/canonical-serialization/
55
+
56
+        """
57
+        return ssh_agent.SSHAgentClient.string(
58
+            ssh_agent.SSHAgentClient.unstring(data)
59
+        )
60
+
61
+    @staticmethod
62
+    def canonicalize2(data: bytes) -> bytes:
63
+        """Return an encoded SSH string from a bytestring.
64
+
65
+        This is a helper function for hypothesis testing.
66
+
67
+        References:
68
+
69
+          * [David R. MacIver: Another invariant to test for
70
+            encoders][DECODE_ENCODE]
71
+
72
+        [DECODE_ENCODE]: https://hypothesis.works/articles/canonical-serialization/
73
+
74
+        """
75
+        unstringed, trailer = ssh_agent.SSHAgentClient.unstring_prefix(data)
76
+        assert not trailer
77
+        return ssh_agent.SSHAgentClient.string(unstringed)
78
+
34 79
     # TODO(the-13th-letter): Re-evaluate if this check is worth keeping.
35 80
     # It cannot provide true tamper-resistence, but probably appears to.
36 81
     @pytest.mark.parametrize(
... ...
@@ -155,6 +200,25 @@ class TestStaticFunctionality:
155 200
         uint32 = ssh_agent.SSHAgentClient.uint32
156 201
         assert uint32(input) == expected
157 202
 
203
+    @hypothesis.given(strategies.integers(min_value=0, max_value=0xFFFFFFFF))
204
+    @hypothesis.example(0xDEADBEEF).via('manual, pre-hypothesis example')
205
+    def test_210a_uint32_from_number(self, num: int) -> None:
206
+        """`uint32` encoding works, starting from numbers."""
207
+        uint32 = ssh_agent.SSHAgentClient.uint32
208
+        assert int.from_bytes(uint32(num), 'big', signed=False) == num
209
+
210
+    @hypothesis.given(strategies.binary(min_size=4, max_size=4))
211
+    @hypothesis.example(b'\xde\xad\xbe\xef').via(
212
+        'manual, pre-hypothesis example'
213
+    )
214
+    def test_210b_uint32_from_bytestring(self, bytestring: bytes) -> None:
215
+        """`uint32` encoding works, starting from length four byte strings."""
216
+        uint32 = ssh_agent.SSHAgentClient.uint32
217
+        assert (
218
+            uint32(int.from_bytes(bytestring, 'big', signed=False))
219
+            == bytestring
220
+        )
221
+
158 222
     @pytest.mark.parametrize(
159 223
         ['input', 'expected'],
160 224
         [
... ...
@@ -182,6 +246,17 @@ class TestStaticFunctionality:
182 246
         string = ssh_agent.SSHAgentClient.string
183 247
         assert bytes(string(input)) == expected
184 248
 
249
+    @hypothesis.given(strategies.binary(max_size=0x0001FFFF))
250
+    @hypothesis.example(b'DEADBEEF' * 10000).via(
251
+        'manual, pre-hypothesis example with highest order bit set'
252
+    )
253
+    def test_211a_string_from_bytestring(self, bytestring: bytes) -> None:
254
+        """SSH string encoding works, starting from a byte string."""
255
+        res = ssh_agent.SSHAgentClient.string(bytestring)
256
+        assert res.startswith((b'\x00\x00', b'\x00\x01'))
257
+        assert int.from_bytes(res[:4], 'big', signed=False) == len(bytestring)
258
+        assert res[4:] == bytestring
259
+
185 260
     @pytest.mark.parametrize(
186 261
         ['input', 'expected'],
187 262
         [
... ...
@@ -209,6 +284,58 @@ class TestStaticFunctionality:
209 284
             b'',
210 285
         )
211 286
 
287
+    @hypothesis.given(strategies.binary(max_size=0x00FFFFFF))
288
+    @hypothesis.example(b'\x00\x00\x00\x07ssh-rsa').via(
289
+        'manual, pre-hypothesis example to attempt to detect double-decoding'
290
+    )
291
+    @hypothesis.example(b'\x00\x00\x00\x01').via(
292
+        'detect no-op encoding via ill-formed SSH string'
293
+    )
294
+    def test_212a_unstring_of_string_of_data(self, bytestring: bytes) -> None:
295
+        """SSH string decoding of encoded SSH strings works.
296
+
297
+        References:
298
+
299
+          * [David R. MacIver: The Encode/Decode invariant][ENCODE_DECODE]
300
+
301
+        [ENCODE_DECODE]: https://hypothesis.works/articles/encode-decode-invariant/
302
+
303
+        """
304
+        string = ssh_agent.SSHAgentClient.string
305
+        unstring = ssh_agent.SSHAgentClient.unstring
306
+        unstring_prefix = ssh_agent.SSHAgentClient.unstring_prefix
307
+        encoded = string(bytestring)
308
+        assert unstring(encoded) == bytestring
309
+        assert unstring_prefix(encoded) == (bytestring, b'')
310
+        trailing_data = b'  trailing data'
311
+        encoded2 = string(bytestring) + trailing_data
312
+        assert unstring_prefix(encoded2) == (bytestring, trailing_data)
313
+
314
+    @hypothesis.given(
315
+        strategies.binary(max_size=0x00FFFFFF).map(
316
+            # Scoping issues, and the fact that staticmethod objects
317
+            # (before class finalization) are not callable, necessitate
318
+            # wrapping this staticmethod call in a lambda.
319
+            lambda x: TestStaticFunctionality.as_ssh_string(x)  # noqa: PLW0108
320
+        ),
321
+    )
322
+    def test_212b_string_of_unstring_of_data(self, encoded: bytes) -> None:
323
+        """SSH string decoding of encoded SSH strings works.
324
+
325
+        References:
326
+
327
+          * [David R. MacIver: Another invariant to test for
328
+            encoders][DECODE_ENCODE]
329
+
330
+        [DECODE_ENCODE]: https://hypothesis.works/articles/canonical-serialization/
331
+
332
+        """
333
+        canonical_functions = [self.canonicalize1, self.canonicalize2]
334
+        for canon1 in canonical_functions:
335
+            for canon2 in canonical_functions:
336
+                assert canon1(encoded) == canon2(encoded)
337
+                assert canon1(canon2(encoded)) == canon1(encoded)
338
+
212 339
     @pytest.mark.parametrize(
213 340
         ['value', 'exc_type', 'exc_pattern'],
214 341
         [
... ...
@@ -814,105 +941,3 @@ class TestAgentInteraction:
814 941
                 match=r'Malformed response|does not match request',
815 942
             ):
816 943
                 client.query_extensions()
817
-
818
-
819
-class TestHypotheses:
820
-    """Test properties via hypothesis."""
821
-
822
-    @staticmethod
823
-    def as_ssh_string(bytestring: bytes) -> bytes:
824
-        return int.to_bytes(len(bytestring), 4, 'big') + bytestring
825
-
826
-    @staticmethod
827
-    def canonicalize1(data: bytes) -> bytes:
828
-        return ssh_agent.SSHAgentClient.string(
829
-            ssh_agent.SSHAgentClient.unstring(data)
830
-        )
831
-
832
-    @staticmethod
833
-    def canonicalize2(data: bytes) -> bytes:
834
-        unstringed, trailer = ssh_agent.SSHAgentClient.unstring_prefix(data)
835
-        assert not trailer
836
-        return ssh_agent.SSHAgentClient.string(unstringed)
837
-
838
-    @hypothesis.given(strategies.integers(min_value=0, max_value=0xFFFFFFFF))
839
-    @hypothesis.example(0xDEADBEEF).via('manual, pre-hypothesis example')
840
-    def test_210a_uint32_from_number(self, num: int) -> None:
841
-        """`uint32` encoding works, starting from numbers."""
842
-        uint32 = ssh_agent.SSHAgentClient.uint32
843
-        assert int.from_bytes(uint32(num), 'big', signed=False) == num
844
-
845
-    @hypothesis.given(strategies.binary(min_size=4, max_size=4))
846
-    @hypothesis.example(b'\xde\xad\xbe\xef').via(
847
-        'manual, pre-hypothesis example'
848
-    )
849
-    def test_210b_uint32_from_bytestring(self, bytestring: bytes) -> None:
850
-        """`uint32` encoding works, starting from length four byte strings."""
851
-        uint32 = ssh_agent.SSHAgentClient.uint32
852
-        assert (
853
-            uint32(int.from_bytes(bytestring, 'big', signed=False))
854
-            == bytestring
855
-        )
856
-
857
-    @hypothesis.given(strategies.binary(max_size=0x0001FFFF))
858
-    @hypothesis.example(b'DEADBEEF' * 10000).via(
859
-        'manual, pre-hypothesis example with highest order bit set'
860
-    )
861
-    def test_211a_string_from_bytestring(self, bytestring: bytes) -> None:
862
-        """SSH string encoding works, starting from a byte string."""
863
-        res = ssh_agent.SSHAgentClient.string(bytestring)
864
-        assert res.startswith((b'\x00\x00', b'\x00\x01'))
865
-        assert int.from_bytes(res[:4], 'big', signed=False) == len(bytestring)
866
-        assert res[4:] == bytestring
867
-
868
-    @hypothesis.given(strategies.binary(max_size=0x00FFFFFF))
869
-    @hypothesis.example(b'\x00\x00\x00\x07ssh-rsa').via(
870
-        'manual, pre-hypothesis example to attempt to detect double-decoding'
871
-    )
872
-    @hypothesis.example(b'\x00\x00\x00\x01').via(
873
-        'detect no-op encoding via ill-formed SSH string'
874
-    )
875
-    def test_212a_unstring_of_string_of_data(self, bytestring: bytes) -> None:
876
-        """SSH string decoding of encoded SSH strings works.
877
-
878
-        References:
879
-
880
-          * [David R. MacIver: The Encode/Decode invariant][ENCODE_DECODE]
881
-
882
-        [ENCODE_DECODE]: https://hypothesis.works/articles/encode-decode-invariant/
883
-
884
-        """
885
-        string = ssh_agent.SSHAgentClient.string
886
-        unstring = ssh_agent.SSHAgentClient.unstring
887
-        unstring_prefix = ssh_agent.SSHAgentClient.unstring_prefix
888
-        encoded = string(bytestring)
889
-        assert unstring(encoded) == bytestring
890
-        assert unstring_prefix(encoded) == (bytestring, b'')
891
-        trailing_data = b'  trailing data'
892
-        encoded2 = string(bytestring) + trailing_data
893
-        assert unstring_prefix(encoded2) == (bytestring, trailing_data)
894
-
895
-    @hypothesis.given(
896
-        strategies.binary(max_size=0x00FFFFFF).map(
897
-            # Scoping issues, and the fact that staticmethod objects
898
-            # (before class finalization) are not callable, necessitate
899
-            # wrapping this staticmethod call in a lambda.
900
-            lambda x: TestHypotheses.as_ssh_string(x)  # noqa: PLW0108
901
-        ),
902
-    )
903
-    def test_212b_string_of_unstring_of_data(self, encoded: bytes) -> None:
904
-        """SSH string decoding of encoded SSH strings works.
905
-
906
-        References:
907
-
908
-          * [David R. MacIver: Another invariant to test for
909
-            encoders][DECODE_ENCODE]
910
-
911
-        [DECODE_ENCODE]: https://hypothesis.works/articles/canonical-serialization/
912
-
913
-        """
914
-        canonical_functions = [self.canonicalize1, self.canonicalize2]
915
-        for canon1 in canonical_functions:
916
-            for canon2 in canonical_functions:
917
-                assert canon1(encoded) == canon2(encoded)
918
-                assert canon1(canon2(encoded)) == canon1(encoded)
919 944