From 7f91138a63ffc82b56b4111541567459d5a3d01f Mon Sep 17 00:00:00 2001 From: Isaiah Odhner Date: Tue, 12 Sep 2023 18:38:08 -0400 Subject: [PATCH] Use pyfakefs for file dialog tests - The app's directory structure is not a constant, and shouldn't play into this test. Aside from codebase restructuring, directories like `__pycache__` can come and go. - Even if a temporary directory were created with files enough to fill the view, the scrollbar would still change based on the folder structure outside of the temporary folder. - pyfakefs is one way to ensure a consistent view of a folder structure for testing. It allows adding real folders in a readonly way. It's more complicated than I thought it would be going in, since I had to add workarounds for pyfiglet and pytest-textual-snapshot, and handle an edge case in my EnhancedDirectoryTree (which got an error which seems to be swallowed?), not to mention pyfakefs raises an error saying "No such file or directory in the fake filesystem" when actually it's the real directory not existing when trying to add it to the fake filesystem, and VS Code was hiding stack frames and refusing to step into library code, and it turned out that I was resolving the absolute path wrong, but it looked right to me because the only part that was missing was "textual-paint", when, at a glance it seemed present, since the "textual_paint" part was present. Ay-ay-ay! - I don't know if this will fix the problem I saw where these tests' snapshots all changed with no visual or even structural changes, just the IDs of elements changing. I don't know what caused that. Oh yeah and this is still actually a problem: ============================================= short test summary info ============================================== FAILED tests/test_snapshots.py::test_paint_open_dialog[light_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[dark_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[light_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[dark_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[light_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[dark_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[light_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[dark_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? ========================== 8 failed, 56 passed, 1 xfailed, 1 warning in 152.86s (0:02:32) ========================== It worked when running in debug, but not when running normally. --- cspell.json | 1 + requirements.txt | 1 + src/textual_paint/enhanced_directory_tree.py | 6 ++- tests/test_snapshots.py | 43 ++++++++++++++++++-- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/cspell.json b/cspell.json index c271670..1b5f95d 100644 --- a/cspell.json +++ b/cspell.json @@ -118,6 +118,7 @@ "psutil", "pybabel", "pycache", + "pyfakefs", "pyfiglet", "Pylance", "pyobjc", diff --git a/requirements.txt b/requirements.txt index d0fad9c..6af68b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,3 +14,4 @@ types-Pillow==10.0.0.1 # for development types-psutil==5.9.5.15 # for development pytest==7.4.1 # for development pytest-textual-snapshot==0.4.0 # for development +pyfakefs==5.2.4 # for development diff --git a/src/textual_paint/enhanced_directory_tree.py b/src/textual_paint/enhanced_directory_tree.py index b9dbaf8..0e7689a 100644 --- a/src/textual_paint/enhanced_directory_tree.py +++ b/src/textual_paint/enhanced_directory_tree.py @@ -163,7 +163,11 @@ class EnhancedDirectoryTree(DirectoryTree): if callback is None: callback = lambda: None - self._expand_matching_child(self.root, target_path.parts[1:], callback) + + remaining_parts = target_path.parts[1:] + # True unless target_path is the root (edge case which occurs when testing with pyfakefs). + if len(remaining_parts) > 1: + self._expand_matching_child(self.root, remaining_parts, callback) def render_label( self, node: TreeNode[DirEntry], base_style: Style, style: Style diff --git a/tests/test_snapshots.py b/tests/test_snapshots.py index 33f808c..8b4a957 100644 --- a/tests/test_snapshots.py +++ b/tests/test_snapshots.py @@ -1,12 +1,17 @@ from pathlib import Path, PurePath -from typing import TYPE_CHECKING, Awaitable, Callable, Iterable, Protocol +from typing import (TYPE_CHECKING, Awaitable, Callable, Generator, Iterable, + Protocol) +import pyfakefs import pytest +from pyfakefs.fake_file import FakeDirectory +from pyfakefs.fake_filesystem import FakeFilesystem from textual.geometry import Offset from textual.pilot import Pilot from textual.widgets import Input from tests.pilot_helpers import click_by_attr, click_by_index, drag +from textual_paint.figlet_font_writer import FIGletFontWriter if TYPE_CHECKING: # When tests are run, paint.py is re-evaluated, @@ -58,6 +63,38 @@ def each_theme(request: pytest.FixtureRequest): args.theme = "light" args.ascii_only = False +APPS_DIR_ABSOLUTE = (Path(__file__).parent / APPS_DIR).resolve() +TESTS_DIR_ABSOLUTE = Path(__file__).parent.resolve() + +@pytest.fixture +def my_fs(fs: FakeFilesystem) -> Generator[FakeFilesystem, None, None]: + print("adding real directory", APPS_DIR_ABSOLUTE) + fs.add_real_directory(APPS_DIR_ABSOLUTE) + + # Without this, pytest-textual-snapshot will show "No history for this test" + print("adding real directory", TESTS_DIR_ABSOLUTE) + fs.add_real_directory(TESTS_DIR_ABSOLUTE) + + # TODO: use proper mocking or figure out how to get FigletFont to find the real font files. + # This folder doesn't actually exist on my system, so it's not getting them from there. + # from pyfiglet import SHARED_DIRECTORY + # fs.add_real_directory(SHARED_DIRECTORY) + + # Don't fail trying to load the default font "standard", we don't need it! + # `pkg_resources` doesn't seem to work with pyfakefs. + from pyfiglet import FigletFont + def preloadFont(self: FigletFont, font: str): + dumb_font = FIGletFontWriter(commentLines=["Stupid font for testing"]) + for ordinal in dumb_font.charOrder: + dumb_font.figChars[ordinal] = "fallback font for testing" + return dumb_font.createFigFileData() + FigletFont.preloadFont = preloadFont + + # Add an extra file to show how a file looks in the EnhancedDirectoryTree widget. + fs.create_file("/pyfakefs_added_file.txt", contents="pyfakefs ate ur FS") + + yield fs + def test_paint_app(snap_compare: SnapCompareType, each_theme: None): assert snap_compare(PAINT, terminal_size=LARGER) @@ -71,10 +108,10 @@ def test_paint_flip_rotate_dialog(snap_compare: SnapCompareType, each_theme: Non def test_paint_image_attributes_dialog(snap_compare: SnapCompareType, each_theme: None): assert snap_compare(PAINT, press=["ctrl+e"]) -def test_paint_open_dialog(snap_compare: SnapCompareType, each_theme: None): +def test_paint_open_dialog(snap_compare: SnapCompareType, each_theme: None, my_fs: None): assert snap_compare(PAINT, press=["ctrl+o"], terminal_size=LARGER) -def test_paint_save_dialog(snap_compare: SnapCompareType, each_theme: None): +def test_paint_save_dialog(snap_compare: SnapCompareType, each_theme: None, my_fs: None): assert snap_compare(PAINT, press=["ctrl+s"], terminal_size=LARGER) def test_paint_help_dialog(snap_compare: SnapCompareType, each_theme: None):