Skip to content

Commit b6bdd16

Browse files
authored
feat: implement binary selfie methods in Python (#496)
2 parents 529992f + 24a8029 commit b6bdd16

12 files changed

+188
-529
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import pytest
2+
from selfie_lib import expect_selfie
3+
4+
5+
def test_empty_binary_base64():
6+
"""Test base64 encoding of empty byte array"""
7+
expect_selfie(bytes()).to_be_base64("")
8+
9+
10+
def test_large_binary_base64():
11+
"""Test base64 encoding of large byte array (256 bytes)"""
12+
data = bytes(range(256))
13+
expect_selfie(data).to_be_base64(
14+
"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w=="
15+
)
16+
17+
18+
def test_binary_file():
19+
"""Test writing binary data to a file"""
20+
data = b"test binary data"
21+
expect_selfie(data).to_be_file("tests/binary_test__test_binary_file.bin")
22+
23+
24+
def test_binary_file_duplicate_equal():
25+
"""Test writing same binary data to a file multiple times"""
26+
expect_selfie(b"equal").to_be_file(
27+
"tests/binary_test__test_binary_file_duplicate_equal.bin"
28+
)
29+
expect_selfie(b"equal").to_be_file(
30+
"tests/binary_test__test_binary_file_duplicate_equal.bin"
31+
)
32+
33+
34+
def test_binary_file_duplicate_unequal():
35+
"""Test writing same binary data to a file multiple times"""
36+
with pytest.raises(Exception) as exc_info:
37+
expect_selfie(b"a").to_be_file(
38+
"tests/binary_test__test_binary_file_duplicate_unequal.bin"
39+
)
40+
expect_selfie(b"b").to_be_file(
41+
"tests/binary_test__test_binary_file_duplicate_unequal.bin"
42+
)
43+
expect_selfie(safify(str(exc_info.value))).to_be(
44+
"Snapshot mismatch, TODO: string comparison"
45+
)
46+
47+
48+
def test_binary_file_mismatch():
49+
"""Test error handling for mismatched binary data"""
50+
with pytest.raises(AssertionError):
51+
expect_selfie(b"different").to_be_file(
52+
"tests/binary_test__SHOULD_NOT_EXIST.bin"
53+
)
54+
55+
56+
def test_binary_file_not_found():
57+
"""Test error handling for non-existent file"""
58+
with pytest.raises(AssertionError) as exc_info:
59+
expect_selfie(b"test").to_be_file("tests/binary_test__SHOULD_NOT_EXIST.bin")
60+
assert "no such file" in str(exc_info.value)
61+
62+
63+
def test_base64_mismatch():
64+
"""Test error handling for mismatched base64 data"""
65+
with pytest.raises(Exception) as exc_info:
66+
expect_selfie(b"test data").to_be_base64("AAAA")
67+
expect_selfie(safify(str(exc_info.value))).to_be(
68+
"Snapshot mismatch, TODO: string comparison"
69+
)
70+
71+
72+
def safify(string: str) -> str:
73+
return string.split("\n")[0]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test binary data
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
equal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
a

python/example-pytest-selfie/tests/to_be_file_test.py

-24
This file was deleted.

python/example-pytest-selfie/uv.lock

-233
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

python/pytest-selfie/pytest_selfie/plugin.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def __nullable_to_string(self, value, on_null: str) -> str:
5656
def __comparison_assertion(
5757
self, message: str, expected: str, actual: str
5858
) -> Exception:
59-
# this *should* through an exception that a good pytest runner will show nicely
59+
# this *should* throw an exception that a good pytest runner will show nicely
6060
assert expected == actual, message
6161
# but in case it doesn't, we'll create our own here
6262
return AssertionError(message)
@@ -69,6 +69,9 @@ def __init__(self, fs: FSImplementation, settings: SelfieSettingsAPI):
6969
self.__root_folder = TypedPath.of_folder(os.path.abspath(settings.root_dir))
7070
self.unix_newlines = self.__infer_default_line_ending_is_unix()
7171

72+
def root_folder(self) -> TypedPath:
73+
return self.__root_folder
74+
7275
def snapshotfile_for_testfile(self, testfile: TypedPath) -> TypedPath:
7376
if testfile.name.endswith(".py"):
7477
return testfile.parent_folder().resolve_file(f"{testfile.name[:-3]}.ss")

python/pytest-selfie/uv.lock

-79
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

python/selfie-lib/selfie_lib/SelfieImplementations.py

+83-14
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def to_match_disk_TODO(self, sub: str = "") -> "DiskSelfie":
8181
return self
8282
else:
8383
raise _selfieSystem().fs.assert_failed(
84-
f"Can't call `toMatchDisk_TODO` in {Mode.readonly} mode!"
84+
message=f"Can't call `toMatchDisk_TODO` in {Mode.readonly} mode!"
8585
)
8686

8787
def facet(self, facet: str) -> "StringFacet":
@@ -186,17 +186,71 @@ def __init__(self, actual: Snapshot, disk: DiskStorage, only_facet: str):
186186
f"The facet {only_facet} is a string, not a binary snapshot"
187187
)
188188

189-
def to_be_base64(self, expected: str) -> bytes:
190-
raise NotImplementedError
189+
def _actual_bytes(self) -> bytes:
190+
return self.actual.subject_or_facet(self.only_facet).value_binary()
191+
192+
def to_match_disk(self, sub: str = "") -> "BinarySelfie":
193+
super().to_match_disk(sub)
194+
return self
195+
196+
def to_match_disk_TODO(self, sub: str = "") -> "BinarySelfie":
197+
super().to_match_disk_TODO(sub)
198+
return self
191199

192200
def to_be_base64_TODO(self, _: Any = None) -> bytes:
193-
raise NotImplementedError
201+
_toBeDidntMatch(None, self._actual_string(), LiteralString())
202+
return self._actual_bytes()
194203

195-
def to_be_file(self, subpath: str) -> bytes:
196-
raise NotImplementedError
204+
def to_be_base64(self, expected: str) -> bytes:
205+
expected_bytes = base64.b64decode(expected)
206+
actual_bytes = self._actual_bytes()
207+
if actual_bytes == expected_bytes:
208+
return _checkSrc(actual_bytes)
209+
else:
210+
_toBeDidntMatch(expected, self._actual_string(), LiteralString())
211+
return actual_bytes
212+
213+
def _actual_string(self) -> str:
214+
return base64.b64encode(self._actual_bytes()).decode().replace("\r", "")
215+
216+
def _to_be_file_impl(self, subpath: str, is_todo: bool) -> bytes:
217+
call = recordCall(False)
218+
writable = _selfieSystem().mode.can_write(is_todo, call, _selfieSystem())
219+
actual_bytes = self._actual_bytes()
220+
path = _selfieSystem().layout.root_folder().resolve_file(subpath)
221+
222+
if writable:
223+
if is_todo:
224+
_selfieSystem().write_inline(TodoStub.to_be_file.create_literal(), call)
225+
_selfieSystem().write_to_be_file(path, actual_bytes, call)
226+
return actual_bytes
227+
else:
228+
if is_todo:
229+
raise _selfieSystem().fs.assert_failed(
230+
f"Can't call `to_be_file_TODO` in {Mode.readonly} mode!"
231+
)
232+
else:
233+
if not _selfieSystem().fs.file_exists(path):
234+
raise _selfieSystem().fs.assert_failed(
235+
_selfieSystem().mode.msg_snapshot_not_found_no_such_file(path)
236+
)
237+
expected = _selfieSystem().fs.file_read_binary(path)
238+
if expected == actual_bytes:
239+
return actual_bytes
240+
else:
241+
raise _selfieSystem().fs.assert_failed(
242+
message=_selfieSystem().mode.msg_snapshot_mismatch_binary(
243+
expected, actual_bytes
244+
),
245+
expected=expected,
246+
actual=actual_bytes,
247+
)
197248

198249
def to_be_file_TODO(self, subpath: str) -> bytes:
199-
raise NotImplementedError
250+
return self._to_be_file_impl(subpath, True)
251+
252+
def to_be_file(self, subpath: str) -> bytes:
253+
return self._to_be_file_impl(subpath, False)
200254

201255

202256
def _checkSrc(value: T) -> T:
@@ -216,16 +270,27 @@ def _toBeDidntMatch(expected: Optional[T], actual: T, fmt: LiteralFormat[T]) ->
216270
f"Can't call `toBe_TODO` in {Mode.readonly} mode!"
217271
)
218272
else:
219-
raise _selfieSystem().fs.assert_failed(
220-
_selfieSystem().mode.msg_snapshot_mismatch(), expected, actual
221-
)
273+
expectedStr = repr(expected)
274+
actualStr = repr(actual)
275+
if expectedStr == actualStr:
276+
raise ValueError(
277+
f"Value of type {type(actual)} is not `==` to the expected value, but they both have the same `repr` value:\n${expectedStr}"
278+
)
279+
else:
280+
raise _selfieSystem().fs.assert_failed(
281+
message=_selfieSystem().mode.msg_snapshot_mismatch(
282+
expected=expectedStr, actual=actualStr
283+
),
284+
expected=expected,
285+
actual=actual,
286+
)
222287

223288

224289
def _assertEqual(
225290
expected: Optional[Snapshot], actual: Snapshot, storage: SnapshotSystem
226291
):
227292
if expected is None:
228-
raise storage.fs.assert_failed(storage.mode.msg_snapshot_not_found())
293+
raise storage.fs.assert_failed(message=storage.mode.msg_snapshot_not_found())
229294
elif expected == actual:
230295
return
231296
else:
@@ -240,10 +305,14 @@ def _assertEqual(
240305
),
241306
)
242307
)
308+
expectedFacets = _serializeOnlyFacets(expected, mismatched_keys)
309+
actualFacets = _serializeOnlyFacets(actual, mismatched_keys)
243310
raise storage.fs.assert_failed(
244-
storage.mode.msg_snapshot_mismatch(),
245-
_serializeOnlyFacets(expected, mismatched_keys),
246-
_serializeOnlyFacets(actual, mismatched_keys),
311+
message=storage.mode.msg_snapshot_mismatch(
312+
expected=expectedFacets, actual=actualFacets
313+
),
314+
expected=expectedFacets,
315+
actual=actualFacets,
247316
)
248317

249318

0 commit comments

Comments
 (0)