From 2fd88298ae34c954f61bc7b43fd9566cc2e73c91 Mon Sep 17 00:00:00 2001 From: Alex Willmer Date: Mon, 18 Aug 2025 14:34:20 +0100 Subject: [PATCH] tests: Improve master_test.ScanCodeImportsTest coverage This covers existing behaviours of `mitogen.master.scan_code_imports()` some of which are relied on, some not, but regardless weren't tested. Notably - Explicit relative imports return level > 0 - Imports inside `class` and `def` are excluded - Imports inside other blocks are included - Python 3.x prunes impossible if/else branches (previously unknown) It also - Decouples the test results from the implementation details of the unit test. - Fixes a missing import - Fixes at least one Python 2.4 incompatibility (use of with block) --- docs/changelog.rst | 1 + tests/data/importer/scanning/defaults.py | 11 ++ .../importer/scanning/explicit_relative.py | 9 ++ .../importer/scanning/has_absolute_import.py | 13 ++ tests/data/importer/scanning/scoped_class.py | 7 + .../data/importer/scanning/scoped_function.py | 3 + .../data/importer/scanning/scoped_if_else.py | 16 ++ .../importer/scanning/scoped_try_except.py | 9 ++ tests/master_test.py | 138 +++++++++++++++--- 9 files changed, 190 insertions(+), 17 deletions(-) create mode 100644 tests/data/importer/scanning/defaults.py create mode 100644 tests/data/importer/scanning/explicit_relative.py create mode 100644 tests/data/importer/scanning/has_absolute_import.py create mode 100644 tests/data/importer/scanning/scoped_class.py create mode 100644 tests/data/importer/scanning/scoped_function.py create mode 100644 tests/data/importer/scanning/scoped_if_else.py create mode 100644 tests/data/importer/scanning/scoped_try_except.py diff --git a/docs/changelog.rst b/docs/changelog.rst index 7c36f987..410dee9b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -23,6 +23,7 @@ In progress (unreleased) * :gh:issue:`1329` CI: Refactor and de-duplicate Github Actions workflow * :gh:issue:`1315` CI: macOS: Increase failed logins limit of test users +* :gh:issue:`1325` tests: Improve ``master_test.ScanCodeImportsTest`` coverage v0.3.26 (2025-08-04) diff --git a/tests/data/importer/scanning/defaults.py b/tests/data/importer/scanning/defaults.py new file mode 100644 index 00000000..4350ce4c --- /dev/null +++ b/tests/data/importer/scanning/defaults.py @@ -0,0 +1,11 @@ +# pyright: reportMissingImports=false +# ruff: noqa: E401 E702 F401 F403 + +import a +import a.b +import c as d +import e, e.f as g \ + , h; import i + +from j import k, l, m as n +from o import * diff --git a/tests/data/importer/scanning/explicit_relative.py b/tests/data/importer/scanning/explicit_relative.py new file mode 100644 index 00000000..ad0dd4d7 --- /dev/null +++ b/tests/data/importer/scanning/explicit_relative.py @@ -0,0 +1,9 @@ +# pyright: reportMissingImports=false +# ruff: noqa: E401 E702 F401 F403 + +from . import a +from .b import c, d as e +from ... import ( + f, + j as k, +) diff --git a/tests/data/importer/scanning/has_absolute_import.py b/tests/data/importer/scanning/has_absolute_import.py new file mode 100644 index 00000000..eb75406a --- /dev/null +++ b/tests/data/importer/scanning/has_absolute_import.py @@ -0,0 +1,13 @@ +# pyright: reportMissingImports=false +# ruff: noqa: E401 E702 F401 F403 + +from __future__ import absolute_import + +import a +import a.b +import c as d +import e, e.f as g \ + , h; import i + +from j import k, l, m as n +from o import * diff --git a/tests/data/importer/scanning/scoped_class.py b/tests/data/importer/scanning/scoped_class.py new file mode 100644 index 00000000..34577b01 --- /dev/null +++ b/tests/data/importer/scanning/scoped_class.py @@ -0,0 +1,7 @@ +class C: + import in_class + from in_class import x as y + + def m(self): + import in_method + from in_method import x as y, z diff --git a/tests/data/importer/scanning/scoped_function.py b/tests/data/importer/scanning/scoped_function.py new file mode 100644 index 00000000..983f5fe8 --- /dev/null +++ b/tests/data/importer/scanning/scoped_function.py @@ -0,0 +1,3 @@ +def f(): + import in_func + from in_func import x as y, z diff --git a/tests/data/importer/scanning/scoped_if_else.py b/tests/data/importer/scanning/scoped_if_else.py new file mode 100644 index 00000000..8a98b698 --- /dev/null +++ b/tests/data/importer/scanning/scoped_if_else.py @@ -0,0 +1,16 @@ +import sys + + +if True: + import in_if_always_true + from in_if_always_true import x as y, z +else: + import in_else_never_true + from in_else_never_true import x as y, z + +if sys.version >= (3, 0): + import in_if_py3 + from in_if_py3 import x as y, z +else: + import in_else_py2 + from in_else_py2 import x as y, z diff --git a/tests/data/importer/scanning/scoped_try_except.py b/tests/data/importer/scanning/scoped_try_except.py new file mode 100644 index 00000000..d255621e --- /dev/null +++ b/tests/data/importer/scanning/scoped_try_except.py @@ -0,0 +1,9 @@ +try: + import in_try + from in_try import x as y, z +except ImportError: + import in_except_importerror + from in_except_importerror import x as y, z +except Exception: + import in_except_exception + from in_except_exception import x as y, z diff --git a/tests/master_test.py b/tests/master_test.py index 2af00718..519ac3af 100644 --- a/tests/master_test.py +++ b/tests/master_test.py @@ -1,25 +1,129 @@ -import inspect +import os +import sys +import unittest import testlib import mitogen.master +def testmod_compile(path): + path = os.path.join(testlib.MODS_DIR, path) + f = open(path, 'rb') + co = compile(f.read(), path, 'exec') + f.close() + return co + + class ScanCodeImportsTest(testlib.TestCase): func = staticmethod(mitogen.master.scan_code_imports) - if mitogen.core.PY3: - level = 0 - else: - level = -1 - - SIMPLE_EXPECT = [ - (level, 'inspect', ()), - (level, 'testlib', ()), - (level, 'mitogen.master', ()), - ] - - def test_simple(self): - source_path = inspect.getsourcefile(ScanCodeImportsTest) - with open(source_path) as f: - co = compile(f.read(), source_path, 'exec') - self.assertEqual(list(self.func(co)), self.SIMPLE_EXPECT) + @unittest.skipIf(sys.version_info < (3, 0), "Py is 2.x, would be relative") + def test_default_absolute(self): + co = testmod_compile('scanning/defaults.py') + expected = [ + (0, 'a', ()), (0, 'a.b', ()), (0, 'c', ()), + (0, 'e', ()), (0, 'e.f', ()), (0, 'h', ()), + (0, 'i', ()), + (0, 'j', ('k', 'l', 'm')), + (0, 'o', ('*',)), + ] + self.assertEqual(list(self.func(co)), expected) + + @unittest.skipIf(sys.version_info >= (3, 0), "Py is 3.x, would be absolute") + def test_default_relative(self): + co = testmod_compile('scanning/defaults.py') + expected = [ + (-1, 'a', ()), (-1, 'a.b', ()), (-1, 'c', ()), + (-1, 'e', ()), (-1, 'e.f', ()), (-1, 'h', ()), + (-1, 'i', ()), + (-1, 'j', ('k', 'l', 'm')), + (-1, 'o', ('*',)), + ] + self.assertEqual(list(self.func(co)), expected) + + @unittest.skipIf(sys.version_info < (2, 5), "Py is 2.4, no absolute_import") + def test_explicit_absolute(self): + co = testmod_compile('scanning/has_absolute_import.py') + expected = [ + (0, '__future__', ('absolute_import',)), + + (0, 'a', ()), (0, 'a.b', ()), (0, 'c', ()), + (0, 'e', ()), (0, 'e.f', ()), (0, 'h', ()), + (0, 'i', ()), + (0, 'j', ('k', 'l', 'm')), + (0, 'o', ('*',)), + ] + self.assertEqual(list(self.func(co)), expected) + + @unittest.skipIf(sys.version_info < (2, 5), "Py is 2.4, no `from . import x`") + def test_explicit_relative(self): + co = testmod_compile('scanning/explicit_relative.py') + expected = [ + (1, '', ('a',)), + (1, 'b', ('c', 'd')), + (3, '', ('f', 'j')), + ] + self.assertEqual(list(self.func(co)), expected) + + def test_scoped_class(self): + # Imports in `class` or `def` are ignored, a bad heuristc to detect + # lazy imports and skip sending the pre-emptively. + # See + # - https://github.com/mitogen-hq/mitogen/issues/682 + # - https://github.com/mitogen-hq/mitogen/issues/1325#issuecomment-3170482014 + co = testmod_compile('scanning/scoped_class.py') + self.assertEqual(list(self.func(co)), []) + + pass + + def test_scoped_function(self): + co = testmod_compile('scanning/scoped_function.py') + self.assertEqual(list(self.func(co)), []) + + @unittest.skipIf(sys.version_info >= (3, 0), "Python is 3.x, which prunes") + def test_scoped_if_else_unpruned(self): + co = testmod_compile('scanning/scoped_if_else.py') + level = (-1, 0)[int(sys.version_info >= (3, 0))] + expected = [ + (level, 'sys', ()), + (level, 'in_if_always_true', ()), + (level, 'in_if_always_true', ('x', 'z')), + # Python 2.x does no pruning + (level, 'in_else_never_true', ()), + (level, 'in_else_never_true', ('x', 'z')), + (level, 'in_if_py3', ()), + (level, 'in_if_py3', ('x', 'z')), + (level, 'in_else_py2', ()), + (level, 'in_else_py2', ('x', 'z')), + ] + self.assertEqual(list(self.func(co)), expected) + + @unittest.skipIf(sys.version_info < (3, 0), "Python is 2.x, which doesn't prune") + def test_scoped_if_else_pruned(self): + co = testmod_compile('scanning/scoped_if_else.py') + level = (-1, 0)[int(sys.version_info >= (3, 0))] + expected = [ + (level, 'sys', ()), + (level, 'in_if_always_true', ()), + (level, 'in_if_always_true', ('x', 'z')), + # Python 3.x prunes some impossible branches ... + (level, 'in_if_py3', ()), + (level, 'in_if_py3', ('x', 'z')), + # ... but not sys.version_info ones + (level, 'in_else_py2', ()), + (level, 'in_else_py2', ('x', 'z')), + ] + self.assertEqual(list(self.func(co)), expected) + + def test_scoped_try_except(self): + co = testmod_compile('scanning/scoped_try_except.py') + level = (-1, 0)[int(sys.version_info >= (3, 0))] + expected = [ + (level, 'in_try', ()), + (level, 'in_try', ('x', 'z')), + (level, 'in_except_importerror', ()), + (level, 'in_except_importerror', ('x', 'z')), + (level, 'in_except_exception', ()), + (level, 'in_except_exception', ('x', 'z')), + ] + self.assertEqual(list(self.func(co)), expected)