12rambau / pytest-copie

The pytest plugin for your copier templates 📒
https://pytest-copie.readthedocs.io/en/latest/
MIT License
14 stars 2 forks source link

Graceful error handling in pytest-copie teardown process #55

Closed GenevieveBuckley closed 8 months ago

GenevieveBuckley commented 8 months ago

Describe the bug Over at the napari-plugin-template repo we've been struggling to make our CI pass with pytest-copie.

I've found that if there are any errors in the pytest-copie teardown process here, then pytest reports errors, failing the build. Would it be possible to either set shutil.rmtree(test_dir, ignore_errors=True), or have ignoring errors at this point be a user configuration option we can set somehow?

Additional details The reason shutil.rmtree is producing errors for us is that there are some write protected files being generated. We don't really care if the teardown process leaves some files intact - for us it is much more important that the github CI runner shows us whether the tests pass or fail.

(We are ending up with write protected files because our template is running a task to run git init and git add + git commit on the newly created repository. Git is producing some write protected files (weirdly, this is only a problem for us on Windows) and I haven't been able to figure out if there's a way to modify the permissions. There seems like it should be possible to do, but the immutability of git commits is making things difficult)

To Reproduce

I have a minimal, reproducible example on the tox-git-teardown-problem branch of this repository: https://github.com/GenevieveBuckley/demo_template/tree/tox-git-teardown-problem

(pytest-copie-dev) C:\Users\admin\Documents\GitHub\demo_template>pytest
================================================= test session starts =================================================
platform win32 -- Python 3.10.13, pytest-7.4.3, pluggy-1.3.0
rootdir: C:\Users\admin\Documents\GitHub\demo_template
plugins: copie-0.1.2
collected 1 item

tests\test_template.py .E                                                                                        [100%]

======================================================= ERRORS ========================================================
_________________________________________ ERROR at teardown of test_template __________________________________________

request = <SubRequest 'copie' for <Function test_template>>
tmp_path = WindowsPath('C:/Users/admin/AppData/Local/Temp/pytest-of-admin/pytest-19/test_template0')
_copier_config_file = WindowsPath('C:/Users/admin/AppData/Local/Temp/pytest-of-admin/pytest-19/user_dir0/config')

    @pytest.fixture
    def copie(request, tmp_path: Path, _copier_config_file: Path) -> Generator:
        """Yield an instance of the :py:class:`Copie <pytest_copie.plugin.Copie>` helper class.

        The class can then be used to generate a project from a template.

        Args:
            request: the pytest request object
            tmp_path: the temporary directory
            _copier_config_file: the temporary copier config file

        Returns:
            the object instance, ready to copy !
        """
        # extract the template directory from the pytest command parameter
        template_dir = Path(request.config.option.template)

        # set up a test directory in the tmp folder
        (test_dir := tmp_path / "copie").mkdir()

        yield Copie(template_dir, test_dir, _copier_config_file)

        # don't delete the files at the end of the test if requested
        if not request.config.option.keep_copied_projects:
>           rmtree(test_dir)

..\..\..\miniforge3\envs\pytest-copie-dev\lib\site-packages\pytest_copie\plugin.py:144:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
..\..\..\miniforge3\envs\pytest-copie-dev\lib\shutil.py:750: in rmtree
    return _rmtree_unsafe(path, onerror)
..\..\..\miniforge3\envs\pytest-copie-dev\lib\shutil.py:615: in _rmtree_unsafe
    _rmtree_unsafe(fullname, onerror)
..\..\..\miniforge3\envs\pytest-copie-dev\lib\shutil.py:615: in _rmtree_unsafe
    _rmtree_unsafe(fullname, onerror)
..\..\..\miniforge3\envs\pytest-copie-dev\lib\shutil.py:615: in _rmtree_unsafe
    _rmtree_unsafe(fullname, onerror)
..\..\..\miniforge3\envs\pytest-copie-dev\lib\shutil.py:615: in _rmtree_unsafe
    _rmtree_unsafe(fullname, onerror)
..\..\..\miniforge3\envs\pytest-copie-dev\lib\shutil.py:620: in _rmtree_unsafe
    onerror(os.unlink, fullname, sys.exc_info())
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

path = 'C:\\Users\\admin\\AppData\\Local\\Temp\\pytest-of-admin\\pytest-19\\test_template0\\copie\\copie000\\.git\\objects\\07'
onerror = <function rmtree.<locals>.onerror at 0x0000021E17614C10>

    def _rmtree_unsafe(path, onerror):
        try:
            with os.scandir(path) as scandir_it:
                entries = list(scandir_it)
        except OSError:
            onerror(os.scandir, path, sys.exc_info())
            entries = []
        for entry in entries:
            fullname = entry.path
            if _rmtree_isdir(entry):
                try:
                    if entry.is_symlink():
                        # This can only happen if someone replaces
                        # a directory with a symlink after the call to
                        # os.scandir or entry.is_dir above.
                        raise OSError("Cannot call rmtree on a symbolic link")
                except OSError:
                    onerror(os.path.islink, fullname, sys.exc_info())
                    continue
                _rmtree_unsafe(fullname, onerror)
            else:
                try:
>                   os.unlink(fullname)
E                   PermissionError: [WinError 5] Access is denied: 'C:\\Users\\admin\\AppData\\Local\\Temp\\pytest-of-admin\\pytest-19\\test_template0\\copie\\copie000\\.git\\objects\\07\\ca5c6677c34cf78b9dd7bd3a21dba891051fc5'

..\..\..\miniforge3\envs\pytest-copie-dev\lib\shutil.py:618: PermissionError
------------------------------------------------ Captured stdout call -------------------------------------------------

Initialized empty Git repository in C:/Users/admin/AppData/Local/Temp/pytest-of-admin/pytest-19/test_template0/copie/copie000/.git/

------------------------------------------------ Captured stderr call -------------------------------------------------
No git tags found in template; using HEAD as ref

Copying from template version 0.0.0.post3.dev0+ed582b2
 identical  .
    create  README.rst
 > Running task 1 of 1: ['C:\\Users\\admin\\miniforge3\\envs\\pytest-copie-dev\\python.exe', 'C:\\Users\\admin\\AppData\\Local\\Temp\\copier.vcs.clone.8zjlalsf\\_tasks.py']
Switched to a new branch 'main'
warning: in the working copy of 'README.rst', LF will be replaced by CRLF the next time Git touches it
=============================================== short test summary info ===============================================
ERROR tests/test_template.py::test_template - PermissionError: [WinError 5] Access is denied: 'C:\\Users\\admin\\AppData\\Local\\Temp\\pytest-of-admin\\pytest-19...
============================================= 1 passed, 1 error in 2.83s ==============================================