fix coverage output from synthetic packages (#71727)
* fix coverage output from synthetic packages * synthetic packages (eg, implicit collection packages without `__init__.py`) were always created at runtime with empty string source, which was compiled to a code object and exec'd during the package load. When run with code coverage, it created a bogus coverage entry (since the `__synthetic__`-suffixed `__file__` entry didn't exist on disk). * modified collection loader `get_code` to preserve the distinction between `None` (eg synthetic package) and empty string (eg empty `__init__.py`) values from `get_source`, and to return `None` when the source is `None`. This allows the package loader to skip `exec`ing things that truly have no source file on disk, thus not creating bogus coverage entries, while preserving behavior and coverage reporting for empty package inits that actually exist. * add unit test
This commit is contained in:
parent
74a103d655
commit
e813b0151c
3 changed files with 26 additions and 3 deletions
2
changelogs/fragments/fix_bogus_coverage.yml
Normal file
2
changelogs/fragments/fix_bogus_coverage.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
bugfixes:
|
||||||
|
- collection loader - fix bogus code coverage entries for synthetic packages
|
|
@ -355,7 +355,9 @@ class _AnsibleCollectionPkgLoaderBase:
|
||||||
|
|
||||||
with self._new_or_existing_module(fullname, **module_attrs) as module:
|
with self._new_or_existing_module(fullname, **module_attrs) as module:
|
||||||
# execute the module's code in its namespace
|
# execute the module's code in its namespace
|
||||||
exec(self.get_code(fullname), module.__dict__)
|
code_obj = self.get_code(fullname)
|
||||||
|
if code_obj is not None: # things like NS packages that can't have code on disk will return None
|
||||||
|
exec(code_obj, module.__dict__)
|
||||||
|
|
||||||
return module
|
return module
|
||||||
|
|
||||||
|
@ -428,8 +430,11 @@ class _AnsibleCollectionPkgLoaderBase:
|
||||||
filename = '<string>'
|
filename = '<string>'
|
||||||
|
|
||||||
source_code = self.get_source(fullname)
|
source_code = self.get_source(fullname)
|
||||||
if not source_code:
|
|
||||||
source_code = ''
|
# for things like synthetic modules that really have no source on disk, don't return a code object at all
|
||||||
|
# vs things like an empty package init (which has an empty string source on disk)
|
||||||
|
if source_code is None:
|
||||||
|
return None
|
||||||
|
|
||||||
self._compiled_code = compile(source=source_code, filename=filename, mode='exec', flags=0, dont_inherit=True)
|
self._compiled_code = compile(source=source_code, filename=filename, mode='exec', flags=0, dont_inherit=True)
|
||||||
|
|
||||||
|
|
|
@ -594,6 +594,22 @@ def test_bogus_imports():
|
||||||
import_module(bogus_import)
|
import_module(bogus_import)
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_vs_no_code():
|
||||||
|
finder = get_default_finder()
|
||||||
|
reset_collections_loader_state(finder)
|
||||||
|
|
||||||
|
from ansible_collections.testns import testcoll # synthetic package with no code on disk
|
||||||
|
from ansible_collections.testns.testcoll.plugins import module_utils # real package with empty code file
|
||||||
|
|
||||||
|
# ensure synthetic packages have no code object at all (prevent bogus coverage entries)
|
||||||
|
assert testcoll.__loader__.get_source(testcoll.__name__) is None
|
||||||
|
assert testcoll.__loader__.get_code(testcoll.__name__) is None
|
||||||
|
|
||||||
|
# ensure empty package inits do have a code object
|
||||||
|
assert module_utils.__loader__.get_source(module_utils.__name__) == b''
|
||||||
|
assert module_utils.__loader__.get_code(module_utils.__name__) is not None
|
||||||
|
|
||||||
|
|
||||||
def test_finder_playbook_paths():
|
def test_finder_playbook_paths():
|
||||||
finder = get_default_finder()
|
finder = get_default_finder()
|
||||||
reset_collections_loader_state(finder)
|
reset_collections_loader_state(finder)
|
||||||
|
|
Loading…
Reference in a new issue