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
pull/71769/head
Matt Davis 4 years ago committed by GitHub
parent 74a103d655
commit e813b0151c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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…
Cancel
Save