Repository: pypa/setuptools

Issue:
3.4 release fails tests
Originally reported by: **felixonmars (Bitbucket: [felixonmars](http://bitbucket.org/felixonmars), GitHub: [felixonmars](http://github.com/felixonmars))**

---

The first problem I encountered is:

`python setup.py test` just fails before running any tests. Commit related: https://bitbucket.org/pypa/setuptools/commits/affa32a2d30a1b1c0c29983aa21f4f48312b7edc

Traceback:

```
Traceback (most recent call last):
  File "setup.py", line 217, in <module>
    dist = setuptools.setup(**setup_params)
  File "/usr/lib/python3.4/distutils/core.py", line 149, in setup
    dist.run_commands()
  File "/usr/lib/python3.4/distutils/dist.py", line 955, in run_commands
    self.run_command(cmd)
  File "/usr/lib/python3.4/distutils/dist.py", line 974, in run_command
    cmd_obj.run()
  File "setup.py", line 68, in run
    _test.run(self)
  File "/build/python-setuptools/src/setuptools-3.4/setuptools/command/test.py", line 139, in run
    self.with_project_on_sys_path(self.run_tests)
  File "/build/python-setuptools/src/setuptools-3.4/setuptools/command/test.py", line 120, in with_project_on_sys_path
    func()
  File "/build/python-setuptools/src/setuptools-3.4/setuptools/command/test.py", line 161, in run_tests
    runner_ep = EntryPoint.parse("x=" + self.test_runner)
TypeError: Can't convert 'NoneType' object to str implicitly
```

After digging up a little bit, I tried out `-r "unittest:TextTestRunner"` as a workaround. (So I suggest this becomes a default value)

A second problem is: 2 tests fail:

```
======================================================================
ERROR: test_setup_requires (setuptools.tests.test_easy_install.TestUserInstallTest)
Regression test for Distribute issue #318
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/build/python-setuptools/src/setuptools-3.4/setuptools/tests/test_easy_install.py", line 242, in test_setup_requires
    run_setup(test_setup_py, ['install'])
  File "/build/python-setuptools/src/setuptools-3.4/setuptools/sandbox.py", line 50, in run_setup
    lambda: execfile(
  File "/build/python-setuptools/src/setuptools-3.4/setuptools/sandbox.py", line 100, in run
    return func()
  File "/build/python-setuptools/src/setuptools-3.4/setuptools/sandbox.py", line 52, in <lambda>
    {'__file__':setup_script, '__name__':'__main__'}
  File "setup.py", line 2, in <module>
  File "/usr/lib/python2.7/distutils/core.py", line 152, in setup
    dist.run_commands()
  File "/usr/lib/python2.7/distutils/dist.py", line 953, in run_commands
    self.run_command(cmd)
  File "/usr/lib/python2.7/distutils/dist.py", line 972, in run_command
    cmd_obj.run()
  File "/build/python-setuptools/src/setuptools-3.4/setuptools/command/install.py", line 57, in run
    if not self._called_from_setup(inspect.currentframe()):
  File "/build/python-setuptools/src/setuptools-3.4/setuptools/command/install.py", line 81, in _called_from_setup
    res = inspect.getouterframes(run_frame)[2]
  File "/usr/lib/python2.7/inspect.py", line 1033, in getouterframes
    framelist.append((frame,) + getframeinfo(frame, context))
  File "/usr/lib/python2.7/inspect.py", line 1008, in getframeinfo
    lines, lnum = findsource(frame)
  File "/usr/lib/python2.7/inspect.py", line 580, in findsource
    if pat.match(lines[lnum]): break
IndexError: list index out of range

======================================================================
ERROR: test_test (setuptools.tests.test_test.TestTestTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/build/python-setuptools/src/setuptools-3.4/setuptools/tests/test_test.py", line 121, in test_test
    cmd.run()
  File "/build/python-setuptools/src/setuptools-3.4/setuptools/command/test.py", line 139, in run
    self.with_project_on_sys_path(self.run_tests)
  File "/build/python-setuptools/src/setuptools-3.4/setuptools/command/test.py", line 120, in with_project_on_sys_path
    func()
  File "/build/python-setuptools/src/setuptools-3.4/setuptools/command/test.py", line 161, in run_tests
    runner_ep = EntryPoint.parse("x=" + self.test_runner)
TypeError: cannot concatenate 'str' and 'NoneType' objects

----------------------------------------------------------------------
Ran 131 tests in 1.090s

FAILED (errors=2, skipped=7)
```

Python 2.7 and 3.4 fail on the same tests with same traceback. The second test (for test itself) indicates the first problem I mentioned above, and I've no idea about the other...

---
- Bitbucket: https://bitbucket.org/pypa/setuptools/issue/180



Discussion:
_Original comment by_ **jaraco (Bitbucket: [jaraco](http://bitbucket.org/jaraco), GitHub: [jaraco](http://github.com/jaraco))**:

---

Extract the resolution of loader/runner classes. Allows None value to pass-through to unittest.main. Fixes #180.

_Original comment by_ **jaraco (Bitbucket: [jaraco](http://bitbucket.org/jaraco), GitHub: [jaraco](http://github.com/jaraco))**:

---

I've got a fix in place, but I'm hesitant to release it because I'm now also seeing another unrelated test failure.

_Original comment by_ **jaraco (Bitbucket: [jaraco](http://bitbucket.org/jaraco), GitHub: [jaraco](http://github.com/jaraco))**:

---

@felixonmars Yes, I did consider that approach in my tests. I wanted to avoid special-casing the test for that attribute and take advantage of the default value (None) for the parameter. I didn't properly consider the implications of the entry-point-style resolution.

_Original comment by_ **jaraco (Bitbucket: [jaraco](http://bitbucket.org/jaraco), GitHub: [jaraco](http://github.com/jaraco))**:

---

In #181, I've captured the issue with test coverage, and indeed, the reported failure is now apparent in the test suite.

_Original comment by_ **felixonmars (Bitbucket: [felixonmars](http://bitbucket.org/felixonmars), GitHub: [felixonmars](http://github.com/felixonmars))**:

---

An updated patch was posted to the thread where #176 links to: https://mail.python.org/pipermail/distutils-sig/2008-April/009196.html

A quick look to this patch indicates that it has an if for test_runner unspecified case.

_Original comment by_ **jaraco (Bitbucket: [jaraco](http://bitbucket.org/jaraco), GitHub: [jaraco](http://github.com/jaraco))**:

---

There are two issues here. The first is that the travis-run test suite didn't catch the failure. I'm not sure why that is, but it probably relates to the switch to running the tests using pytest.

More importantly, however, is the issue that the implementation now fails. I'll address that promptly.


Resolving patch:
diff --git a/setuptools/_distutils/dist.py b/setuptools/_distutils/dist.py
--- a/setuptools/_distutils/dist.py
+++ b/setuptools/_distutils/dist.py
@@ -334,7 +334,7 @@ def find_config_files(self):
         - a file named by an environment variable
         """
         check_environ()
-        files = [str(path) for path in self._gen_paths() if path.is_file()]
+        files = [str(path) for path in self._gen_paths() if os.path.isfile(path)]
 
         if DEBUG:
             self.announce("using config files: %s" % ', '.join(files))
diff --git a/setuptools/_distutils/sysconfig.py b/setuptools/_distutils/sysconfig.py
--- a/setuptools/_distutils/sysconfig.py
+++ b/setuptools/_distutils/sysconfig.py
@@ -13,6 +13,7 @@
 import re
 import sys
 import sysconfig
+import pathlib
 
 from .errors import DistutilsPlatformError
 from . import py39compat
@@ -40,14 +41,13 @@
         project_base = os.getcwd()
 
 
-# python_build: (Boolean) if true, we're either building Python or
-# building an extension with an un-installed Python, so we use
-# different (hard-wired) directories.
 def _is_python_source_dir(d):
-    for fn in ("Setup", "Setup.local"):
-        if os.path.isfile(os.path.join(d, "Modules", fn)):
-            return True
-    return False
+    """
+    Return True if the target directory appears to point to an
+    un-installed Python.
+    """
+    modules = pathlib.Path(d).joinpath('Modules')
+    return any(modules.joinpath(fn).is_file() for fn in ('Setup', 'Setup.local'))
 
 
 _sys_home = getattr(sys, '_home', None)


Test patch:
diff --git a/setuptools/_distutils/tests/py37compat.py b/setuptools/_distutils/tests/py37compat.py
new file mode 100644
--- /dev/null
+++ b/setuptools/_distutils/tests/py37compat.py
@@ -0,0 +1,18 @@
+import os
+import sys
+import platform
+
+
+def subprocess_args_compat(*args):
+    return list(map(os.fspath, args))
+
+
+def subprocess_args_passthrough(*args):
+    return list(args)
+
+
+subprocess_args = (
+    subprocess_args_compat
+    if platform.system() == "Windows" and sys.version_info < (3, 8)
+    else subprocess_args_passthrough
+)
diff --git a/setuptools/_distutils/tests/py38compat.py b/setuptools/_distutils/tests/py38compat.py
--- a/setuptools/_distutils/tests/py38compat.py
+++ b/setuptools/_distutils/tests/py38compat.py
@@ -18,27 +18,19 @@
 
 try:
     from test.support.os_helper import (
-        change_cwd,
         rmtree,
         EnvironmentVarGuard,
-        TESTFN,
         unlink,
         skip_unless_symlink,
         temp_dir,
-        create_empty_file,
-        temp_cwd,
     )
 except (ModuleNotFoundError, ImportError):
     from test.support import (
-        change_cwd,
         rmtree,
         EnvironmentVarGuard,
-        TESTFN,
         unlink,
         skip_unless_symlink,
         temp_dir,
-        create_empty_file,
-        temp_cwd,
     )
 
 
diff --git a/setuptools/_distutils/tests/test_archive_util.py b/setuptools/_distutils/tests/test_archive_util.py
--- a/setuptools/_distutils/tests/test_archive_util.py
+++ b/setuptools/_distutils/tests/test_archive_util.py
@@ -9,6 +9,7 @@
 import pathlib
 
 import pytest
+import path
 
 from distutils import archive_util
 from distutils.archive_util import (
@@ -23,7 +24,6 @@
 from test.support import patch
 from .unix_compat import require_unix_id, require_uid_0, grp, pwd, UID_0_SUPPORT
 
-from .py38compat import change_cwd
 from .py38compat import check_warnings
 
 
@@ -95,7 +95,7 @@ def _make_tarball(self, tmpdir, target_name, suffix, **kwargs):
         base_name = os.path.join(tmpdir2, target_name)
 
         # working with relative paths to avoid tar warnings
-        with change_cwd(tmpdir):
+        with path.Path(tmpdir):
             make_tarball(splitdrive(base_name)[1], 'dist', **kwargs)
 
         # check if the compressed tarball was created
@@ -227,7 +227,7 @@ def test_make_zipfile(self):
         # creating something to tar
         tmpdir = self._create_files()
         base_name = os.path.join(self.mkdtemp(), 'archive')
-        with change_cwd(tmpdir):
+        with path.Path(tmpdir):
             make_zipfile(base_name, 'dist')
 
         # check if the compressed tarball was created
@@ -253,7 +253,7 @@ def fake_zipfile(*a, **kw):
         # create something to tar and compress
         tmpdir = self._create_files()
         base_name = os.path.join(self.mkdtemp(), 'archive')
-        with change_cwd(tmpdir):
+        with path.Path(tmpdir):
             make_zipfile(base_name, 'dist')
 
         tarball = base_name + '.zip'
diff --git a/setuptools/_distutils/tests/test_build_ext.py b/setuptools/_distutils/tests/test_build_ext.py
--- a/setuptools/_distutils/tests/test_build_ext.py
+++ b/setuptools/_distutils/tests/test_build_ext.py
@@ -8,6 +8,10 @@
 import tempfile
 import importlib
 import shutil
+import re
+
+import path
+import pytest
 
 from distutils.core import Distribution
 from distutils.command.build_ext import build_ext
@@ -27,10 +31,7 @@
 )
 
 from test import support
-from . import py38compat as os_helper
 from . import py38compat as import_helper
-import pytest
-import re
 
 
 @pytest.fixture()
@@ -47,7 +48,7 @@ def user_site_dir(request):
     # bpo-30132: On Windows, a .pdb file may be created in the current
     # working directory. Create a temporary working directory to cleanup
     # everything at the end of the test.
-    with os_helper.change_cwd(self.tmp_dir):
+    with path.Path(self.tmp_dir):
         yield
 
     site.USER_BASE = orig_user_base
diff --git a/setuptools/_distutils/tests/test_core.py b/setuptools/_distutils/tests/test_core.py
--- a/setuptools/_distutils/tests/test_core.py
+++ b/setuptools/_distutils/tests/test_core.py
@@ -8,7 +8,6 @@
 
 import pytest
 
-from . import py38compat as os_helper
 from distutils.dist import Distribution
 
 # setup script that uses __file__
@@ -61,65 +60,63 @@ def save_stdout(monkeypatch):
     monkeypatch.setattr(sys, 'stdout', sys.stdout)
 
 
+@pytest.fixture
+def temp_file(tmp_path):
+    return tmp_path / 'file'
+
+
 @pytest.mark.usefixtures('save_env')
 @pytest.mark.usefixtures('save_argv')
-@pytest.mark.usefixtures('cleanup_testfn')
 class TestCore:
-    def write_setup(self, text, path=os_helper.TESTFN):
-        f = open(path, "w")
-        try:
-            f.write(text)
-        finally:
-            f.close()
-        return path
-
-    def test_run_setup_provides_file(self):
+    def test_run_setup_provides_file(self, temp_file):
         # Make sure the script can use __file__; if that's missing, the test
         # setup.py script will raise NameError.
-        distutils.core.run_setup(self.write_setup(setup_using___file__))
+        temp_file.write_text(setup_using___file__)
+        distutils.core.run_setup(temp_file)
 
-    def test_run_setup_preserves_sys_argv(self):
+    def test_run_setup_preserves_sys_argv(self, temp_file):
         # Make sure run_setup does not clobber sys.argv
         argv_copy = sys.argv.copy()
-        distutils.core.run_setup(self.write_setup(setup_does_nothing))
+        temp_file.write_text(setup_does_nothing)
+        distutils.core.run_setup(temp_file)
         assert sys.argv == argv_copy
 
-    def test_run_setup_defines_subclass(self):
+    def test_run_setup_defines_subclass(self, temp_file):
         # Make sure the script can use __file__; if that's missing, the test
         # setup.py script will raise NameError.
-        dist = distutils.core.run_setup(self.write_setup(setup_defines_subclass))
+        temp_file.write_text(setup_defines_subclass)
+        dist = distutils.core.run_setup(temp_file)
         install = dist.get_command_obj('install')
         assert 'cmd' in install.sub_commands
 
-    def test_run_setup_uses_current_dir(self):
-        # This tests that the setup script is run with the current directory
-        # as its own current directory; this was temporarily broken by a
-        # previous patch when TESTFN did not use the current directory.
+    def test_run_setup_uses_current_dir(self, tmp_path):
+        """
+        Test that the setup script is run with the current directory
+        as its own current directory.
+        """
         sys.stdout = io.StringIO()
         cwd = os.getcwd()
 
         # Create a directory and write the setup.py file there:
-        os.mkdir(os_helper.TESTFN)
-        setup_py = os.path.join(os_helper.TESTFN, "setup.py")
-        distutils.core.run_setup(self.write_setup(setup_prints_cwd, path=setup_py))
+        setup_py = tmp_path / 'setup.py'
+        setup_py.write_text(setup_prints_cwd)
+        distutils.core.run_setup(setup_py)
 
         output = sys.stdout.getvalue()
         if output.endswith("\n"):
             output = output[:-1]
         assert cwd == output
 
-    def test_run_setup_within_if_main(self):
-        dist = distutils.core.run_setup(
-            self.write_setup(setup_within_if_main), stop_after="config"
-        )
+    def test_run_setup_within_if_main(self, temp_file):
+        temp_file.write_text(setup_within_if_main)
+        dist = distutils.core.run_setup(temp_file, stop_after="config")
         assert isinstance(dist, Distribution)
         assert dist.get_name() == "setup_within_if_main"
 
-    def test_run_commands(self):
+    def test_run_commands(self, temp_file):
         sys.argv = ['setup.py', 'build']
-        dist = distutils.core.run_setup(
-            self.write_setup(setup_within_if_main), stop_after="commandline"
-        )
+        temp_file.write_text(setup_within_if_main)
+        dist = distutils.core.run_setup(temp_file, stop_after="commandline")
         assert 'build' not in dist.have_run
         distutils.core.run_commands(dist)
         assert 'build' in dist.have_run
diff --git a/setuptools/_distutils/tests/test_dist.py b/setuptools/_distutils/tests/test_dist.py
--- a/setuptools/_distutils/tests/test_dist.py
+++ b/setuptools/_distutils/tests/test_dist.py
@@ -14,7 +14,6 @@
 from distutils.cmd import Command
 
 from test.support import captured_stdout, captured_stderr
-from .py38compat import TESTFN
 from distutils.tests import support
 from distutils import log
 
@@ -95,15 +94,15 @@ def test_command_packages_cmdline(self, clear_argv):
         'distutils' not in Distribution.parse_config_files.__module__,
         reason='Cannot test when virtualenv has monkey-patched Distribution',
     )
-    def test_venv_install_options(self, request):
+    def test_venv_install_options(self, tmp_path):
         sys.argv.append("install")
-        request.addfinalizer(functools.partial(os.unlink, TESTFN))
+        file = str(tmp_path / 'file')
 
         fakepath = '/somedir'
 
         jaraco.path.build(
             {
-                TESTFN: f"""
+                file: f"""
                     [install]
                     install-base = {fakepath}
                     install-platbase = {fakepath}
@@ -124,9 +123,9 @@ def test_venv_install_options(self, request):
 
         # Base case: Not in a Virtual Environment
         with mock.patch.multiple(sys, prefix='/a', base_prefix='/a'):
-            d = self.create_distribution([TESTFN])
+            d = self.create_distribution([file])
 
-        option_tuple = (TESTFN, fakepath)
+        option_tuple = (file, fakepath)
 
         result_dict = {
             'install_base': option_tuple,
@@ -153,35 +152,35 @@ def test_venv_install_options(self, request):
 
         # Test case: In a Virtual Environment
         with mock.patch.multiple(sys, prefix='/a', base_prefix='/b'):
-            d = self.create_distribution([TESTFN])
+            d = self.create_distribution([file])
 
         for key in result_dict.keys():
             assert key not in d.command_options.get('install', {})
 
-    def test_command_packages_configfile(self, request, clear_argv):
+    def test_command_packages_configfile(self, tmp_path, clear_argv):
         sys.argv.append("build")
-        request.addfinalizer(functools.partial(os.unlink, TESTFN))
+        file = str(tmp_path / "file")
         jaraco.path.build(
             {
-                TESTFN: """
+                file: """
                     [global]
                     command_packages = foo.bar, splat
                     """,
             }
         )
 
-        d = self.create_distribution([TESTFN])
+        d = self.create_distribution([file])
         assert d.get_command_packages() == ["distutils.command", "foo.bar", "splat"]
 
         # ensure command line overrides config:
         sys.argv[1:] = ["--command-packages", "spork", "build"]
-        d = self.create_distribution([TESTFN])
+        d = self.create_distribution([file])
         assert d.get_command_packages() == ["distutils.command", "spork"]
 
         # Setting --command-packages to '' should cause the default to
         # be used even if a config file specified something else:
         sys.argv[1:] = ["--command-packages", "", "build"]
-        d = self.create_distribution([TESTFN])
+        d = self.create_distribution([file])
         assert d.get_command_packages() == ["distutils.command"]
 
     def test_empty_options(self, request):
@@ -259,6 +258,18 @@ def test_find_config_files_disable(self, temp_home):
         # make sure --no-user-cfg disables the user cfg file
         assert len(all_files) - 1 == len(files)
 
+    @pytest.mark.skipif(
+        'platform.system() == "Windows"',
+        reason='Windows does not honor chmod 000',
+    )
+    def test_find_config_files_permission_error(self, fake_home):
+        """
+        Finding config files should not fail when directory is inaccessible.
+        """
+        fake_home.joinpath(pydistutils_cfg).write_text('')
+        fake_home.chmod(0o000)
+        Distribution().find_config_files()
+
 
 @pytest.mark.usefixtures('save_env')
 @pytest.mark.usefixtures('save_argv')
diff --git a/setuptools/_distutils/tests/test_filelist.py b/setuptools/_distutils/tests/test_filelist.py
--- a/setuptools/_distutils/tests/test_filelist.py
+++ b/setuptools/_distutils/tests/test_filelist.py
@@ -8,10 +8,12 @@
 from distutils import filelist
 
 from test.support import captured_stdout
-from distutils.tests import support
 
-from . import py38compat as os_helper
 import pytest
+import jaraco.path
+
+from distutils.tests import support
+from . import py38compat as os_helper
 
 
 MANIFEST_IN = """\
@@ -303,44 +305,35 @@ def test_process_template(self):
 
 class TestFindAll:
     @os_helper.skip_unless_symlink
-    def test_missing_symlink(self):
-        with os_helper.temp_cwd():
-            os.symlink('foo', 'bar')
-            assert filelist.findall() == []
+    def test_missing_symlink(self, temp_cwd):
+        os.symlink('foo', 'bar')
+        assert filelist.findall() == []
 
-    def test_basic_discovery(self):
+    def test_basic_discovery(self, temp_cwd):
         """
         When findall is called with no parameters or with
         '.' as the parameter, the dot should be omitted from
         the results.
         """
-        with os_helper.temp_cwd():
-            os.mkdir('foo')
-            file1 = os.path.join('foo', 'file1.txt')
-            os_helper.create_empty_file(file1)
-            os.mkdir('bar')
-            file2 = os.path.join('bar', 'file2.txt')
-            os_helper.create_empty_file(file2)
-            expected = [file2, file1]
-            assert sorted(filelist.findall()) == expected
-
-    def test_non_local_discovery(self):
+        jaraco.path.build({'foo': {'file1.txt': ''}, 'bar': {'file2.txt': ''}})
+        file1 = os.path.join('foo', 'file1.txt')
+        file2 = os.path.join('bar', 'file2.txt')
+        expected = [file2, file1]
+        assert sorted(filelist.findall()) == expected
+
+    def test_non_local_discovery(self, tmp_path):
         """
         When findall is called with another path, the full
         path name should be returned.
         """
-        with os_helper.temp_dir() as temp_dir:
-            file1 = os.path.join(temp_dir, 'file1.txt')
-            os_helper.create_empty_file(file1)
-            expected = [file1]
-            assert filelist.findall(temp_dir) == expected
+        filename = tmp_path / 'file1.txt'
+        filename.write_text('')
+        expected = [str(filename)]
+        assert filelist.findall(tmp_path) == expected
 
     @os_helper.skip_unless_symlink
-    def test_symlink_loop(self):
-        with os_helper.temp_dir() as temp_dir:
-            link = os.path.join(temp_dir, 'link-to-parent')
-            content = os.path.join(temp_dir, 'somefile')
-            os_helper.create_empty_file(content)
-            os.symlink('.', link)
-            files = filelist.findall(temp_dir)
-            assert len(files) == 1
+    def test_symlink_loop(self, tmp_path):
+        tmp_path.joinpath('link-to-parent').symlink_to('.')
+        tmp_path.joinpath('somefile').write_text('')
+        files = filelist.findall(tmp_path)
+        assert len(files) == 1
diff --git a/setuptools/_distutils/tests/test_spawn.py b/setuptools/_distutils/tests/test_spawn.py
--- a/setuptools/_distutils/tests/test_spawn.py
+++ b/setuptools/_distutils/tests/test_spawn.py
@@ -6,6 +6,8 @@
 
 from test.support import unix_shell
 
+import path
+
 from . import py38compat as os_helper
 
 from distutils.spawn import find_executable
@@ -44,83 +46,82 @@ def test_spawn(self):
         os.chmod(exe, 0o777)
         spawn([exe])  # should work without any error
 
-    def test_find_executable(self):
-        with os_helper.temp_dir() as tmp_dir:
-            # use TESTFN to get a pseudo-unique filename
-            program_noeext = os_helper.TESTFN
-            # Give the temporary program an ".exe" suffix for all.
-            # It's needed on Windows and not harmful on other platforms.
-            program = program_noeext + ".exe"
-
-            filename = os.path.join(tmp_dir, program)
-            with open(filename, "wb"):
-                pass
-            os.chmod(filename, stat.S_IXUSR)
-
-            # test path parameter
-            rv = find_executable(program, path=tmp_dir)
+    def test_find_executable(self, tmp_path):
+        program_noeext = 'program'
+        # Give the temporary program an ".exe" suffix for all.
+        # It's needed on Windows and not harmful on other platforms.
+        program = program_noeext + ".exe"
+
+        program_path = tmp_path / program
+        program_path.write_text("")
+        program_path.chmod(stat.S_IXUSR)
+        filename = str(program_path)
+        tmp_dir = path.Path(tmp_path)
+
+        # test path parameter
+        rv = find_executable(program, path=tmp_dir)
+        assert rv == filename
+
+        if sys.platform == 'win32':
+            # test without ".exe" extension
+            rv = find_executable(program_noeext, path=tmp_dir)
             assert rv == filename
 
-            if sys.platform == 'win32':
-                # test without ".exe" extension
-                rv = find_executable(program_noeext, path=tmp_dir)
-                assert rv == filename
-
-            # test find in the current directory
-            with os_helper.change_cwd(tmp_dir):
+        # test find in the current directory
+        with tmp_dir:
+            rv = find_executable(program)
+            assert rv == program
+
+        # test non-existent program
+        dont_exist_program = "dontexist_" + program
+        rv = find_executable(dont_exist_program, path=tmp_dir)
+        assert rv is None
+
+        # PATH='': no match, except in the current directory
+        with os_helper.EnvironmentVarGuard() as env:
+            env['PATH'] = ''
+            with mock.patch(
+                'distutils.spawn.os.confstr', return_value=tmp_dir, create=True
+            ), mock.patch('distutils.spawn.os.defpath', tmp_dir):
                 rv = find_executable(program)
-                assert rv == program
-
-            # test non-existent program
-            dont_exist_program = "dontexist_" + program
-            rv = find_executable(dont_exist_program, path=tmp_dir)
-            assert rv is None
-
-            # PATH='': no match, except in the current directory
-            with os_helper.EnvironmentVarGuard() as env:
-                env['PATH'] = ''
-                with mock.patch(
-                    'distutils.spawn.os.confstr', return_value=tmp_dir, create=True
-                ), mock.patch('distutils.spawn.os.defpath', tmp_dir):
-                    rv = find_executable(program)
-                    assert rv is None
-
-                    # look in current directory
-                    with os_helper.change_cwd(tmp_dir):
-                        rv = find_executable(program)
-                        assert rv == program
-
-            # PATH=':': explicitly looks in the current directory
-            with os_helper.EnvironmentVarGuard() as env:
-                env['PATH'] = os.pathsep
-                with mock.patch(
-                    'distutils.spawn.os.confstr', return_value='', create=True
-                ), mock.patch('distutils.spawn.os.defpath', ''):
+                assert rv is None
+
+                # look in current directory
+                with tmp_dir:
                     rv = find_executable(program)
-                    assert rv is None
+                    assert rv == program
+
+        # PATH=':': explicitly looks in the current directory
+        with os_helper.EnvironmentVarGuard() as env:
+            env['PATH'] = os.pathsep
+            with mock.patch(
+                'distutils.spawn.os.confstr', return_value='', create=True
+            ), mock.patch('distutils.spawn.os.defpath', ''):
+                rv = find_executable(program)
+                assert rv is None
 
-                    # look in current directory
-                    with os_helper.change_cwd(tmp_dir):
-                        rv = find_executable(program)
-                        assert rv == program
+                # look in current directory
+                with tmp_dir:
+                    rv = find_executable(program)
+                    assert rv == program
 
-            # missing PATH: test os.confstr("CS_PATH") and os.defpath
-            with os_helper.EnvironmentVarGuard() as env:
-                env.pop('PATH', None)
+        # missing PATH: test os.confstr("CS_PATH") and os.defpath
+        with os_helper.EnvironmentVarGuard() as env:
+            env.pop('PATH', None)
 
-                # without confstr
-                with mock.patch(
-                    'distutils.spawn.os.confstr', side_effect=ValueError, create=True
-                ), mock.patch('distutils.spawn.os.defpath', tmp_dir):
-                    rv = find_executable(program)
-                    assert rv == filename
+            # without confstr
+            with mock.patch(
+                'distutils.spawn.os.confstr', side_effect=ValueError, create=True
+            ), mock.patch('distutils.spawn.os.defpath', tmp_dir):
+                rv = find_executable(program)
+                assert rv == filename
 
-                # with confstr
-                with mock.patch(
-                    'distutils.spawn.os.confstr', return_value=tmp_dir, create=True
-                ), mock.patch('distutils.spawn.os.defpath', ''):
-                    rv = find_executable(program)
-                    assert rv == filename
+            # with confstr
+            with mock.patch(
+                'distutils.spawn.os.confstr', return_value=tmp_dir, create=True
+            ), mock.patch('distutils.spawn.os.defpath', ''):
+                rv = find_executable(program)
+                assert rv == filename
 
     def test_spawn_missing_exe(self):
         with pytest.raises(DistutilsExecError) as ctx:
diff --git a/setuptools/_distutils/tests/test_sysconfig.py b/setuptools/_distutils/tests/test_sysconfig.py
--- a/setuptools/_distutils/tests/test_sysconfig.py
+++ b/setuptools/_distutils/tests/test_sysconfig.py
@@ -1,13 +1,14 @@
 """Tests for distutils.sysconfig."""
 import contextlib
 import os
-import shutil
 import subprocess
 import sys
-import textwrap
+import pathlib
 
 import pytest
 import jaraco.envs
+import path
+from jaraco.text import trim
 
 import distutils
 from distutils import sysconfig
@@ -15,33 +16,23 @@
 from distutils.unixccompiler import UnixCCompiler
 from test.support import swap_item
 
-from .py38compat import TESTFN
+from . import py37compat
 
 
 @pytest.mark.usefixtures('save_env')
-@pytest.mark.usefixtures('cleanup_testfn')
 class TestSysconfig:
-    def cleanup_testfn(self):
-        if os.path.isfile(TESTFN):
-            os.remove(TESTFN)
-        elif os.path.isdir(TESTFN):
-            shutil.rmtree(TESTFN)
-
     def test_get_config_h_filename(self):
         config_h = sysconfig.get_config_h_filename()
-        assert os.path.isfile(config_h), config_h
+        assert os.path.isfile(config_h)
 
     @pytest.mark.skipif("platform.system() == 'Windows'")
     @pytest.mark.skipif("sys.implementation.name != 'cpython'")
     def test_get_makefile_filename(self):
         makefile = sysconfig.get_makefile_filename()
-        assert os.path.isfile(makefile), makefile
+        assert os.path.isfile(makefile)
 
-    def test_get_python_lib(self):
-        # XXX doesn't work on Linux when Python was never installed before
-        # self.assertTrue(os.path.isdir(lib_dir), lib_dir)
-        # test for pythonxx.lib?
-        assert sysconfig.get_python_lib() != sysconfig.get_python_lib(prefix=TESTFN)
+    def test_get_python_lib(self, tmp_path):
+        assert sysconfig.get_python_lib() != sysconfig.get_python_lib(prefix=tmp_path)
 
     def test_get_config_vars(self):
         cvars = sysconfig.get_config_vars()
@@ -49,33 +40,39 @@ def test_get_config_vars(self):
         assert cvars
 
     @pytest.mark.skipif('sysconfig.IS_PYPY')
-    @pytest.mark.xfail(reason="broken")
-    def test_srcdir(self):
-        # See Issues #15322, #15364.
-        srcdir = sysconfig.get_config_var('srcdir')
+    @pytest.mark.skipif('sysconfig.python_build')
+    @pytest.mark.xfail('platform.system() == "Windows"')
+    def test_srcdir_simple(self):
+        # See #15364.
+        srcdir = pathlib.Path(sysconfig.get_config_var('srcdir'))
 
-        assert os.path.isabs(srcdir), srcdir
-        assert os.path.isdir(srcdir), srcdir
+        assert srcdir.absolute()
+        assert srcdir.is_dir()
 
-        if sysconfig.python_build:
-            # The python executable has not been installed so srcdir
-            # should be a full source checkout.
-            Python_h = os.path.join(srcdir, 'Include', 'Python.h')
-            assert os.path.exists(Python_h), Python_h
-            assert sysconfig._is_python_source_dir(srcdir)
-        elif os.name == 'posix':
-            assert os.path.dirname(sysconfig.get_makefile_filename()) == srcdir
+        makefile = pathlib.Path(sysconfig.get_makefile_filename())
+        assert makefile.parent.samefile(srcdir)
+
+    @pytest.mark.skipif('sysconfig.IS_PYPY')
+    @pytest.mark.skipif('not sysconfig.python_build')
+    def test_srcdir_python_build(self):
+        # See #15364.
+        srcdir = pathlib.Path(sysconfig.get_config_var('srcdir'))
+
+        # The python executable has not been installed so srcdir
+        # should be a full source checkout.
+        Python_h = srcdir.joinpath('Include', 'Python.h')
+        assert Python_h.is_file()
+        assert sysconfig._is_python_source_dir(srcdir)
+        assert sysconfig._is_python_source_dir(str(srcdir))
 
     def test_srcdir_independent_of_cwd(self):
-        # srcdir should be independent of the current working directory
-        # See Issues #15322, #15364.
+        """
+        srcdir should be independent of the current working directory
+        """
+        # See #15364.
         srcdir = sysconfig.get_config_var('srcdir')
-        cwd = os.getcwd()
-        try:
-            os.chdir('..')
+        with path.Path('..'):
             srcdir2 = sysconfig.get_config_var('srcdir')
-        finally:
-            os.chdir(cwd)
         assert srcdir == srcdir2
 
     def customize_compiler(self):
@@ -169,26 +166,32 @@ def test_customize_compiler(self):
         assert comp.shared_lib_extension == 'sc_shutil_suffix'
         assert 'ranlib' not in comp.exes
 
-    def test_parse_makefile_base(self):
-        self.makefile = TESTFN
-        fd = open(self.makefile, 'w')
-        try:
-            fd.write(r"CONFIG_ARGS=  '--arg1=optarg1' 'ENV=LIB'" '\n')
-            fd.write('VAR=$OTHER\nOTHER=foo')
-        finally:
-            fd.close()
-        d = sysconfig.parse_makefile(self.makefile)
+    def test_parse_makefile_base(self, tmp_path):
+        makefile = tmp_path / 'Makefile'
+        makefile.write_text(
+            trim(
+                """
+                CONFIG_ARGS=  '--arg1=optarg1' 'ENV=LIB'
+                VAR=$OTHER
+                OTHER=foo
+                """
+            )
+        )
+        d = sysconfig.parse_makefile(makefile)
         assert d == {'CONFIG_ARGS': "'--arg1=optarg1' 'ENV=LIB'", 'OTHER': 'foo'}
 
-    def test_parse_makefile_literal_dollar(self):
-        self.makefile = TESTFN
-        fd = open(self.makefile, 'w')
-        try:
-            fd.write(r"CONFIG_ARGS=  '--arg1=optarg1' 'ENV=\$$LIB'" '\n')
-            fd.write('VAR=$OTHER\nOTHER=foo')
-        finally:
-            fd.close()
-        d = sysconfig.parse_makefile(self.makefile)
+    def test_parse_makefile_literal_dollar(self, tmp_path):
+        makefile = tmp_path / 'Makefile'
+        makefile.write_text(
+            trim(
+                """
+                CONFIG_ARGS=  '--arg1=optarg1' 'ENV=\\$$LIB'
+                VAR=$OTHER
+                OTHER=foo
+                """
+            )
+        )
+        d = sysconfig.parse_makefile(makefile)
         assert d == {'CONFIG_ARGS': r"'--arg1=optarg1' 'ENV=\$LIB'", 'OTHER': 'foo'}
 
     def test_sysconfig_module(self):
@@ -231,24 +234,24 @@ def test_SO_deprecation(self):
         with pytest.warns(DeprecationWarning):
             sysconfig.get_config_var('SO')
 
-    def test_customize_compiler_before_get_config_vars(self):
+    def test_customize_compiler_before_get_config_vars(self, tmp_path):
         # Issue #21923: test that a Distribution compiler
         # instance can be called without an explicit call to
         # get_config_vars().
-        with open(TESTFN, 'w') as f:
-            f.writelines(
-                textwrap.dedent(
-                    '''\
+        file = tmp_path / 'file'
+        file.write_text(
+            trim(
+                """
                 from distutils.core import Distribution
                 config = Distribution().get_command_obj('config')
                 # try_compile may pass or it may fail if no compiler
                 # is found but it should not raise an exception.
                 rc = config.try_compile('int x;')
-                '''
-                )
+                """
             )
+        )
         p = subprocess.Popen(
-            [str(sys.executable), TESTFN],
+            py37compat.subprocess_args(sys.executable, file),
             stdout=subprocess.PIPE,
             stderr=subprocess.STDOUT,
             universal_newlines=True,
@@ -278,11 +281,11 @@ def test_win_ext_suffix(self):
         '\\PCbuild\\'.casefold() not in sys.executable.casefold(),
         reason='Need sys.executable to be in a source tree',
     )
-    def test_win_build_venv_from_source_tree(self):
+    def test_win_build_venv_from_source_tree(self, tmp_path):
         """Ensure distutils.sysconfig detects venvs from source tree builds."""
         env = jaraco.envs.VEnv()
         env.create_opts = env.clean_opts
-        env.root = TESTFN
+        env.root = tmp_path
         env.ensure_env()
         cmd = [
             env.exe(),