Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds a feature to control how directories created by the tmp_path fixture are kept. #10442

Merged
merged 24 commits into from
Nov 15, 2022

Conversation

ykadowak
Copy link
Contributor

closes #8141

I recognize that the tests are insufficient but I couldn't find a good reference. Is there any integration tests that actually make sure only 3 directories remain after executing a test that uses tmp_dir fixture for multiple times? I assume no and it's probably because it's difficult to deal with atexit.register?

@ykadowak ykadowak changed the title Tmp dir configure Adds feature to control how directories created by the tmp_path fixture are kept. Oct 27, 2022
@ykadowak ykadowak changed the title Adds feature to control how directories created by the tmp_path fixture are kept. Adds a feature to control how directories created by the tmp_path fixture are kept. Oct 27, 2022
Copy link
Member

@nicoddemus nicoddemus left a comment

Choose a reason for hiding this comment

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

Thanks @yusuke-kadowaki for the contribution.

We also need to update the tmpdir.rst docs with the new options, and mention the configuration variables (as I requested them to be changed from command-line options to conf variables) in reference.rst.

@@ -228,6 +228,23 @@ def pytest_addoption(parser: Parser) -> None:
),
)

group.addoption(
Copy link
Member

Choose a reason for hiding this comment

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

Let's make those configuration variables instead, seems like something users will configure once instead of passing them in the command-line all the time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.
I was confused with command line options and configuration options.

@@ -31,10 +35,14 @@ class TempPathFactory:
_given_basetemp = attr.ib(type=Optional[Path])
_trace = attr.ib()
_basetemp = attr.ib(type=Optional[Path])
_retention_count = attr.ib(type=int)
_retention_policy = attr.ib(type=str)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
_retention_policy = attr.ib(type=str)
_retention_policy = attr.ib(type=Literal["none", "failed", "all"])

We should probably make a TypeAlias for that and reuse it everywhere it is declared.

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 made this.

RetentionType = Literal["all", "failed", "none"]

rmtree(passed_dir)

# Register to remove the base directory before starting cleanup_numbered_dir
atexit.register(cleanup_passed_dir, tmp_path_factory._basetemp)
Copy link
Member

Choose a reason for hiding this comment

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

I would prefer to explicitly remove them at this point instead of relying on atexit.

pytest might be embedded, for example as part of user script or IDE, which might not actually exit the interpreter for a awhile.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.
Is there any reason for using atexit to execute cleanup_numbered_dir in the first place? That makes it really difficult to test the behavior.

@@ -0,0 +1 @@
Added `--tmp-path-retention-count` and `--tmp-path-retention-policy` options to control how directories created by the `tmp_path` fixture are kept.
Copy link
Member

Choose a reason for hiding this comment

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

We should also mention explicitly that the default has changed to remove all directories if all tests passed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done
ce71d7b

tmp_path_factory: TempPathFactory = session.config._tmp_path_factory
if tmp_path_factory._basetemp is None:
return
policy = tmp_path_factory._retention_policy
Copy link
Member

Choose a reason for hiding this comment

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

I was under the impression that the failed policy would remove all passed tests, even if some tests failed.

Here it will remove all directories only if all tests pass.

Or is that handled somewhere else that I am missing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here it will remove all directories only if all tests pass.

This is actually how I implemented it. And I added some implementation to match the behavior you mentioned.
0abbf8c

@ykadowak
Copy link
Contributor Author

Only the Windows tests are failing but I'm not familiar with Windows so I need some help.
Seems like monkeypatch holds the tmpdir when we try to rmtree it (and it only happens in Windows 🤔?).

My first guess is we need to stick to atexit but again I'm noob on Windows so I want someone else's opinion.

Copy link
Member

@nicoddemus nicoddemus left a comment

Choose a reason for hiding this comment

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

Thanks @yusuke-kadowaki!

Left a few comments.

@RonnyPfannschmidt would also appreciate your review given your familiarity with this code.

@@ -0,0 +1,2 @@
Added `--tmp-path-retention-count` and `--tmp-path-retention-policy` options to control how directories created by the `tmp_path` fixture are kept.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Added `--tmp-path-retention-count` and `--tmp-path-retention-policy` options to control how directories created by the `tmp_path` fixture are kept.
Added :confval:`tmp_path_retention_count` and :confval:`tmp_path_retention_policy` configuration options to control how directories created by the :fixture:`tmp_path` fixture are kept.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done with c7eb5a3.

@@ -0,0 +1,2 @@
Added `--tmp-path-retention-count` and `--tmp-path-retention-policy` options to control how directories created by the `tmp_path` fixture are kept.
The default behavior has changed to remove all directories if all tests passed.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
The default behavior has changed to remove all directories if all tests passed.
The default behavior has changed to keep only directories for failed tests, equivalent to `tmp_path_retention_policy="failed"`.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done with c7eb5a3.

tmp_path_factory: TempPathFactory = request.session.config._tmp_path_factory # type: ignore
policy = tmp_path_factory._retention_policy
if policy == "failed" and request.node.result_call.passed:
rmtree(path)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
rmtree(path)
# We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
# permissions, etc, in which case we ignore it.
rmtree(path, ignore_errors=True)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done with 94740c0.

def pytest_runtest_makereport(item, call):
outcome = yield
result = outcome.get_result()
setattr(item, "result_" + result.when, result)
Copy link
Member

Choose a reason for hiding this comment

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

Let us make this private to not encourage users and plugins to depend on it.

Suggested change
setattr(item, "result_" + result.when, result)
setattr(item, "_tmp_path_result_" + result.when, result)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done with dc11283.

# Register to remove the base directory before starting cleanup_numbered_dir
passed_dir = tmp_path_factory._basetemp
if passed_dir.exists():
rmtree(passed_dir)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
rmtree(passed_dir)
# We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
# permissions, etc, in which case we ignore it.
rmtree(path, ignore_errors=True)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done with 94740c0.

elif name == "tmp_path_retention_policy":
return "failed"
else:
assert False
Copy link
Member

Choose a reason for hiding this comment

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

👍

basetemp = tmp_path_factory._basetemp
if basetemp is None:
return
for left_dir in basetemp.iterdir():
Copy link
Member

Choose a reason for hiding this comment

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

Lets move this block of code to a function in pathlib.py and reuse it in cleanup_numbered_dir. 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done with b04bbb1.

pytester.inline_run(p)
root = pytester._test_tmproot
for child in root.iterdir():
base_dir = filter(lambda x: not x.is_symlink(), child.iterdir())
Copy link
Member

Choose a reason for hiding this comment

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

Don't we want to ensure the symlinks are removed too?

Suggested change
base_dir = filter(lambda x: not x.is_symlink(), child.iterdir())
base_dir = list(child.iterdir())

Copy link
Contributor Author

@ykadowak ykadowak Nov 3, 2022

Choose a reason for hiding this comment

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

This symlink will be deleted by cleanup_numbered_dir after the test finishes because it's triggered by atexit. So you cannot really test it here.
Added comments about it here instead.

f853117

@ykadowak ykadowak requested review from nicoddemus and removed request for RonnyPfannschmidt November 3, 2022 12:11
@ykadowak
Copy link
Contributor Author

ykadowak commented Nov 3, 2022

Sorry I accidentally removed @RonnyPfannschmidt from reviewers and cannot get it back.

@nicoddemus
Copy link
Member

Thanks @yusuke-kadowaki, great work!

@ykadowak
Copy link
Contributor Author

ykadowak commented Nov 7, 2022

@RonnyPfannschmidt
Could you take a look at this please?

@ykadowak
Copy link
Contributor Author

@nicoddemus
What should we do with this?

@nicoddemus
Copy link
Member

Let's go ahead and merge this, @RonnyPfannschmidt can always take a look later and we can do a follow up if needed. 👍

@nicoddemus nicoddemus merged commit cca029d into pytest-dev:main Nov 15, 2022
@nicoddemus
Copy link
Member

Thanks @yusuke-kadowaki!

@RonnyPfannschmidt
Copy link
Member

thanks for landing this, this pr is good, i just dont have bandwidth for deep reviews for family reasons

@ykadowak
Copy link
Contributor Author

Thank you for the reviews and also maintaining this great library!
Just lemme know when something needs to be updated. I'm here to work on it😉

def pytest_runtest_makereport(item, call):
outcome = yield
result = outcome.get_result()
setattr(item, "_tmp_path_result_" + result.when, result)
Copy link
Member

Choose a reason for hiding this comment

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

Those aren't guaranteed to exist if a phase wasn't run, see #10502.

But also, didn't we introduce the .stash stuff to avoid having to monkeypatch random attributes on items? It seems like we should use that here, no? Search for e.g. caplog_records_key in the codebase to see how this is done elsewhere, including storing things based on when.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@The-Compiler @nicoddemus

Made a fix for this #10517.

Copy link
Member

Choose a reason for hiding this comment

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

But also, didn't we introduce the .stash stuff to avoid having to monkeypatch random attributes on items? It seems like we should use that here, no?

Thanks for catching this @The-Compiler, completely missed that on my review. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Keep temporary directory for failing tests only
4 participants