|
| 1 | +""" |
| 2 | +Code to enable coverage of any external code called by the |
| 3 | +notebook. |
| 4 | +""" |
| 5 | + |
| 6 | +import os |
| 7 | +import coverage |
| 8 | +import warnings |
| 9 | + |
| 10 | + |
| 11 | +# Coverage setup/teardown code to run in kernel |
| 12 | +# Inspired by pytest-cov code. |
| 13 | +_python_setup = """\ |
| 14 | +import coverage |
| 15 | +
|
| 16 | +__cov = coverage.Coverage( |
| 17 | + data_file=%r, |
| 18 | + source=%r, |
| 19 | + config_file=%r, |
| 20 | + auto_data=True, |
| 21 | + data_suffix='nbval', |
| 22 | + ) |
| 23 | +__cov.load() |
| 24 | +__cov.start() |
| 25 | +__cov._warn_no_data = False |
| 26 | +__cov._warn_unimported_source = False |
| 27 | +""" |
| 28 | +_python_teardown = """\ |
| 29 | +__cov.stop() |
| 30 | +__cov.save() |
| 31 | +""" |
| 32 | + |
| 33 | + |
| 34 | +def setup_coverage(config, kernel, floc, output_loc=None): |
| 35 | + """Start coverage reporting in kernel. |
| 36 | +
|
| 37 | + Currently supported kernel languages are: |
| 38 | + - Python |
| 39 | + """ |
| 40 | + |
| 41 | + language = kernel.language |
| 42 | + if language.startswith('python'): |
| 43 | + # Get the pytest-cov coverage object |
| 44 | + cov = get_cov(config) |
| 45 | + if cov: |
| 46 | + # If present, copy the data file location used by pytest-cov |
| 47 | + data_file = os.path.abspath(cov.config.data_file) |
| 48 | + else: |
| 49 | + # Fall back on output_loc and current dir if not |
| 50 | + data_file = os.path.abspath(os.path.join(output_loc or os.getcwd(), '.coverage')) |
| 51 | + |
| 52 | + # Get options from pytest-cov's command line arguments: |
| 53 | + source = config.option.cov_source |
| 54 | + config_file = config.option.cov_config |
| 55 | + if isinstance(config_file, str) and os.path.isfile(config_file): |
| 56 | + config_file = os.path.abspath(config_file) |
| 57 | + |
| 58 | + # Build setup command and execute in kernel: |
| 59 | + cmd = _python_setup % (data_file, source, config_file) |
| 60 | + msg_id = kernel.kc.execute(cmd, stop_on_error=False) |
| 61 | + kernel.await_idle(msg_id, 60) # A minute should be plenty to enable coverage |
| 62 | + else: |
| 63 | + warnings.warn_explicit( |
| 64 | + 'Coverage currently not supported for language %r.' % language, |
| 65 | + category=UserWarning, |
| 66 | + filename=floc[0] if floc else '', |
| 67 | + lineno=0 |
| 68 | + ) |
| 69 | + return |
| 70 | + |
| 71 | + |
| 72 | +def teardown_coverage(config, kernel, output_loc=None): |
| 73 | + """Finish coverage reporting in kernel. |
| 74 | +
|
| 75 | + The coverage should previously have been started with |
| 76 | + setup_coverage. |
| 77 | + """ |
| 78 | + language = kernel.language |
| 79 | + if language.startswith('python'): |
| 80 | + # Teardown code does not require any input, simply execute: |
| 81 | + msg_id = kernel.kc.execute(_python_teardown) |
| 82 | + kernel.await_idle(msg_id, 60) # A minute should be plenty to write out coverage |
| 83 | + |
| 84 | + # Ensure we merge our data into parent data of pytest-cov, if possible |
| 85 | + cov = get_cov(config) |
| 86 | + _merge_nbval_coverage_data(cov) |
| 87 | + |
| 88 | + else: |
| 89 | + # Warnings should be given on setup, or there might be no teardown |
| 90 | + # for a specific language, so do nothing here |
| 91 | + pass |
| 92 | + |
| 93 | + |
| 94 | +def get_cov(config): |
| 95 | + """Returns the coverage object of pytest-cov.""" |
| 96 | + |
| 97 | + # Check with hasplugin to avoid getplugin exception in older pytest. |
| 98 | + if config.pluginmanager.hasplugin('_cov'): |
| 99 | + plugin = config.pluginmanager.getplugin('_cov') |
| 100 | + if plugin.cov_controller: |
| 101 | + return plugin.cov_controller.cov |
| 102 | + return None |
| 103 | + |
| 104 | +def _merge_nbval_coverage_data(cov): |
| 105 | + """Merge nbval coverage data into pytest-cov data.""" |
| 106 | + if not cov: |
| 107 | + return |
| 108 | + |
| 109 | + if coverage.version_info > (5, 0): |
| 110 | + data = cov.get_data() |
| 111 | + nbval_data = coverage.CoverageData(data.data_filename(), suffix='.nbval', debug=cov.debug) |
| 112 | + nbval_data.read() |
| 113 | + cov.get_data().update(nbval_data, aliases=aliases) |
| 114 | + else: |
| 115 | + # Get the filename of the nbval coverage: |
| 116 | + filename = cov.data_files.filename + '.nbval' |
| 117 | + |
| 118 | + # Read coverage generated by nbval in this run: |
| 119 | + nbval_data = coverage.CoverageData(debug=cov.debug) |
| 120 | + try: |
| 121 | + nbval_data.read_file(os.path.abspath(filename)) |
| 122 | + except coverage.CoverageException: |
| 123 | + return |
| 124 | + |
| 125 | + # Set up aliases (following internal coverage.py code here) |
| 126 | + aliases = None |
| 127 | + if cov.config.paths: |
| 128 | + aliases = coverage.files.PathAliases() |
| 129 | + for paths in cov.config.paths.values(): |
| 130 | + result = paths[0] |
| 131 | + for pattern in paths[1:]: |
| 132 | + aliases.add(pattern, result) |
| 133 | + |
| 134 | + # Merge nbval data into pytest-cov data: |
| 135 | + cov.data.update(nbval_data, aliases=aliases) |
| 136 | + # Delete our nbval coverage data |
| 137 | + coverage.misc.file_be_gone(filename) |
| 138 | + |
| 139 | + |
| 140 | +""" |
| 141 | +Note about coverage data/datafiles: |
| 142 | +
|
| 143 | +When pytest is running, we get the pytest-cov coverage object. |
| 144 | +This object tracks its own coverage data, which is stored in its |
| 145 | +data file. For several reasons detailed below, we cannot use the |
| 146 | +same file in the kernel, so we have to ensure our own, and then |
| 147 | +ensure that they are all merged correctly at the end. The important |
| 148 | +factor here is the data_suffix attribute which might be set. |
| 149 | +
|
| 150 | +Cases: |
| 151 | +1. data_suffix is set to None: |
| 152 | + No suffix is used by pytest-cov. We need to create a new file, |
| 153 | + so we add a suffix for kernel, and then merge this file into |
| 154 | + the pytest-cov data at teardown. |
| 155 | +2. data_suffix is set to a string: |
| 156 | + We need to create a new file, so we append a string to the |
| 157 | + suffix passed to the kernel. We merge this file into the |
| 158 | + pytest-cov data at teardown. |
| 159 | +3. data_suffix is set to True: |
| 160 | + The suffix will be autogenerated by coverage.py, along the lines |
| 161 | + of 'hostname.pid.random'. This is typically used for parallel |
| 162 | + tests. We pass True as suffix to kernel, ensuring a unique |
| 163 | + auto-suffix later. We cannot merge this data into the pytest-cov |
| 164 | + one, as we do not know the suffix, but we can just leave the data |
| 165 | + for automatic collection. However, this might lead to a warning |
| 166 | + about no coverage data being collected by the pytest-cov |
| 167 | + collector. |
| 168 | +
|
| 169 | +Why do we need our own coverage data file? |
| 170 | +Coverage data can get lost if we try to sync via load/save/load cycles |
| 171 | +between the two. By having our own file, we can do an in-memory merge |
| 172 | +of the data afterwards using the official API. Either way, the data |
| 173 | +will always be merged to one coverage file in the end, so these files |
| 174 | +are transient. |
| 175 | +""" |
0 commit comments