Repository: facebookincubator/ptr

Issue:
Add a mode to show all Projects with no PTR params/config set
## How is `ptr` not doing what you expect?
It would be nice if you could have ptr crawl the file system and list all the projects who don't have PTR set in setup.cfg or setup.py

## What is your suggestion to make this better?
- Add the feature

## How can a developer reproduce this?
You can't - But an example run could be:
```
ptr --find-unactivated-modules
```

Discussion:

Patch:
diff --git a/ptr.py b/ptr.py
index 5ee0624..eae138d 100755
--- a/ptr.py
+++ b/ptr.py
@@ -296,7 +296,10 @@ def _generate_pyre_cmd(
 
 
 def _get_test_modules(
-    base_path: Path, stats: Dict[str, int], run_disabled: bool
+    base_path: Path,
+    stats: Dict[str, int],
+    run_disabled: bool,
+    print_non_configured: bool,
 ) -> Dict[Path, Dict]:
     get_tests_start_time = time()
     all_setup_pys = find_setup_pys(
@@ -307,6 +310,7 @@ def _get_test_modules(
     )
     stats["total.setup_pys"] = len(all_setup_pys)
 
+    non_configured_modules = []  # type: List[Path]
     test_modules = {}  # type: Dict[Path, Dict]
     for setup_py in all_setup_pys:  # pylint: disable=R1702
         disabled_err_msg = "Not running {} as ptr is disabled via config".format(
@@ -345,7 +349,13 @@ def _get_test_modules(
                                     setup_py
                                 )
                             )
+        if setup_py not in test_modules:
+            non_configured_modules.append(setup_py)
+
+    if print_non_configured and non_configured_modules:
+        print_non_configured_modules(non_configured_modules)
 
+    stats["total.non_ptr_setup_pys"] = len(non_configured_modules)
     stats["total.ptr_setup_pys"] = len(test_modules)
     stats["runtime.parse_setup_pys"] = int(time() - get_tests_start_time)
     return test_modules
@@ -743,32 +753,35 @@ def find_py_files(py_files: Set[str], base_dir: Path) -> None:
         find_py_files(py_files, directory)
 
 
-def find_setup_pys(
-    base_path: Path, exclude_patterns: Set[str], follow_symlinks: bool = False
-) -> Set[Path]:
-    def _recursive_find_files(files: Set[Path], base_dir: Path) -> None:
-        dirs = [d for d in base_dir.iterdir() if d.is_dir()]
-        files.update(
-            {x for x in base_dir.iterdir() if x.is_file() and x.name == "setup.py"}
-        )
-        for directory in dirs:
-            if not follow_symlinks and directory.is_symlink():
-                continue
+def _recursive_find_files(
+    files: Set[Path], base_dir: Path, exclude_patterns: Set[str], follow_symlinks: bool
+) -> None:
+    dirs = [d for d in base_dir.iterdir() if d.is_dir()]
+    files.update(
+        {x for x in base_dir.iterdir() if x.is_file() and x.name == "setup.py"}
+    )
+    for directory in dirs:
+        if not follow_symlinks and directory.is_symlink():
+            continue
 
-            skip_dir = False
-            for exclude_pattern in exclude_patterns:
-                if directory.match(exclude_pattern):
-                    skip_dir = True
-                    LOG.debug(
-                        "Skipping {} due to exclude pattern {}".format(
-                            directory, exclude_pattern
-                        )
+        skip_dir = False
+        for exclude_pattern in exclude_patterns:
+            if directory.match(exclude_pattern):
+                skip_dir = True
+                LOG.debug(
+                    "Skipping {} due to exclude pattern {}".format(
+                        directory, exclude_pattern
                     )
-            if not skip_dir:
-                _recursive_find_files(files, directory)
+                )
+        if not skip_dir:
+            _recursive_find_files(files, directory, exclude_patterns, follow_symlinks)
+
 
+def find_setup_pys(
+    base_path: Path, exclude_patterns: Set[str], follow_symlinks: bool = False
+) -> Set[Path]:
     setup_pys = set()  # type: Set[Path]
-    _recursive_find_files(setup_pys, base_path)
+    _recursive_find_files(setup_pys, base_path, exclude_patterns, follow_symlinks)
     return setup_pys
 
 
@@ -803,6 +816,12 @@ def parse_setup_cfg(setup_py: Path) -> Dict[str, Any]:
     return ptr_params
 
 
+def print_non_configured_modules(modules: List[Path]) -> None:
+    print("== {} non ptr configured modules ==".format(len(modules)))
+    for module in sorted(modules):
+        print(" - {}".format(str(module)))
+
+
 def print_test_results(
     test_results: Sequence[test_result], stats: Optional[Dict[str, int]] = None
 ) -> Dict[str, int]:
@@ -935,13 +954,16 @@ async def async_main(
     venv: str,
     venv_keep: bool,
     print_cov: bool,
+    print_non_configured: bool,
     run_disabled: bool,
     force_black: bool,
     stats_file: str,
     venv_timeout: float,
 ) -> int:
     stats = defaultdict(int)  # type: Dict[str, int]
-    tests_to_run = _get_test_modules(base_path, stats, run_disabled)
+    tests_to_run = _get_test_modules(
+        base_path, stats, run_disabled, print_non_configured
+    )
     if not tests_to_run:
         LOG.error(
             "{} has no setup.py files with unit tests defined. Exiting".format(
@@ -950,6 +972,9 @@ async def async_main(
         )
         return 1
 
+    if print_non_configured:
+        return 0
+
     try:
         venv_path = Path(venv)  # type: Optional[Path]
         if venv_path and not venv_path.exists():
@@ -1013,6 +1038,11 @@ def main() -> None:
     parser.add_argument(
         "--print-cov", action="store_true", help="Print modules coverage report"
     )
+    parser.add_argument(
+        "--print-non-configured",
+        action="store_true",
+        help="Print modules not configured to run ptr",
+    )
     parser.add_argument(
         "--progress-interval",
         default=0,
@@ -1053,6 +1083,7 @@ def main() -> None:
                     args.venv,
                     args.keep_venv,
                     args.print_cov,
+                    args.print_non_configured,
                     args.run_disabled,
                     args.force_black,
                     args.stats_file,
diff --git a/setup.py b/setup.py
index 0bb7805..c0c56d0 100644
--- a/setup.py
+++ b/setup.py
@@ -20,7 +20,7 @@ ptr_params = {
     "test_suite": "ptr_tests",
     "test_suite_timeout": 120,
     # Relative path from setup.py to module (e.g. ptr == ptr.py)
-    "required_coverage": {"ptr.py": 84, "TOTAL": 89},
+    "required_coverage": {"ptr.py": 83, "TOTAL": 89},
     # Run black or not
     "run_black": True,
     # Run mypy or not
@@ -47,7 +47,7 @@ def get_long_desc() -> str:
 
 setup(
     name=ptr_params["entry_point_module"],
-    version="19.6.15",
+    version="19.7.16",
     description="Parallel asyncio Python setup.(cfg|py) Test Runner",
     long_description=get_long_desc(),
     long_description_content_type="text/markdown",


Test patch:
diff --git a/ptr_tests.py b/ptr_tests.py
index 39afaeb..fb8e02a 100644
--- a/ptr_tests.py
+++ b/ptr_tests.py
@@ -130,7 +130,20 @@ class TestPtr(unittest.TestCase):
     @patch("ptr.run_tests", async_none)
     @patch("ptr._get_test_modules")
     def test_async_main(self, mock_gtm: Mock) -> None:
-        args = [1, Path("/"), "mirror", 1, "venv", True, True, True, False, "stats", 30]
+        args = [
+            1,
+            Path("/"),
+            "mirror",
+            1,
+            "venv",
+            True,
+            True,
+            False,
+            True,
+            False,
+            "stats",
+            30,
+        ]
         mock_gtm.return_value = False
         self.assertEqual(
             self.loop.run_until_complete(ptr.async_main(*args)), 1  # pyre-ignore
@@ -156,7 +169,7 @@ class TestPtr(unittest.TestCase):
         self.assertEqual(
             sc["ptr"].get("pypi_url", ""), expected_pypi_url  # pyre-ignore
         )
-        self.assertEqual(len(sc["ptr"].get("venv_pkgs", "").split()), 8)  # pyre-ignore
+        self.assertEqual(len(sc["ptr"].get("venv_pkgs", "").split()), 8)
 
     @patch("ptr._gen_check_output", async_none)  # noqa
     @patch("ptr._set_pip_mirror")  # noqa
@@ -280,16 +293,20 @@ class TestPtr(unittest.TestCase):
             lib_path.mkdir()
             self.assertIsNone(ptr._get_site_packages_path(lib_path.parent))
 
-    def test_get_test_modules(self) -> None:
+    @patch("ptr.print")  # noqa
+    def test_get_test_modules(self, mock_print: Mock) -> None:
         base_path = Path(__file__).parent
         stats = defaultdict(int)  # type: Dict[str, int]
-        test_modules = ptr._get_test_modules(base_path, stats, True)
+        test_modules = ptr._get_test_modules(base_path, stats, True, True)
         self.assertEqual(
             test_modules[base_path / "setup.py"],
             ptr_tests_fixtures.EXPECTED_TEST_PARAMS,
         )
-        self.assertEqual(stats["total.setup_pys"], 1)
+        self.assertEqual(stats["total.non_ptr_setup_pys"], 0)
         self.assertEqual(stats["total.ptr_setup_pys"], 1)
+        self.assertEqual(stats["total.setup_pys"], 1)
+        # Make sure we don't run print even tho we set the option to True
+        self.assertFalse(mock_print.called)
 
     def test_gen_output(self) -> None:
         test_cmd = ("echo.exe", "''") if ptr.WINDOWS else ("/bin/echo",)
@@ -335,6 +352,13 @@ class TestPtr(unittest.TestCase):
             ptr.parse_setup_cfg(setup_py), ptr_tests_fixtures.EXPECTED_TEST_PARAMS
         )
 
+    @patch("ptr.print")  # noqa
+    def test_print_non_configured_modules(self, mock_print: Mock) -> None:
+        modules = [Path("/tmp/foo/setup.py"), Path("/tmp/bla/setup.py")]
+        # TODO: Workout why pylint things this function does not exist
+        ptr.print_non_configured_modules(modules)  # pylint: disable=E1101
+        self.assertEqual(3, mock_print.call_count)
+
     @patch("ptr.print")  # noqa
     def test_print_test_results(self, mock_print: Mock) -> None:
         stats = ptr.print_test_results(ptr_tests_fixtures.EXPECTED_COVERAGE_RESULTS)
diff --git a/ptr_tests_fixtures.py b/ptr_tests_fixtures.py
index d278888..cb51eaa 100644
--- a/ptr_tests_fixtures.py
+++ b/ptr_tests_fixtures.py
@@ -29,7 +29,7 @@ EXPECTED_TEST_PARAMS = {
     "entry_point_module": "ptr",
     "test_suite": "ptr_tests",
     "test_suite_timeout": 120,
-    "required_coverage": {"ptr.py": 84, "TOTAL": 89},
+    "required_coverage": {"ptr.py": 83, "TOTAL": 89},
     "run_black": True,
     "run_mypy": True,
     "run_flake8": True,
@@ -196,7 +196,7 @@ disabled = true
 entry_point_module = ptr
 test_suite = ptr_tests
 test_suite_timeout = 120
-required_coverage_ptr.py = 84
+required_coverage_ptr.py = 83
 required_coverage_TOTAL = 89
 run_black = true
 run_mypy = true