Skip to content

Commit c757b1c

Browse files
authored
Inline snapshots are working (#354)
2 parents 1b883ab + d57967a commit c757b1c

File tree

6 files changed

+102
-43
lines changed

6 files changed

+102
-43
lines changed

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
# def test_read_fail():
99
# expect_selfie("A").to_be("B")
1010

11-
"""
11+
1212
def test_write():
1313
expect_selfie("B").to_be_TODO()
14-
"""

python/selfie-lib/selfie_lib/CommentTracker.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def paths_with_once(self) -> Iterable[TypedPath]:
3232
]
3333

3434
def hasWritableComment(self, call: CallStack, layout: SnapshotFileLayout) -> bool:
35-
path = layout.sourcefile_for_call(call)
35+
path = layout.sourcefile_for_call(call.location)
3636
with self.lock:
3737
if path in self.cache:
3838
comment = self.cache[path]

python/selfie-lib/selfie_lib/SnapshotSystem.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def can_write(self, is_todo: bool, call: CallStack, system: SnapshotSystem) -> b
109109
elif self == Mode.readonly:
110110
if system.source_file_has_writable_comment(call):
111111
layout = system.layout
112-
path = layout.sourcefile_for_call(call)
112+
path = layout.sourcefile_for_call(call.location)
113113
comment, line = CommentTracker.commentString(path)
114114
raise system.fs.assert_failed(
115115
f"Selfie is in readonly mode, so `{comment}` is illegal at {call.location.with_line(line).ide_link(layout)}"

python/selfie-lib/selfie_lib/SourceFile.py

+46-33
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
from .Slice import Slice
23
from .Literals import Language, LiteralFormat, LiteralValue
34
from .EscapeLeadingWhitespace import EscapeLeadingWhitespace
@@ -9,15 +10,15 @@ class SourceFile:
910

1011
def __init__(self, filename: str, content: str) -> None:
1112
self.__unix_newlines: bool = "\r" not in content
12-
self.__content_slice: Slice = Slice(content.replace("\r\n", "\n"))
13+
self._content_slice: Slice = Slice(content.replace("\r\n", "\n"))
1314
self.__language: Language = Language.from_filename(filename)
1415
self.__escape_leading_whitespace = EscapeLeadingWhitespace.appropriate_for(
15-
self.__content_slice.__str__()
16+
self._content_slice.__str__()
1617
)
1718

1819
def remove_selfie_once_comments(self):
1920
# Split content into lines
20-
lines = self.__content_slice.__str__().split("\n")
21+
lines = self._content_slice.__str__().split("\n")
2122

2223
# Create a new list of lines, excluding lines containing '# selfieonce' or '#selfieonce'
2324
new_lines = []
@@ -39,28 +40,30 @@ def remove_selfie_once_comments(self):
3940
new_content = "\n".join(new_lines)
4041

4142
# Update the content slice with new content
42-
self.__content_slice = Slice(new_content)
43+
self._content_slice = Slice(new_content)
4344

4445
if not self.__unix_newlines:
45-
self.__content_slice = Slice(new_content.replace("\n", "\r\n"))
46+
self._content_slice = Slice(new_content.replace("\n", "\r\n"))
4647

4748
@property
4849
def as_string(self) -> str:
4950
return (
50-
self.__content_slice.__str__()
51+
self._content_slice.__str__()
5152
if self.__unix_newlines
52-
else self.__content_slice.__str__().replace("\n", "\r\n")
53+
else self._content_slice.__str__().replace("\n", "\r\n")
5354
)
5455

5556
class ToBeLiteral:
5657
def __init__(
5758
self,
59+
parent: "SourceFile",
5860
dot_fun_open_paren: str,
5961
function_call_plus_arg: Slice,
6062
arg: Slice,
6163
language: Language,
6264
escape_leading_whitespace: EscapeLeadingWhitespace,
6365
) -> None:
66+
self.__parent = parent
6467
self.__dot_fun_open_paren = dot_fun_open_paren
6568
self.__function_call_plus_arg = function_call_plus_arg
6669
self.__arg = arg
@@ -92,16 +95,19 @@ def set_literal_and_get_newline_delta(self, literal_value: LiteralValue) -> int:
9295
)
9396
existing_newlines = self.__function_call_plus_arg.count("\n")
9497
new_newlines = encoded.count("\n")
95-
self.__content_slice = self.__function_call_plus_arg.replaceSelfWith(
96-
f"{self.__dot_fun_open_paren}{encoded})"
98+
self.__parent._content_slice = Slice(
99+
self.__function_call_plus_arg.replaceSelfWith(
100+
f"{self.__dot_fun_open_paren}{encoded})"
101+
)
97102
)
103+
98104
return new_newlines - existing_newlines
99105

100106
def parse_literal(self, literal_format: LiteralFormat) -> Any:
101107
return literal_format.parse(self.__arg.__str__(), self.__language)
102108

103109
def find_on_line(self, to_find: str, line_one_indexed: int) -> Slice:
104-
line_content = self.__content_slice.unixLine(line_one_indexed)
110+
line_content = self._content_slice.unixLine(line_one_indexed)
105111
idx = line_content.indexOf(to_find)
106112
if idx == -1:
107113
raise AssertionError(
@@ -115,12 +121,12 @@ def find_on_line(self, to_find: str, line_one_indexed: int) -> Slice:
115121
def replace_on_line(self, line_one_indexed: int, find: str, replace: str) -> None:
116122
assert "\n" not in find
117123
assert "\n" not in replace
118-
line_content = self.__content_slice.unixLine(line_one_indexed).__str__()
124+
line_content = self._content_slice.unixLine(line_one_indexed).__str__()
119125
new_content = line_content.replace(find, replace)
120-
self.__content_slice = Slice(self.__content_slice.replaceSelfWith(new_content))
126+
self._content_slice = Slice(self._content_slice.replaceSelfWith(new_content))
121127

122128
def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral:
123-
line_content = self.__content_slice.unixLine(line_one_indexed)
129+
line_content = self._content_slice.unixLine(line_one_indexed)
124130
dot_fun_open_paren = None
125131

126132
for to_be_like in TO_BE_LIKES:
@@ -137,24 +143,24 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral:
137143
dot_function_call = dot_function_call_in_place + line_content.startIndex
138144
arg_start = dot_function_call + len(dot_fun_open_paren)
139145

140-
if self.__content_slice.__len__() == arg_start:
146+
if self._content_slice.__len__() == arg_start:
141147
raise AssertionError(
142148
f"Appears to be an unclosed function call `{dot_fun_open_paren}` "
143149
f"on line {line_one_indexed}"
144150
)
145-
while self.__content_slice[arg_start].isspace():
151+
while self._content_slice[arg_start].isspace():
146152
arg_start += 1
147-
if self.__content_slice.__len__() == arg_start:
153+
if self._content_slice.__len__() == arg_start:
148154
raise AssertionError(
149155
f"Appears to be an unclosed function call `{dot_fun_open_paren}` "
150156
f"on line {line_one_indexed}"
151157
)
152158

153159
end_arg = -1
154160
end_paren = 0
155-
if self.__content_slice[arg_start] == '"':
156-
if self.__content_slice[arg_start].startswith(self.TRIPLE_QUOTE):
157-
end_arg = self.__content_slice.indexOf(
161+
if self._content_slice[arg_start] == '"':
162+
if self._content_slice[arg_start].startswith(self.TRIPLE_QUOTE):
163+
end_arg = self._content_slice.indexOf(
158164
self.TRIPLE_QUOTE, arg_start + len(self.TRIPLE_QUOTE)
159165
)
160166
if end_arg == -1:
@@ -168,11 +174,11 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral:
168174
else:
169175
end_arg = arg_start + 1
170176
while (
171-
self.__content_slice[end_arg] != '"'
172-
or self.__content_slice[end_arg - 1] == "\\"
177+
self._content_slice[end_arg] != '"'
178+
or self._content_slice[end_arg - 1] == "\\"
173179
):
174180
end_arg += 1
175-
if end_arg == self.__content_slice.__len__():
181+
if end_arg == self._content_slice.__len__():
176182
raise AssertionError(
177183
f'Appears to be an unclosed string literal `"` '
178184
f"on line {line_one_indexed}"
@@ -181,37 +187,44 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral:
181187
end_paren = end_arg
182188
else:
183189
end_arg = arg_start
184-
while not self.__content_slice[end_arg].isspace():
185-
if self.__content_slice[end_arg] == ")":
190+
while not self._content_slice[end_arg].isspace():
191+
if self._content_slice[end_arg] == ")":
186192
break
187193
end_arg += 1
188-
if end_arg == self.__content_slice.__len__():
194+
if end_arg == self._content_slice.__len__():
189195
raise AssertionError(
190196
f"Appears to be an unclosed numeric literal "
191197
f"on line {line_one_indexed}"
192198
)
193199
end_paren = end_arg
194-
while self.__content_slice[end_paren] != ")":
195-
if not self.__content_slice[end_paren].isspace():
200+
while self._content_slice[end_paren] != ")":
201+
if not self._content_slice[end_paren].isspace():
196202
raise AssertionError(
197203
f"Non-primitive literal in `{dot_fun_open_paren}` starting at "
198204
f"line {line_one_indexed}: error for character "
199-
f"`{self.__content_slice[end_paren]}` on line "
200-
f"{self.__content_slice.baseLineAtOffset(end_paren)}"
205+
f"`{self._content_slice[end_paren]}` on line "
206+
f"{self._content_slice.baseLineAtOffset(end_paren)}"
201207
)
202208
end_paren += 1
203-
if end_paren == self.__content_slice.__len__():
209+
if end_paren == self._content_slice.__len__():
204210
raise AssertionError(
205211
f"Appears to be an unclosed function call `{dot_fun_open_paren}` "
206212
f"starting at line {line_one_indexed}"
207213
)
208214
return self.ToBeLiteral(
215+
self,
209216
dot_fun_open_paren.replace("_TODO", ""),
210-
self.__content_slice.subSequence(dot_function_call, end_paren + 1),
211-
self.__content_slice.subSequence(arg_start, end_arg),
217+
self._content_slice.subSequence(dot_function_call, end_paren + 1),
218+
self._content_slice.subSequence(arg_start, end_arg),
212219
self.__language,
213220
self.__escape_leading_whitespace,
214221
)
215222

216223

217-
TO_BE_LIKES = [".toBe(", ".toBe_TODO(", ".toBeBase64(", ".toBeBase64_TODO("]
224+
TO_BE_LIKES = [
225+
".to_be(",
226+
".to_be_TODO(",
227+
".to_be_base64(",
228+
".to_be_base64_TODO(",
229+
".to_be_TODO(",
230+
]

python/selfie-lib/selfie_lib/WriteTracker.py

+46-6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from functools import total_ordering
88

99
from .SourceFile import SourceFile
10-
from .Literals import LiteralValue
10+
from .Literals import LiteralValue, LiteralTodoStub
1111
from .TypedPath import TypedPath
1212
from .FS import FS
1313

@@ -87,8 +87,8 @@ class SnapshotFileLayout:
8787
def __init__(self, fs: FS):
8888
self.fs = fs
8989

90-
def sourcefile_for_call(self, call: CallStack) -> TypedPath:
91-
file_path = call.location.file_name
90+
def sourcefile_for_call(self, call: CallLocation) -> TypedPath:
91+
file_path = call.file_name
9292
if not file_path:
9393
raise ValueError("No file path available in CallLocation.")
9494
return TypedPath(os.path.abspath(Path(file_path)))
@@ -174,8 +174,7 @@ def record(
174174
):
175175
super().recordInternal(call.location, snapshot, call, layout)
176176

177-
call_stack_from_location = CallStack(call.location, [])
178-
file = layout.sourcefile_for_call(call_stack_from_location)
177+
file = layout.sourcefile_for_call(call.location)
179178

180179
if snapshot.expected is not None:
181180
content = SourceFile(file.name, layout.fs.file_read(file))
@@ -197,4 +196,45 @@ def record(
197196
)
198197

199198
def persist_writes(self, layout: SnapshotFileLayout):
200-
raise NotImplementedError("InlineWriteTracker does not support persist_writes")
199+
# Assuming there is at least one write to process
200+
if not self.writes:
201+
return
202+
203+
# Sorting writes based on file name and line number
204+
sorted_writes = sorted(
205+
self.writes.values(),
206+
key=lambda x: (x.call_stack.location.file_name, x.call_stack.location.line),
207+
)
208+
209+
# Initialize from the first write
210+
first_write = sorted_writes[0]
211+
current_file = layout.sourcefile_for_call(first_write.call_stack.location)
212+
content = SourceFile(current_file.name, layout.fs.file_read(current_file))
213+
delta_line_numbers = 0
214+
215+
for write in sorted_writes:
216+
# Determine the file path for the current write
217+
file_path = layout.sourcefile_for_call(write.call_stack.location)
218+
# If we switch to a new file, write changes to the disk for the previous file
219+
if file_path != current_file:
220+
layout.fs.file_write(current_file, content.as_string)
221+
current_file = file_path
222+
content = SourceFile(
223+
current_file.name, layout.fs.file_read(current_file)
224+
)
225+
delta_line_numbers = 0
226+
227+
# Calculate the line number taking into account changes that shifted line numbers
228+
line = write.call_stack.location.line + delta_line_numbers
229+
if isinstance(write.snapshot.format, LiteralTodoStub):
230+
content.replace_on_line(line, ".${kind.name}_TODO(", ".${kind.name}(")
231+
else:
232+
to_be_literal = content.parse_to_be_like(line)
233+
# Attempt to set the literal value and adjust for line shifts due to content changes
234+
literal_change = to_be_literal.set_literal_and_get_newline_delta(
235+
write.snapshot
236+
)
237+
delta_line_numbers += literal_change
238+
239+
# Final write to disk for the last file processed
240+
layout.fs.file_write(current_file, content.as_string)

python/selfie-lib/tests/Slice_test.py

+7
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,10 @@ def test_unixLine():
1212
assert str(one_two_three.unixLine(4)) == ""
1313
assert str(one_two_three.unixLine(5)) == "FOURTH"
1414
assert str(one_two_three.unixLine(6)) == ""
15+
16+
17+
def test_replace_self():
18+
undertest = Slice("ABC")
19+
justB = undertest.subSequence(1, 2)
20+
assert str(justB) == "B"
21+
assert justB.replaceSelfWith("D") == "ADC"

0 commit comments

Comments
 (0)