2from collections
import defaultdict
3from collections.abc
import ByteString, Iterator
4from typing
import Optional
7from selfie_lib
import (
32from .SelfieSettingsAPI
import SelfieSettingsAPI
36 def assert_failed(self, message, expected=None, actual=None) -> Exception:
37 if expected
is None and actual
is None:
38 return AssertionError(message)
43 if not expected_str
and not actual_str
and (expected
is None or actual
is None):
53 def __nullable_to_string(self, value, on_null: str) -> str:
54 return str(value)
if value
is not None else on_null
56 def __comparison_assertion(
57 self, message: str, expected: str, actual: str
60 assert expected == actual, message
62 return AssertionError(message)
66 def __init__(self, fs: FSImplementation, settings: SelfieSettingsAPI):
69 self.
__root_folder = TypedPath.of_folder(os.path.abspath(settings.root_dir))
76 if testfile.name.endswith(
".py"):
77 return testfile.parent_folder().resolve_file(f
"{testfile.name[:-3]}.ss")
79 raise ValueError(f
"Unknown file extension, expected .py: {testfile.name}")
81 def __infer_default_line_ending_is_unix(self) -> bool:
82 def walk_callback(walk: Iterator[TypedPath]) -> bool:
83 for file_path
in walk:
85 txt = self.
fs.file_read(file_path)
88 return "\r" not in txt
89 except UnicodeDecodeError:
99 session: pytest.Session, config: pytest.Config, items: list[pytest.Item]
103 session.selfie_system = system
104 _initSelfieSystem(system)
106 (file, _, testname) = item.reportinfo()
107 system.planning_to_run(TypedPath.of_file(os.path.abspath(file)), testname)
112 system: PytestSnapshotSystem = session.selfie_system
113 system.finished_all_tests()
114 _clearSelfieSystem(system)
117@pytest.hookimpl(hookwrapper=True)
119 (file, _, testname) = item.reportinfo()
120 testfile = TypedPath.of_file(os.path.abspath(file))
122 system: PytestSnapshotSystem = item.session.selfie_system
123 system.test_start(testfile, testname)
125 system.test_finish(testfile, testname)
130 if call.excinfo
is not None and call.when
in (
134 system: PytestSnapshotSystem = item.session.selfie_system
135 (file, _, testname) = item.reportinfo()
136 system.test_failed(TypedPath.of_file(os.path.abspath(file)), testname)
140 """A special defaultdict that passes the key to the default_factory."""
143 if self.default_factory
is None:
146 ret = self[key] = self.default_factory(key)
153 self.
__mode = settings.calc_mode()
159 self.__progress_per_file: defaultdict[TypedPath, SnapshotFileProgress] = (
165 self.check_for_invalid_state: AtomicReference[Optional[ArraySet[TypedPath]]] = (
170 progress = self.__progress_per_file[testfile]
171 progress.finishes_expected += 1
174 def update_fun(arg: Optional[ArraySet[TypedPath]]):
177 "Snapshot file is being written after all tests were finished."
179 return arg.plusOrThis(path)
181 self.check_for_invalid_state.update_and_get(update_fun)
186 f
"Test already in progress. {self.__in_progress.test_file} is running, can't start {testfile}"
200 def __assert_inprogress(self, testfile: TypedPath):
202 raise RuntimeError(
"No test in progress")
205 f
"{self.__in_progress.test_file} is in progress, can't accept data for {testfile}."
209 snapshotsFilesWrittenToDisk = self.check_for_invalid_state.get_and_update(
212 if snapshotsFilesWrittenToDisk
is None:
213 raise RuntimeError(
"finished_all_tests() was called more than once.")
221 source.remove_selfie_once_comments()
222 self.
fsfs.file_write(path, source.as_string)
241 raise RuntimeError(
"No test in progress")
253 self, path: TypedPath, data:
"ByteString", call: CallStack
262 def __init__(self, progress:
"SnapshotFileProgress", testname: str):
266 def read_disk(self, sub: str, call:
"CallStack") -> Optional[
"Snapshot"]:
269 def write_disk(self, actual:
"Snapshot", sub: str, call:
"CallStack"):
278 def keep(self, sub_or_keep_all: Optional[str]):
284 return f
"/{sub}" if sub
else ""
288 TERMINATED = ArrayMap.empty().plus(
" ~ / f!n1shed / ~ ",
WithinTestGC())
290 def __init__(self, system: PytestSnapshotSystem, test_file: TypedPath):
303 self.
file: Optional[SnapshotFile] =
None
304 self.tests: AtomicReference[ArrayMap[str, WithinTestGC]] =
AtomicReference(
313 if self.tests.get() == SnapshotFileProgress.TERMINATED:
315 "Cannot call methods on a terminated SnapshotFileProgress"
320 raise ValueError(f
"Test name cannot contain '/', was {testname}")
324 f
"Cannot start a new test {testname}, {self.testname_in_progress} is already in progress"
327 self.tests.update_and_get(
lambda it: it.plus_or_noop(testname,
WithinTestGC()))
332 self.tests.get()[testname].keep_all()
341 def __assert_in_progress(self, testname: str):
344 raise RuntimeError(
"Can't finish, no test was in progress!")
347 f
"Can't finish {testname}, {self.testname_in_progress} was in progress"
350 def __all_tests_finished(self):
353 tests = self.tests.get_and_update(
lambda _: SnapshotFileProgress.TERMINATED)
354 if tests == SnapshotFileProgress.TERMINATED:
355 raise ValueError(f
"Snapshot for {self.test_file} already terminated!")
356 if self.
file is not None:
357 stale_snapshot_indices = []
360 if stale_snapshot_indices
or self.
file.was_set_at_test_time:
361 self.
file.remove_all_indices(stale_snapshot_indices)
362 snapshot_path = self.
system.layout_pytest.snapshotfile_for_testfile(
365 if not self.
file.snapshots:
368 self.
system.mark_path_as_written(
369 self.
system.layout_pytest.snapshotfile_for_testfile(
374 os.path.dirname(snapshot_path.absolute_path), exist_ok=
True
377 snapshot_path.absolute_path,
"w", encoding=
"utf-8"
380 self.
file.serialize(filecontent)
381 for line
in filecontent:
385 every_test_in_class_ran =
not any(
389 every_test_in_class_ran
391 and all(it.succeeded_and_used_no_snapshots()
for it
in tests.values())
394 snapshot_file = self.
system.layout_pytest.snapshotfile_for_testfile(
401 def keep(self, test: str, suffix_or_all: Optional[str]):
403 if suffix_or_all
is None:
404 self.tests.get()[test].keep_all()
406 self.tests.get()[test].keep_suffix(suffix_or_all)
413 call_stack: CallStack,
414 layout: SnapshotFileLayout,
417 key = f
"{test}{suffix}"
419 self.tests.get()[test].keep_suffix(suffix)
420 self.
read_file().set_at_test_time(key, snapshot)
422 def read(self, test: str, suffix: str) -> Optional[Snapshot]:
424 snapshot = self.
read_file().snapshots.get(f
"{test}{suffix}")
425 if snapshot
is not None:
426 self.tests.get()[test].keep_suffix(suffix)
430 if self.
file is None:
431 snapshot_path = self.
system.layout_pytest.snapshotfile_for_testfile(
434 if os.path.exists(snapshot_path.absolute_path)
and os.path.isfile(
435 snapshot_path.absolute_path
437 with open(snapshot_path.absolute_path,
"rb")
as f:
439 self.
file = SnapshotFile.parse(SnapshotValueReader.of_binary(content))
441 self.
file = SnapshotFile.create_empty_with_unix_newlines(
442 self.
system.layout_pytest.unix_newlines
448 if os.path.isfile(snapshot_file.absolute_path):
449 os.remove(snapshot_file.absolute_path)
451 parent = os.path.dirname(snapshot_file.absolute_path)
452 if not os.listdir(parent):
458 tests: ArrayMap[str, WithinTestGC],
459) -> ArrayMap[str, WithinTestGC]:
462 return ArrayMap.empty()
466 group = parser.getgroup(
"selfie")
472 help=
'Set the value for the fixture "bar".',
475 parser.addini(
"HELLO",
"Dummy pytest.ini setting")
480 return request.config.option.dest_foo
keep(self, Optional[str] sub_or_keep_all)
write_disk(self, "Snapshot" actual, str sub, "CallStack" call)
__init__(self, "SnapshotFileProgress" progress, str testname)
Optional["Snapshot"] read_disk(self, str sub, "CallStack" call)
str _suffix(self, str sub)
Exception __comparison_assertion(self, str message, str expected, str actual)
str __nullable_to_string(self, value, str on_null)
Exception assert_failed(self, message, expected=None, actual=None)
TypedPath root_folder(self)
bool __infer_default_line_ending_is_unix(self)
__init__(self, FSImplementation fs, SelfieSettingsAPI settings)
TypedPath snapshotfile_for_testfile(self, TypedPath testfile)
mark_path_as_written(self, TypedPath path)
bool source_file_has_writable_comment(self, CallStack call)
SnapshotFileLayout layout(self)
test_finish(self, TypedPath testfile, str testname)
None write_to_be_file(self, TypedPath path, "ByteString" data, CallStack call)
DiskStorage disk_thread_local(self)
planning_to_run(self, TypedPath testfile, str testname)
__init__(self, SelfieSettingsAPI settings)
write_inline(self, LiteralValue literal_value, CallStack call)
test_start(self, TypedPath testfile, str testname)
__assert_inprogress(self, TypedPath testfile)
test_failed(self, TypedPath testfile, str testname)
SnapshotFile read_file(self)
test_failed(self, str testname)
write(self, str test, str suffix, Snapshot snapshot, CallStack call_stack, SnapshotFileLayout layout)
testname_in_progress_failed
test_finish(self, str testname)
test_start(self, str testname)
keep(self, str test, Optional[str] suffix_or_all)
__init__(self, PytestSnapshotSystem system, TypedPath test_file)
Optional[Snapshot] read(self, str test, str suffix)
__assert_in_progress(self, str testname)
assert_not_terminated(self)
__all_tests_finished(self)
SnapshotFileLayout layout(self)
ArrayMap[str, WithinTestGC] find_test_methods_that_didnt_run(TypedPath testfile, ArrayMap[str, WithinTestGC] tests)
pytest_runtest_makereport(pytest.CallInfo[None] call, pytest.Item item)
delete_file_and_parent_dir_if_empty(TypedPath snapshot_file)
pytest_sessionfinish(pytest.Session session, exitstatus)
pytest_runtest_protocol(pytest.Item item, Optional[pytest.Item] nextitem)
None pytest_collection_modifyitems(pytest.Session session, pytest.Config config, list[pytest.Item] items)