Selfie
Loading...
Searching...
No Matches
SnapshotSystem.py
Go to the documentation of this file.
1from abc import ABC, abstractmethod
2from collections.abc import ByteString
3from enum import Enum, auto
4from typing import Optional
5
6from .CommentTracker import CommentTracker
7from .FS import FS
8from .Literals import LiteralValue
9from .Snapshot import Snapshot
10from .TypedPath import TypedPath
11from .WriteTracker import CallStack, SnapshotFileLayout
12
13
14class DiskStorage(ABC):
15 from .WriteTracker import CallStack
16
17 @abstractmethod
18 def read_disk(self, sub: str, call: CallStack) -> Optional[Snapshot]:
19 pass
20
21 @abstractmethod
22 def write_disk(self, actual: Snapshot, sub: str, call: CallStack):
23 pass
24
25 @abstractmethod
26 def keep(self, sub_or_keep_all: Optional[str]):
27 pass
28
29
30class SnapshotSystem(ABC):
31 @property
32 @abstractmethod
33 def fs(self) -> FS: ...
34
35 @property
36 @abstractmethod
37 def mode(self) -> "Mode": ...
38
39 @property
40 @abstractmethod
41 def layout(self) -> SnapshotFileLayout: ...
42
43 @abstractmethod
44 def source_file_has_writable_comment(self, call: CallStack) -> bool:
45 """
46 Returns true if the sourcecode for the given call has a writable annotation.
47 """
48 ...
49
50 @abstractmethod
51 def write_inline(self, literal_value: LiteralValue, call: CallStack) -> None:
52 """
53 Indicates that the following value should be written into test sourcecode.
54 """
55 ...
56
57 @abstractmethod
59 self, path: TypedPath, data: ByteString, call: CallStack
60 ) -> None:
61 """
62 Writes the given bytes to the given file, checking for duplicate writes.
63 """
64 ...
65
66 @abstractmethod
67 def disk_thread_local(self) -> DiskStorage:
68 """
69 Returns the DiskStorage for the test associated with this thread, else error.
70 """
71 ...
72
73
74selfieSystem = None
75
76
77def _selfieSystem() -> "SnapshotSystem":
78 if selfieSystem is None:
79 raise Exception(
80 "Selfie system not initialized, make sure that `pytest-selfie` is installed and that you are running tests with `pytest`."
81 )
82 return selfieSystem
83
84
85def _initSelfieSystem(system: SnapshotSystem):
86 global selfieSystem
87 if selfieSystem is not None:
88 raise Exception("Selfie system already initialized")
89 selfieSystem = system
90
91
92def _clearSelfieSystem(system: SnapshotSystem):
93 global selfieSystem
94 if selfieSystem is not system:
95 raise Exception("This was not the running system!")
96 selfieSystem = None
97
98
99class Mode(Enum):
100 interactive = auto()
101 readonly = auto()
102 overwrite = auto()
103
104 def can_write(self, is_todo: bool, call: CallStack, system: SnapshotSystem) -> bool:
105 if self == Mode.interactive:
106 return is_todo or system.source_file_has_writable_comment(call)
107 elif self == Mode.readonly:
108 if system.source_file_has_writable_comment(call):
109 layout = system.layout
110 path = layout.sourcefile_for_call(call.location)
111 comment, line = CommentTracker.commentString(path)
112 raise system.fs.assert_failed(
113 f"Selfie is in readonly mode, so `{comment}` is illegal at {call.location.with_line(line).ide_link(layout)}"
114 )
115 return False
116 elif self == Mode.overwrite:
117 return True
118 else:
119 raise ValueError(f"Unknown mode: {self}")
120
121 def msg_snapshot_not_found(self) -> str:
122 return self.msg("Snapshot not found")
123
125 return self.msg(f"Snapshot not found: no such file {file}")
126
127 def msg_snapshot_mismatch(self, expected: str, actual: str) -> str: # noqa: ARG002
128 return self.msg(
129 "Snapshot mismatch (error msg could be better https://github.com/diffplug/selfie/issues/501)"
130 )
131
132 def msg_snapshot_mismatch_binary(self, expected: bytes, actual: bytes) -> str:
133 return self.msg_snapshot_mismatch(
134 self._to_quoted_printable(expected), self._to_quoted_printable(actual)
135 )
136
137 def _to_quoted_printable(self, byte_data: bytes) -> str:
138 result = []
139 for b in byte_data:
140 # b is already an integer in [0..255] when iterating through a bytes object
141 if 33 <= b <= 126 and b != 61: # '=' is ASCII 61, so skip it
142 result.append(chr(b))
143 else:
144 # Convert to uppercase hex, pad to 2 digits, and prepend '='
145 result.append(f"={b:02X}")
146 return "".join(result)
147
148 def msg(self, headline: str) -> str:
149 if self == Mode.interactive:
150 return (
151 f"{headline}\n"
152 "- update this snapshot by adding `_TODO` to the function name\n"
153 "- update all snapshots in this file by adding `#selfieonce` or `#SELFIEWRITE`"
154 )
155 elif self == Mode.readonly:
156 return headline
157 elif self == Mode.overwrite:
158 return f"{headline}\n(didn't expect this to ever happen in overwrite mode)"
159 else:
160 raise ValueError(f"Unknown mode: {self}")
keep(self, Optional[str] sub_or_keep_all)
write_disk(self, Snapshot actual, str sub, CallStack call)
Optional[Snapshot] read_disk(self, str sub, CallStack call)
bool can_write(self, bool is_todo, CallStack call, SnapshotSystem system)
str msg(self, str headline)
str msg_snapshot_mismatch(self, str expected, str actual)
str msg_snapshot_not_found_no_such_file(self, file)
str _to_quoted_printable(self, bytes byte_data)
str msg_snapshot_mismatch_binary(self, bytes expected, bytes actual)
None write_to_be_file(self, TypedPath path, ByteString data, CallStack call)
bool source_file_has_writable_comment(self, CallStack call)
None write_inline(self, LiteralValue literal_value, CallStack call)
"SnapshotSystem" _selfieSystem()