pw_presubmit: Don't use setup.py test

Previously, Python tests were executed using setup.py test. There are a
few issues with this. First, with the current setup, tests are executed
twice for some reason. Occasionally other odd errors occur. Also,
setuptools has deprecated the test command for setup.py and states that
it will be removed in a future version.

This change discovers tests by finding the package directories of
affected files and searching for tests with customizable patterns
('*_test.py' by default). This approach is simpler, faster, and no
longer relies on setuptools or unittest.

Since setup.py test is no longer used, this change removes test suite
discovery from Pigweed's setup.py files.

Change-Id: I3de6c5d05ea5e38eea27b0e3aca956fe468035be
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/12662
Reviewed-by: Rob Mohr <mohrr@google.com>
Commit-Queue: Wyatt Hepler <hepler@google.com>
diff --git a/pw_bloat/py/setup.py b/pw_bloat/py/setup.py
index f639fae..8827b83 100644
--- a/pw_bloat/py/setup.py
+++ b/pw_bloat/py/setup.py
@@ -13,15 +13,8 @@
 # the License.
 """pw_bloat"""
 
-import unittest
 import setuptools
 
-
-def test_suite():
-    """Test suite for pw_bloat module."""
-    return unittest.TestLoader().discover('./', pattern='*_test.py')
-
-
 setuptools.setup(
     name='pw_bloat',
     version='0.0.1',
@@ -29,5 +22,4 @@
     author_email='pigweed-developers@googlegroups.com',
     description='Tools for generating binary size report cards',
     packages=setuptools.find_packages(),
-    test_suite='setup.test_suite',
 )
diff --git a/pw_cli/py/setup.py b/pw_cli/py/setup.py
index 699a33c..94df754 100644
--- a/pw_cli/py/setup.py
+++ b/pw_cli/py/setup.py
@@ -13,15 +13,8 @@
 # the License.
 """pw_cli"""
 
-import unittest
 import setuptools
 
-
-def test_suite():
-    """Test suite for pw_cli module."""
-    return unittest.TestLoader().discover('./', pattern='*_test.py')
-
-
 setuptools.setup(
     name='pw_cli',
     version='0.0.1',
@@ -29,6 +22,5 @@
     author_email='pigweed-developers@googlegroups.com',
     description='Pigweed swiss-army knife',
     packages=setuptools.find_packages(),
-    test_suite='setup.test_suite',
     entry_points={'console_scripts': ['pw = pw_cli.__main__:main']},
 )
diff --git a/pw_doctor/py/setup.py b/pw_doctor/py/setup.py
index b870a42..752f5e2 100644
--- a/pw_doctor/py/setup.py
+++ b/pw_doctor/py/setup.py
@@ -13,15 +13,8 @@
 # the License.
 """The pw_doctor package."""
 
-import unittest
 import setuptools
 
-
-def test_suite():
-    """Test suite for pw_doctor module."""
-    return unittest.TestLoader().discover('./', pattern='*_test.py')
-
-
 setuptools.setup(
     name='pw_doctor',
     version='0.0.1',
@@ -29,5 +22,4 @@
     author_email='pigweed-developers@googlegroups.com',
     description='Environment check script for Pigweed',
     packages=setuptools.find_packages(),
-    test_suite='setup.test_suite',
 )
diff --git a/pw_env_setup/py/setup.py b/pw_env_setup/py/setup.py
index 9cd86f4..a0beace 100644
--- a/pw_env_setup/py/setup.py
+++ b/pw_env_setup/py/setup.py
@@ -13,15 +13,8 @@
 # the License.
 """env_setup module definition for PyOxidizer."""
 
-import unittest
 import setuptools
 
-
-def test_suite():
-    """Test suite for env_setup module."""
-    return unittest.TestLoader().discover('./', pattern='*_test.py')
-
-
 setuptools.setup(
     name='pw_env_setup',
     version='0.0.1',
@@ -29,7 +22,6 @@
     author_email='pigweed-developers@googlegroups.com',
     description='Environment setup for Pigweed',
     packages=setuptools.find_packages(),
-    test_suite='setup.test_suite',
     entry_points={
         'console_scripts': ['_pw_env_setup = pw_env_setup.env_setup:main'],
     },
diff --git a/pw_module/py/setup.py b/pw_module/py/setup.py
index 4014feb..46e1ab1 100644
--- a/pw_module/py/setup.py
+++ b/pw_module/py/setup.py
@@ -13,15 +13,8 @@
 # the License.
 """pw_module"""
 
-import unittest
 import setuptools
 
-
-def test_suite():
-    """Test suite for pw_module module."""
-    return unittest.TestLoader().discover('./', pattern='*_test.py')
-
-
 setuptools.setup(
     name='pw_module',
     version='0.0.1',
@@ -29,5 +22,4 @@
     author_email='pigweed-developers@googlegroups.com',
     description='Meta-module for Pigweed',
     packages=setuptools.find_packages(),
-    test_suite='setup.test_suite',
 )
diff --git a/pw_presubmit/py/pw_presubmit/python_checks.py b/pw_presubmit/py/pw_presubmit/python_checks.py
index 9b294ee..2fbb4a0 100644
--- a/pw_presubmit/py/pw_presubmit/python_checks.py
+++ b/pw_presubmit/py/pw_presubmit/python_checks.py
@@ -21,7 +21,7 @@
 from pathlib import Path
 import re
 import sys
-from typing import List
+from typing import Callable, Iterable, List, Tuple
 
 try:
     import pw_presubmit
@@ -42,7 +42,16 @@
 
 
 @filter_paths(endswith='.py')
-def test_python_packages(ctx: pw_presubmit.PresubmitContext):
+def test_python_packages(ctx: pw_presubmit.PresubmitContext,
+                         patterns: Iterable[str] = '*_test.py') -> None:
+    """Finds and runs test files in Python package directories.
+
+    Finds the Python packages containing the affected paths, then searches
+    within that package for test files. All files matching the provided patterns
+    are executed with Python.
+    """
+    test_globs = [patterns] if isinstance(patterns, str) else list(patterns)
+
     packages: List[Path] = []
     for repo in ctx.repos:
         packages += git_repo.find_python_packages(ctx.paths, repo=repo)
@@ -52,11 +61,13 @@
         return
 
     for package in packages:
-        call('python', package / 'setup.py', 'test', cwd=package)
+        for test in git_repo.list_files(pathspecs=test_globs,
+                                        repo_path=package):
+            call('python', test)
 
 
 @filter_paths(endswith='.py')
-def pylint(ctx: pw_presubmit.PresubmitContext):
+def pylint(ctx: pw_presubmit.PresubmitContext) -> None:
     disable_checkers = [
         # BUG(pwbug/22): Hanging indent check conflicts with YAPF 0.29. For
         # now, use YAPF's version even if Pylint is doing the correct thing
@@ -79,7 +90,7 @@
 
 
 @filter_paths(endswith='.py')
-def mypy(ctx: pw_presubmit.PresubmitContext):
+def mypy(ctx: pw_presubmit.PresubmitContext) -> None:
     env = os.environ.copy()
     # Use this environment variable to force mypy to colorize output.
     # See https://github.com/python/mypy/issues/7771
@@ -90,6 +101,7 @@
         *(p for p in ctx.paths if not _SETUP_PY.fullmatch(p.as_posix())),
         '--pretty',
         '--color-output',
+        '--show-error-codes',
         # TODO(pwbug/146): Some imports from installed packages fail. These
         # imports should be fixed and this option removed.
         '--ignore-missing-imports',
@@ -103,7 +115,8 @@
 )
 
 
-def all_checks(endswith='.py', **filter_paths_args):
+def all_checks(endswith: str = '.py',
+               **filter_paths_args) -> Tuple[Callable, ...]:
     return tuple(
         filter_paths(endswith=endswith, **filter_paths_args)(function)
         for function in _ALL_CHECKS)
diff --git a/pw_presubmit/py/setup.py b/pw_presubmit/py/setup.py
index bdb486f..791691a 100644
--- a/pw_presubmit/py/setup.py
+++ b/pw_presubmit/py/setup.py
@@ -13,15 +13,8 @@
 # the License.
 """The pw_presubmit package."""
 
-import unittest
 import setuptools
 
-
-def test_suite():
-    """Test suite for pw_module module."""
-    return unittest.TestLoader().discover('./', pattern='*_test.py')
-
-
 setuptools.setup(
     name='pw_presubmit',
     version='0.0.1',
@@ -34,5 +27,4 @@
         'yapf==0.30.0',
     ],
     packages=setuptools.find_packages(),
-    test_suite='setup.test_suite',
 )
diff --git a/pw_protobuf/py/setup.py b/pw_protobuf/py/setup.py
index 4d99196..7134f1a 100644
--- a/pw_protobuf/py/setup.py
+++ b/pw_protobuf/py/setup.py
@@ -13,15 +13,8 @@
 # the License.
 """pw_protobuf"""
 
-import unittest
 import setuptools
 
-
-def test_suite():
-    """Test suite for pw_protobuf module."""
-    return unittest.TestLoader().discover('./', pattern='*_test.py')
-
-
 setuptools.setup(
     name='pw_protobuf',
     version='0.0.1',
@@ -29,7 +22,6 @@
     author_email='pigweed-developers@googlegroups.com',
     description='Lightweight streaming protobuf implementation',
     packages=setuptools.find_packages(),
-    test_suite='setup.test_suite',
     entry_points={
         'console_scripts': ['pw_protobuf_codegen = pw_protobuf.plugin:main']
     },
diff --git a/pw_protobuf_compiler/py/setup.py b/pw_protobuf_compiler/py/setup.py
index bf706c7..ebee3fd 100644
--- a/pw_protobuf_compiler/py/setup.py
+++ b/pw_protobuf_compiler/py/setup.py
@@ -13,15 +13,8 @@
 # the License.
 """pw_protobuf_compiler"""
 
-import unittest
 import setuptools
 
-
-def test_suite():
-    """Test suite for pw_protobuf_compiler module."""
-    return unittest.TestLoader().discover('./', pattern='*_test.py')
-
-
 setuptools.setup(
     name='pw_protobuf_compiler',
     version='0.0.1',
@@ -29,7 +22,6 @@
     author_email='pigweed-developers@googlegroups.com',
     description='Pigweed protoc wrapper',
     packages=setuptools.find_packages(),
-    test_suite='setup.test_suite',
     entry_points={
         'console_scripts':
         ['generate_protos = pw_protobuf_compiler.generate_protos:main']
diff --git a/pw_tokenizer/py/setup.py b/pw_tokenizer/py/setup.py
index 81ec43a..3b3dec2 100644
--- a/pw_tokenizer/py/setup.py
+++ b/pw_tokenizer/py/setup.py
@@ -13,14 +13,8 @@
 # the License.
 """The pw_tokenizer package."""
 
-import unittest
 import setuptools
 
-
-def test_suite():
-    return unittest.TestLoader().discover('./', pattern='*_test.py')
-
-
 setuptools.setup(
     name='pw_tokenizer',
     version='0.0.1',
diff --git a/pw_unit_test/py/setup.py b/pw_unit_test/py/setup.py
index e3bfae1..bfe6b39 100644
--- a/pw_unit_test/py/setup.py
+++ b/pw_unit_test/py/setup.py
@@ -13,15 +13,8 @@
 # the License.
 """pw_unit_test"""
 
-import unittest
 import setuptools
 
-
-def test_suite():
-    """Test suite for pw_unit_test module."""
-    return unittest.TestLoader().discover('./', pattern='*_test.py')
-
-
 setuptools.setup(
     name='pw_unit_test',
     version='0.1.0',
@@ -29,7 +22,6 @@
     author_email='pigweed-developers@googlegroups.com',
     description='Unit tests for Pigweed projects',
     packages=setuptools.find_packages(),
-    test_suite='setup.test_suite',
     install_requires=[
         'pw_cli',
     ],
diff --git a/pw_watch/py/setup.py b/pw_watch/py/setup.py
index b4fc30d..f099427 100644
--- a/pw_watch/py/setup.py
+++ b/pw_watch/py/setup.py
@@ -13,15 +13,8 @@
 # the License.
 """pw_watch"""
 
-import unittest
 import setuptools
 
-
-def test_suite():
-    """Test suite for pw_watch module."""
-    return unittest.TestLoader().discover('./', pattern='*_test.py')
-
-
 setuptools.setup(
     name='pw_watch',
     version='0.0.1',
@@ -29,7 +22,6 @@
     author_email='pigweed-developers@googlegroups.com',
     description='Pigweed automatic builder',
     packages=setuptools.find_packages(),
-    test_suite='setup.test_suite',
     install_requires=[
         'watchdog',
     ],
diff --git a/targets/lm3s6965evb-qemu/py/setup.py b/targets/lm3s6965evb-qemu/py/setup.py
index 7e5b521..a60dc3f 100644
--- a/targets/lm3s6965evb-qemu/py/setup.py
+++ b/targets/lm3s6965evb-qemu/py/setup.py
@@ -13,15 +13,8 @@
 # the License.
 """lm3s6965evb_qemu_utils"""
 
-import unittest
 import setuptools
 
-
-def test_suite():
-    """Test suite for lm3s6965evb_qemu_utils."""
-    return unittest.TestLoader().discover('./', pattern='*_test.py')
-
-
 setuptools.setup(
     name='lm3s6965evb_qemu_utils',
     version='0.0.1',
@@ -30,7 +23,6 @@
     description=
     'Target-specific python scripts for the lm3s6965evb-qemu target',
     packages=setuptools.find_packages(),
-    test_suite='setup.test_suite',
     entry_points={
         'console_scripts': [
             'lm3s6965evb_qemu_unit_test_runner = '
diff --git a/targets/stm32f429i-disc1/py/setup.py b/targets/stm32f429i-disc1/py/setup.py
index 2abbe06..1c47928 100644
--- a/targets/stm32f429i-disc1/py/setup.py
+++ b/targets/stm32f429i-disc1/py/setup.py
@@ -13,15 +13,8 @@
 # the License.
 """stm32f429i_disc1_utils"""
 
-import unittest
 import setuptools
 
-
-def test_suite():
-    """Test suite for stm32f429i_disc1_utils."""
-    return unittest.TestLoader().discover('./', pattern='*_test.py')
-
-
 setuptools.setup(
     name='stm32f429i_disc1_utils',
     version='0.0.1',
@@ -30,7 +23,6 @@
     description=
     'Target-specific python scripts for the stm32f429i-disc1 target',
     packages=setuptools.find_packages(),
-    test_suite='setup.test_suite',
     entry_points={
         'console_scripts': [
             'stm32f429i_disc1_unit_test_runner = '