Selfie
Loading...
Searching...
No Matches
SelfieImplementations.py
Go to the documentation of this file.
1import base64
2from abc import ABC, abstractmethod
3from itertools import chain
4from typing import Any, Generic, Optional, TypeVar
5
6from .Literals import (
7 LiteralFormat,
8 LiteralRepr,
9 LiteralString,
10 LiteralValue,
11 TodoStub,
12)
13from .Snapshot import Snapshot
14from .SnapshotFile import SnapshotFile
15from .SnapshotSystem import DiskStorage, Mode, SnapshotSystem, _selfieSystem
16from .WriteTracker import recordCall as recordCall
17
18T = TypeVar("T")
19
20
21class FluentFacet(ABC):
22 @abstractmethod
23 def facet(self, facet: str) -> "StringFacet":
24 """Extract a single facet from a snapshot in order to do an inline snapshot."""
25
26 @abstractmethod
27 def facets(self, *facets: str) -> "StringFacet":
28 """Extract multiple facets from a snapshot in order to do an inline snapshot."""
29
30 @abstractmethod
31 def facet_binary(self, facet: str) -> "BinaryFacet":
32 pass
33
34
36 @abstractmethod
37 def to_be(self, expected: str) -> str:
38 pass
39
40 @abstractmethod
41 def to_be_TODO(self, _: Any = None) -> str:
42 pass
43
44
46 @abstractmethod
47 def to_be_base64(self, expected: str) -> bytes:
48 pass
49
50 @abstractmethod
51 def to_be_base64_TODO(self, _: Any = None) -> bytes:
52 pass
53
54 @abstractmethod
55 def to_be_file(self, subpath: str) -> bytes:
56 pass
57
58 @abstractmethod
59 def to_be_file_TODO(self, subpath: str) -> bytes:
60 pass
61
62
64 def __init__(self, actual: Snapshot, disk: DiskStorage):
65 self.actual = actual
66 self.disk = disk
67
68 def to_match_disk(self, sub: str = "") -> "DiskSelfie":
69 call = recordCall(False)
70 if _selfieSystem().mode.can_write(False, call, _selfieSystem()):
71 self.disk.write_disk(self.actual, sub, call)
72 else:
73 _assert_equal(self.disk.read_disk(sub, call), self.actual, _selfieSystem())
74 return self
75
76 def to_match_disk_TODO(self, sub: str = "") -> "DiskSelfie":
77 call = recordCall(False)
78 if _selfieSystem().mode.can_write(True, call, _selfieSystem()):
79 self.disk.write_disk(self.actual, sub, call)
80 _selfieSystem().write_inline(TodoStub.to_match_disk.create_literal(), call)
81 return self
82 else:
83 raise _selfieSystem().fs.assert_failed(
84 message=f"Can't call `toMatchDisk_TODO` in {Mode.readonly} mode!"
85 )
86
87 def facet(self, facet: str) -> "StringFacet":
88 return StringSelfie(self.actual, self.disk, [facet])
89
90 def facets(self, *facets: str) -> "StringFacet":
91 return StringSelfie(self.actual, self.disk, list(facets))
92
93 def facet_binary(self, facet: str) -> "BinaryFacet":
94 return BinarySelfie(self.actual, self.disk, facet)
95
96
97class ReprSelfie(DiskSelfie, Generic[T]):
98 def __init__(self, actual_before_repr: T, actual: Snapshot, disk: DiskStorage):
99 super().__init__(actual, disk)
100 self.actual_before_repr = actual_before_repr
101
102 def to_be_TODO(self, _: Optional[T] = None) -> T:
104
105 def to_be(self, expected: T) -> T:
106 if self.actual_before_repr == expected:
107 return _check_src(self.actual_before_repr)
108 else:
109 return _to_be_didnt_match(expected, self.actual_before_repr, LiteralRepr())
110
111
112class StringSelfie(ReprSelfie[str], StringFacet):
114 self,
115 actual: Snapshot,
116 disk: DiskStorage,
117 only_facets: Optional[list[str]] = None,
118 ):
119 super().__init__("<IT IS AN ERROR FOR THIS TO BE VISIBLE>", actual, disk)
120 self.only_facets = only_facets
121
122 if self.only_facets is not None:
123 assert all(
124 facet == "" or facet in actual.facets for facet in self.only_facets
125 ), f"The following facets were not found in the snapshot: {[facet for facet in self.only_facets if actual.subject_or_facet_maybe(facet) is None]}\navailable facets are: {list(actual.facets.keys())}"
126 assert (
127 len(self.only_facets) > 0
128 ), "Must have at least one facet to display, this was empty."
129 if "" in self.only_facets:
130 assert (
131 self.only_facets.index("") == 0
132 ), f'If you\'re going to specify the subject facet (""), you have to list it first, this was {self.only_facets}'
133
134 def to_match_disk(self, sub: str = "") -> "StringSelfie":
135 super().to_match_disk(sub)
136 return self
137
138 def to_match_disk_TODO(self, sub: str = "") -> "StringSelfie":
139 super().to_match_disk_TODO(sub)
140 return self
141
142 def __actual(self) -> str:
143 if not self.actualactual.facets or (self.only_facets and len(self.only_facets) == 1):
144 # single value doesn't have to worry about escaping at all
145 only_value = self.actualactual.subject_or_facet(
146 self.only_facets[0] if self.only_facets else ""
147 )
148 if only_value.is_binary:
149 return (
150 base64.b64encode(only_value.value_binary())
151 .decode()
152 .replace("\r", "")
153 )
154 else:
155 return only_value.value_string()
156 else:
158 self.actualactual, self.only_facets or ["", *list(self.actualactual.facets.keys())]
159 )
160
161 def to_be_TODO(self, _: Any = None) -> str:
162 return _to_be_didnt_match(None, self.__actual(), LiteralString())
163
164 def to_be(self, expected: str) -> str:
165 actual_string = self.__actual()
166 if actual_string == expected:
167 return _check_src(actual_string)
168 else:
169 return _to_be_didnt_match(
170 expected,
171 actual_string,
173 )
174
175
176class BinarySelfie(ReprSelfie[bytes], BinaryFacet):
177 def __init__(self, actual: Snapshot, disk: DiskStorage, only_facet: str):
178 super().__init__(actual.subject.value_binary(), actual, disk)
179 self.only_facet = only_facet
180
181 facet_value = actual.subject_or_facet_maybe(only_facet)
182 if facet_value is None:
183 raise ValueError(f"The facet {only_facet} was not found in the snapshot")
184 elif not facet_value.is_binary:
185 raise ValueError(
186 f"The facet {only_facet} is a string, not a binary snapshot"
187 )
188
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
199
200 def to_be_base64_TODO(self, _: Any = None) -> bytes:
202 return self._actual_bytes()
203
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 _check_src(actual_bytes)
209 else:
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 )
248
249 def to_be_file_TODO(self, subpath: str) -> bytes:
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)
254
255
256def _check_src(value: T) -> T:
257 _selfieSystem().mode.can_write(False, recordCall(True), _selfieSystem())
258 return value
259
260
261def _to_be_didnt_match(expected: Optional[T], actual: T, fmt: LiteralFormat[T]) -> T:
262 call = recordCall(False)
263 writable = _selfieSystem().mode.can_write(expected is None, call, _selfieSystem())
264 if writable:
265 _selfieSystem().write_inline(LiteralValue(expected, actual, fmt), call)
266 return actual
267 else:
268 if expected is None:
269 raise _selfieSystem().fs.assert_failed(
270 f"Can't call `to_be_TODO` in {Mode.readonly} mode!"
271 )
272 else:
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 )
287
288
290 expected: Optional[Snapshot], actual: Snapshot, storage: SnapshotSystem
291):
292 if expected is None:
293 raise storage.fs.assert_failed(message=storage.mode.msg_snapshot_not_found())
294 elif expected == actual:
295 return
296 else:
297 mismatched_keys = sorted(
298 filter(
299 lambda facet: expected.subject_or_facet_maybe(facet)
300 != actual.subject_or_facet_maybe(facet),
301 chain(
302 [""],
303 expected.facets.keys(),
304 (facet for facet in actual.facets if facet not in expected.facets),
305 ),
306 )
307 )
308 expectedFacets = _serialize_only_facets(expected, mismatched_keys)
309 actualFacets = _serialize_only_facets(actual, mismatched_keys)
310 raise storage.fs.assert_failed(
311 message=storage.mode.msg_snapshot_mismatch(
312 expected=expectedFacets, actual=actualFacets
313 ),
314 expected=expectedFacets,
315 actual=actualFacets,
316 )
317
318
319def _serialize_only_facets(snapshot: Snapshot, keys: list[str]) -> str:
320 writer = []
321 for key in keys:
322 if not key:
323 SnapshotFile.writeEntry(writer, "", None, snapshot.subject_or_facet(key))
324 else:
325 value = snapshot.subject_or_facet(key)
326 if value is not None:
327 SnapshotFile.writeEntry(writer, "", key, value)
328
329 EMPTY_KEY_AND_FACET = "╔═ ═╗\n"
330 writer_str = "".join(writer)
331
332 if writer_str.startswith(EMPTY_KEY_AND_FACET):
333 # this codepath is triggered by the `key.isEmpty()` line above
334 return writer_str[len(EMPTY_KEY_AND_FACET) : -1]
335 else:
336 return writer_str[:-1]
"BinarySelfie" to_match_disk_TODO(self, str sub="")
__init__(self, Snapshot actual, DiskStorage disk, str only_facet)
bytes _to_be_file_impl(self, str subpath, bool is_todo)
__init__(self, Snapshot actual, DiskStorage disk)
"DiskSelfie" to_match_disk_TODO(self, str sub="")
__init__(self, T actual_before_repr, Snapshot actual, DiskStorage disk)
__init__(self, Snapshot actual, DiskStorage disk, Optional[list[str]] only_facets=None)
"StringSelfie" to_match_disk_TODO(self, str sub="")
T _to_be_didnt_match(Optional[T] expected, T actual, LiteralFormat[T] fmt)
str _serialize_only_facets(Snapshot snapshot, list[str] keys)
_assert_equal(Optional[Snapshot] expected, Snapshot actual, SnapshotSystem storage)