163 self.all_files_nums = [] |
172 self.all_files_nums = [] |
164 self.incr = IncrementalChecker(self.directory) |
173 self.incr = IncrementalChecker(self.directory) |
165 self.datagen = HtmlDataGeneration(self.coverage) |
174 self.datagen = HtmlDataGeneration(self.coverage) |
166 self.totals = Numbers(precision=self.config.precision) |
175 self.totals = Numbers(precision=self.config.precision) |
167 self.directory_was_empty = False |
176 self.directory_was_empty = False |
|
177 self.first_fr = None |
|
178 self.final_fr = None |
168 |
179 |
169 self.template_globals = { |
180 self.template_globals = { |
170 # Functions available in the templates. |
181 # Functions available in the templates. |
171 'escape': escape, |
182 'escape': escape, |
172 'pair': pair, |
183 'pair': pair, |
186 'category': { |
197 'category': { |
187 'exc': 'exc show_exc', |
198 'exc': 'exc show_exc', |
188 'mis': 'mis show_mis', |
199 'mis': 'mis show_mis', |
189 'par': 'par run show_par', |
200 'par': 'par run show_par', |
190 'run': 'run', |
201 'run': 'run', |
191 } |
202 }, |
192 } |
203 } |
193 self.pyfile_html_source = read_data("pyfile.html") |
204 self.pyfile_html_source = read_data("pyfile.html") |
194 self.source_tmpl = Templite(self.pyfile_html_source, self.template_globals) |
205 self.source_tmpl = Templite(self.pyfile_html_source, self.template_globals) |
195 |
206 |
196 def report(self, morfs): |
207 def report(self, morfs): |
202 # Read the status data and check that this run used the same |
213 # Read the status data and check that this run used the same |
203 # global data as the last run. |
214 # global data as the last run. |
204 self.incr.read() |
215 self.incr.read() |
205 self.incr.check_global_data(self.config, self.pyfile_html_source) |
216 self.incr.check_global_data(self.config, self.pyfile_html_source) |
206 |
217 |
207 # Process all the files. |
218 # Process all the files. For each page we need to supply a link |
|
219 # to the next and previous page. |
|
220 files_to_report = [] |
|
221 |
208 for fr, analysis in get_analysis_to_report(self.coverage, morfs): |
222 for fr, analysis in get_analysis_to_report(self.coverage, morfs): |
209 self.html_file(fr, analysis) |
223 ftr = FileToReport(fr, analysis) |
|
224 should = self.should_report_file(ftr) |
|
225 if should: |
|
226 files_to_report.append(ftr) |
|
227 else: |
|
228 file_be_gone(os.path.join(self.directory, ftr.html_filename)) |
|
229 |
|
230 for i, ftr in enumerate(files_to_report): |
|
231 if i == 0: |
|
232 prev_html = "index.html" |
|
233 else: |
|
234 prev_html = files_to_report[i - 1].html_filename |
|
235 if i == len(files_to_report) - 1: |
|
236 next_html = "index.html" |
|
237 else: |
|
238 next_html = files_to_report[i + 1].html_filename |
|
239 self.write_html_file(ftr, prev_html, next_html) |
210 |
240 |
211 if not self.all_files_nums: |
241 if not self.all_files_nums: |
212 raise NoDataError("No data to report.") |
242 raise NoDataError("No data to report.") |
213 |
243 |
214 self.totals = sum(self.all_files_nums) |
244 self.totals = sum(self.all_files_nums) |
215 |
245 |
216 # Write the index file. |
246 # Write the index file. |
217 self.index_file() |
247 if files_to_report: |
|
248 first_html = files_to_report[0].html_filename |
|
249 final_html = files_to_report[-1].html_filename |
|
250 else: |
|
251 first_html = final_html = "index.html" |
|
252 self.index_file(first_html, final_html) |
218 |
253 |
219 self.make_local_static_report_files() |
254 self.make_local_static_report_files() |
220 return self.totals.n_statements and self.totals.pc_covered |
255 return self.totals.n_statements and self.totals.pc_covered |
|
256 |
|
257 def make_directory(self): |
|
258 """Make sure our htmlcov directory exists.""" |
|
259 ensure_dir(self.directory) |
|
260 if not os.listdir(self.directory): |
|
261 self.directory_was_empty = True |
221 |
262 |
222 def make_local_static_report_files(self): |
263 def make_local_static_report_files(self): |
223 """Make local instances of static files for HTML report.""" |
264 """Make local instances of static files for HTML report.""" |
224 # The files we provide must always be copied. |
265 # The files we provide must always be copied. |
225 for static in self.STATIC_FILES: |
266 for static in self.STATIC_FILES: |
234 |
275 |
235 # The user may have extra CSS they want copied. |
276 # The user may have extra CSS they want copied. |
236 if self.extra_css: |
277 if self.extra_css: |
237 shutil.copyfile(self.config.extra_css, os.path.join(self.directory, self.extra_css)) |
278 shutil.copyfile(self.config.extra_css, os.path.join(self.directory, self.extra_css)) |
238 |
279 |
239 def html_file(self, fr, analysis): |
280 def should_report_file(self, ftr): |
240 """Generate an HTML file for one source file.""" |
281 """Determine if we'll report this file.""" |
241 rootname = flat_rootname(fr.relative_filename()) |
|
242 html_filename = rootname + ".html" |
|
243 ensure_dir(self.directory) |
|
244 if not os.listdir(self.directory): |
|
245 self.directory_was_empty = True |
|
246 html_path = os.path.join(self.directory, html_filename) |
|
247 |
|
248 # Get the numbers for this file. |
282 # Get the numbers for this file. |
249 nums = analysis.numbers |
283 nums = ftr.analysis.numbers |
250 self.all_files_nums.append(nums) |
284 self.all_files_nums.append(nums) |
251 |
285 |
252 if self.skip_covered: |
286 if self.skip_covered: |
253 # Don't report on 100% files. |
287 # Don't report on 100% files. |
254 no_missing_lines = (nums.n_missing == 0) |
288 no_missing_lines = (nums.n_missing == 0) |
255 no_missing_branches = (nums.n_partial_branches == 0) |
289 no_missing_branches = (nums.n_partial_branches == 0) |
256 if no_missing_lines and no_missing_branches: |
290 if no_missing_lines and no_missing_branches: |
257 # If there's an existing file, remove it. |
291 # If there's an existing file, remove it. |
258 file_be_gone(html_path) |
|
259 self.skipped_covered_count += 1 |
292 self.skipped_covered_count += 1 |
260 return |
293 return False |
261 |
294 |
262 if self.skip_empty: |
295 if self.skip_empty: |
263 # Don't report on empty files. |
296 # Don't report on empty files. |
264 if nums.n_statements == 0: |
297 if nums.n_statements == 0: |
265 file_be_gone(html_path) |
|
266 self.skipped_empty_count += 1 |
298 self.skipped_empty_count += 1 |
267 return |
299 return False |
|
300 |
|
301 return True |
|
302 |
|
303 def write_html_file(self, ftr, prev_html, next_html): |
|
304 """Generate an HTML file for one source file.""" |
|
305 self.make_directory() |
268 |
306 |
269 # Find out if the file on disk is already correct. |
307 # Find out if the file on disk is already correct. |
270 if self.incr.can_skip_file(self.data, fr, rootname): |
308 if self.incr.can_skip_file(self.data, ftr.fr, ftr.rootname): |
271 self.file_summaries.append(self.incr.index_info(rootname)) |
309 self.file_summaries.append(self.incr.index_info(ftr.rootname)) |
272 return |
310 return |
273 |
311 |
274 # Write the HTML page for this file. |
312 # Write the HTML page for this file. |
275 file_data = self.datagen.data_for_file(fr, analysis) |
313 file_data = self.datagen.data_for_file(ftr.fr, ftr.analysis) |
276 for ldata in file_data.lines: |
314 for ldata in file_data.lines: |
277 # Build the HTML for the line. |
315 # Build the HTML for the line. |
278 html = [] |
316 html = [] |
279 for tok_type, tok_text in ldata.tokens: |
317 for tok_type, tok_text in ldata.tokens: |
280 if tok_type == "ws": |
318 if tok_type == "ws": |
290 # 202F is NARROW NO-BREAK SPACE. |
328 # 202F is NARROW NO-BREAK SPACE. |
291 # 219B is RIGHTWARDS ARROW WITH STROKE. |
329 # 219B is RIGHTWARDS ARROW WITH STROKE. |
292 ldata.annotate = ", ".join( |
330 ldata.annotate = ", ".join( |
293 f"{ldata.number} ↛ {d}" |
331 f"{ldata.number} ↛ {d}" |
294 for d in ldata.short_annotations |
332 for d in ldata.short_annotations |
295 ) |
333 ) |
296 else: |
334 else: |
297 ldata.annotate = None |
335 ldata.annotate = None |
298 |
336 |
299 if ldata.long_annotations: |
337 if ldata.long_annotations: |
300 longs = ldata.long_annotations |
338 longs = ldata.long_annotations |
304 ldata.annotate_long = "{:d} missed branches: {}".format( |
342 ldata.annotate_long = "{:d} missed branches: {}".format( |
305 len(longs), |
343 len(longs), |
306 ", ".join( |
344 ", ".join( |
307 f"{num:d}) {ann_long}" |
345 f"{num:d}) {ann_long}" |
308 for num, ann_long in enumerate(longs, start=1) |
346 for num, ann_long in enumerate(longs, start=1) |
309 ), |
347 ), |
310 ) |
348 ) |
311 else: |
349 else: |
312 ldata.annotate_long = None |
350 ldata.annotate_long = None |
313 |
351 |
314 css_classes = [] |
352 css_classes = [] |
315 if ldata.category: |
353 if ldata.category: |
316 css_classes.append(self.template_globals['category'][ldata.category]) |
354 css_classes.append(self.template_globals['category'][ldata.category]) |
317 ldata.css_class = ' '.join(css_classes) or "pln" |
355 ldata.css_class = ' '.join(css_classes) or "pln" |
318 |
356 |
319 html = self.source_tmpl.render(file_data.__dict__) |
357 html_path = os.path.join(self.directory, ftr.html_filename) |
|
358 html = self.source_tmpl.render({ |
|
359 **file_data.__dict__, |
|
360 'prev_html': prev_html, |
|
361 'next_html': next_html, |
|
362 }) |
320 write_html(html_path, html) |
363 write_html(html_path, html) |
321 |
364 |
322 # Save this file's information for the index file. |
365 # Save this file's information for the index file. |
323 index_info = { |
366 index_info = { |
324 'nums': nums, |
367 'nums': ftr.analysis.numbers, |
325 'html_filename': html_filename, |
368 'html_filename': ftr.html_filename, |
326 'relative_filename': fr.relative_filename(), |
369 'relative_filename': ftr.fr.relative_filename(), |
327 } |
370 } |
328 self.file_summaries.append(index_info) |
371 self.file_summaries.append(index_info) |
329 self.incr.set_index_info(rootname, index_info) |
372 self.incr.set_index_info(ftr.rootname, index_info) |
330 |
373 |
331 def index_file(self): |
374 def index_file(self, first_html, final_html): |
332 """Write the index.html file for this report.""" |
375 """Write the index.html file for this report.""" |
|
376 self.make_directory() |
333 index_tmpl = Templite(read_data("index.html"), self.template_globals) |
377 index_tmpl = Templite(read_data("index.html"), self.template_globals) |
334 |
378 |
335 skipped_covered_msg = skipped_empty_msg = "" |
379 skipped_covered_msg = skipped_empty_msg = "" |
336 if self.skipped_covered_count: |
380 if self.skipped_covered_count: |
337 n = self.skipped_covered_count |
381 n = self.skipped_covered_count |
343 html = index_tmpl.render({ |
387 html = index_tmpl.render({ |
344 'files': self.file_summaries, |
388 'files': self.file_summaries, |
345 'totals': self.totals, |
389 'totals': self.totals, |
346 'skipped_covered_msg': skipped_covered_msg, |
390 'skipped_covered_msg': skipped_covered_msg, |
347 'skipped_empty_msg': skipped_empty_msg, |
391 'skipped_empty_msg': skipped_empty_msg, |
|
392 'first_html': first_html, |
|
393 'final_html': final_html, |
348 }) |
394 }) |
349 |
395 |
350 index_file = os.path.join(self.directory, "index.html") |
396 index_file = os.path.join(self.directory, "index.html") |
351 write_html(index_file, html) |
397 write_html(index_file, html) |
352 self.coverage._message(f"Wrote HTML report to {index_file}") |
398 self.coverage._message(f"Wrote HTML report to {index_file}") |