Skip to content

Conversation

@zfbx
Copy link
Contributor

@zfbx zfbx commented Apr 1, 2025

Summary

Replaced dev/null path with a universal temp_dir function that persists through the test session and cleans itself up afterwards to function identically but runs on windows as well as others? I don't have another platform to test this on so it will need to be tested.

Sorry, it was bugging me I wanted tests to pass xD

image

Tasks Completed

  • Platforms Tested:
    • Windows x86
    • Windows ARM
    • macOS x86
    • macOS ARM
    • Linux x86
    • Linux ARM
  • Tested For:
    • Basic functionality
    • PyInstaller executable

@CyanVoxel CyanVoxel added Type: Bug Something isn't working as intended Type: Tests Tests or testing related System: Windows For Microsoft Windows labels Apr 1, 2025
@CyanVoxel CyanVoxel moved this to 👀 In review in TagStudio Development Apr 1, 2025
@CyanVoxel CyanVoxel linked an issue Apr 1, 2025 that may be closed by this pull request
3 tasks
zfbx and others added 3 commits April 1, 2025 02:18
Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
@Computerdores Computerdores self-requested a review April 1, 2025 09:07
Copy link
Collaborator

@Computerdores Computerdores left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't been able to test it yet, but it seems to be good apart from what I commented on.

Comment on lines 54 to 62
@pytest.fixture(scope="session")
def temp_dir():
"""Creates a shared library path for tests, and cleans it up after the session."""
temp_root = Path(tempfile.gettempdir())
test_dir = temp_root / "ts-test-temp"

test_dir.mkdir(parents=True, exist_ok=True)

return test_dir
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason you are not using tempfile.NamedTemporaryDirectory here? Also, if you change to that, it might make more sense to create a new one in each test in order to isolate the different tests from each other (I would in that case specifically suggest making use of it's ability to be used with with)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find any NamedTemporaryDirectory, only NamedTemporaryFile. I actually preferred when it would clean up after itself rather than leaving directories behind but I'm not sure what's proper in this sort of case. I've not had much experience in testing, I just wanted to get them to pass on windows 😅 I'm open to changes suggested if you think it would be better

Copy link
Collaborator

@Computerdores Computerdores Apr 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad the class is just called TemporaryDirectory and, I think, you can access its Path via the property name, see https://docs.python.org/3/library/tempfile.html#tempfile.TemporaryDirectory

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module creates temporary files and directories. It works on all supported platforms. TemporaryFile, NamedTemporaryFile, TemporaryDirectory, and SpooledTemporaryFile are high-level interfaces which provide automatic cleanup and can be used as context managers. mkstemp() and mkdtemp() are lower-level functions which require manual cleanup.

TemporaryDirectory seems to be an easy solution that cleans up after itself, and I agree that it might make more sense to use this in each relevant test rather than pass the same value around

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use TemporaryDirectory and tested working.

Copy link
Member

@CyanVoxel CyanVoxel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got a few more suggestions to look into after the most recent changes, some affecting pre-existing code that was seemingly trying to do what you're currently trying to do now.

I also caught an old note of mine inside the CacheManager that described special behavior that was in direct response to the "dev/null" assumptions in these tests up until now. This note and check can now be removed (cache_manager.py):

- # NOTE: The /dev/null check is a workaround for current test assumptions.
- if self.last_lib_path and self.last_lib_path != Path("/dev/null"):
+ if self.last_lib_path and self.last_lib_path:

Comment on lines 63 to 77
def library(request, temp_dir: Path):
# when no param is passed, use the default
library_path = "/dev/null/"
library_path = str(temp_dir)
if hasattr(request, "param"):
if isinstance(request.param, TemporaryDirectory):
library_path = request.param.name
else:
library_path = request.param

thumbs_path = Path(library_path) / ".TagStudio" / "thumbs"
thumbs_path.mkdir(parents=True, exist_ok=True)

lib = Library()
status = lib.open_library(Path(library_path), ":memory:")
assert status.success
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So due to the high number of casts back and forth between str and Path for this temp directory I went and looked into what this whole hasattr block is actually doing. Turns out it's also using TemporaryDirectory but in a much more confusing and roundabout way. I've been able to greatly simplify this section of the code by removing it and using the temp_dir parameter directly - here I renamed it to library_dir:

@pytest.fixture
def library(library_dir: Path):
    thumbs_path = library_dir / ".TagStudio" / "thumbs"
    thumbs_path.mkdir(parents=True, exist_ok=True)

    lib = Library()
    status = lib.open_library(Path(library_dir), ":memory:")
    assert status.success

The single test that used this was test_refresh_new_files inside test_refresh_dir.py, which would now need the (simplified) parameters:

@pytest.mark.parametrize("exclude_mode", [True, False])
def test_refresh_new_files(library: Library, exclude_mode):

Finally if you did also decide to rename temp_dir to library_dir as I did, it would also need to be changed inside test_file_path_options.py. Honestly though it's a minor thing and I'd just like to see this fix be pulled before it stirs up more skeletons 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will see what I can do here 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looked into what this whole hasattr block is actually doing. Turns out it's also using TemporaryDirectory but in a much more confusing and roundabout way. I've been able to greatly simplify this section of the code by removing it and using the temp_dir parameter directly - here I renamed it to library_dir:

Okay, I see what's going on, doing this was because of this on test_refresh_dir:test_refresh_new_files() needed a unique directory for it's test and it goes through library() to do so.

New changes up and pass on Windows x86 and Mac x86 (technically x64?)

return lib


@pytest.fixture(scope="session")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the difference between the default scope vs the session scope here in practice, as they both seem to generate different temp directories when this fixture is called.

The only tangible effect I can observe is having (scope="session") causes the tests in test_refresh_new_files() to fail if using my suggested removal of the strange pre-existing "hasattr" code...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this my understanding of it is that when you use the scope="session" fixture it runs the code inside it once and then any further calls to it return the same temporary directory path so it can be reused across multiple tests and then when the session is complete it runs cleanup and deletes the directory which is why the yield is used instead of return because return causes the program to end before TempDir does it's cleanup leaving the files in the temp directory.

Which leads to the other comment, Yeah, I did notice you had more uses of TemporaryDirectory in other test but I wasn't sure if there was a reason for it maybe needing fresh directories for different tests. I only built it this way because it seemed test_file_path_options:test_title_update() depended on the path being exactly the same as used with conftest:library() which is probably why you had it the way you did.

I hope any of this made sense cause I'm only like 80% following my own words here myself

Copy link
Collaborator

@Computerdores Computerdores left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes look good!
What I noticed though is that the json migration test fails for me and I can't properly check why because I keep getting this error (this was already present before the tests broke on windows):

d:\Coding\TagStudio\tests\test_json_migration.py::test_json_migration failed with error:  
 The python test process was terminated before it could exit on its own, the process errored with: Code: 3221226505, Signal: null

And the only way to circumvent that is to run all tests at once, really weird.
Only thing I can tell atm is that check_field_parity is what fails

@Computerdores
Copy link
Collaborator

The changes look good! What I noticed though is that the json migration test fails for me and I can't properly check why because I keep getting this error (this was already present before the tests broke on windows):

d:\Coding\TagStudio\tests\test_json_migration.py::test_json_migration failed with error:  
 The python test process was terminated before it could exit on its own, the process errored with: Code: 3221226505, Signal: null

And the only way to circumvent that is to run all tests at once, really weird. Only thing I can tell atm is that check_field_parity is what fails

Ok I also got this error:
image
And running the tests twice in the terminal had the migration tests fail in only one of the runs... I think this might just be weirdness of my setup.

Copy link
Member

@CyanVoxel CyanVoxel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay on getting back to this - Everything seems to function on my end (macOS + Windows) and it looks like things are ready to pull.
Thank you for your work on this!

@CyanVoxel CyanVoxel moved this from 👀 In review to 🍃 Pending Merge in TagStudio Development May 5, 2025
@CyanVoxel CyanVoxel merged commit 36f3b45 into TagStudioDev:main May 5, 2025
5 checks passed
@github-project-automation github-project-automation bot moved this from 🍃 Pending Merge to ✅ Done in TagStudio Development May 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

System: Windows For Microsoft Windows Type: Bug Something isn't working as intended Type: Tests Tests or testing related

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

[Bug]: Tests failing on windows

3 participants