Coverage for tests / test_html.py: 100.000%
682 statements
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
4"""Tests that HTML generation is awesome."""
6from __future__ import annotations 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
8import collections 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
9import datetime 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
10import glob 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
11import json 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
12import os 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
13import os.path 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
14import re 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
15import sys 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
17from html.parser import HTMLParser 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
18from typing import Any, IO 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
19from unittest import mock 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
21import pytest 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
23import coverage 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
24import coverage.html 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
25from coverage import env, Coverage 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
26from coverage.exceptions import NoDataError, NotPython, NoSource 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
27from coverage.files import abs_file, flat_rootname 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
28from coverage.report_core import get_analysis_to_report 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
29from coverage.types import TLineNo, TMorf 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
31from tests import testenv 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
32from tests.coveragetest import CoverageTest, TESTS_DIR 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
33from tests.goldtest import gold_path 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
34from tests.goldtest import compare, contains, contains_rx, doesnt_contain, contains_any 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
35from tests.helpers import assert_coverage_warnings, change_dir 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
38class HtmlTestHelpers(CoverageTest): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
39 """Methods that help with HTML tests."""
41 def create_initial_files(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
42 """Create the source files we need to run these tests."""
43 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
44 "main_file.py",
45 """\
46 import helper1, helper2
47 helper1.func1(12)
48 helper2.func2(12)
49 """,
50 )
51 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
52 "helper1.py",
53 """\
54 def func1(x):
55 if x % 2:
56 print("odd")
57 """,
58 )
59 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
60 "helper2.py",
61 """\
62 def func2(x):
63 print("x is %d" % x)
64 """,
65 )
67 def run_coverage( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8
68 self,
69 covargs: dict[str, Any] | None = None,
70 htmlargs: dict[str, Any] | None = None,
71 ) -> float:
72 """Run coverage.py on main_file.py, and create an HTML report."""
73 self.clean_local_file_imports() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
74 cov = coverage.Coverage(**(covargs or {})) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
75 self.start_import_stop(cov, "main_file") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
76 ret = cov.html_report(**(htmlargs or {})) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
77 self.assert_valid_hrefs() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
78 return ret 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
80 def get_html_report_content(self, module: str) -> str: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
81 """Return the content of the HTML report for `module`."""
82 filename = flat_rootname(module) + ".html" 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
83 filename = os.path.join("htmlcov", filename) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
84 with open(filename, encoding="utf-8") as f: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
85 return f.read() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
87 def get_html_index_content(self) -> str: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
88 """Return the content of index.html.
90 Time stamps are replaced with a placeholder so that clocks don't matter.
92 """
93 with open("htmlcov/index.html", encoding="utf-8") as f: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
94 index = f.read() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
95 index = re.sub( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
96 r"created at \d{4}-\d{2}-\d{2} \d{2}:\d{2} \+\d{4}",
97 r"created at YYYY-MM-DD HH:MM +ZZZZ",
98 index,
99 )
100 index = re.sub( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
101 r"created at \d{4}-\d{2}-\d{2} \d{2}:\d{2}",
102 r"created at YYYY-MM-DD HH:MM",
103 index,
104 )
105 return index 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
107 def get_html_report_text_lines(self, module: str) -> list[str]: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
108 """Parse the HTML report, and return a list of strings, the text rendered."""
109 parser = HtmlReportParser() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
110 parser.feed(self.get_html_report_content(module)) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
111 return parser.text() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
113 def assert_correct_timestamp(self, html: str) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
114 """Extract the time stamp from `html`, and assert it is recent."""
115 timestamp_pat = r"created at (\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})" 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
116 m = re.search(timestamp_pat, html) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
117 assert m, "Didn't find a time stamp!" 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
118 timestamp = datetime.datetime(*[int(v) for v in m.groups()]) # type: ignore[arg-type] 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
119 # The time stamp only records the minute, so the delta could be from
120 # 12:00 to 12:01:59, or two minutes.
121 self.assert_recent_datetime( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
122 timestamp,
123 seconds=120,
124 msg=f"Time stamp is wrong: {timestamp}",
125 )
127 def assert_valid_hrefs(self, directory: str = "htmlcov") -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
128 """Assert that the hrefs in htmlcov/*.html are valid.
130 Doesn't check external links (those with a protocol).
131 """
132 hrefs = collections.defaultdict(set) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
133 for fname in glob.glob(f"{directory}/*.html"): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
134 with open(fname, encoding="utf-8") as fhtml: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
135 html = fhtml.read() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
136 for href in re.findall(r""" href=['"]([^'"]*)['"]""", html): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
137 if href.startswith("#"): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
138 assert re.search(rf""" id=['"]{href[1:]}['"]""", html), ( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
139 f"Fragment {href!r} in {fname} has no anchor"
140 )
141 continue 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
142 if "://" in href: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
143 continue 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
144 href = href.partition("#")[0] # ignore fragment in URLs. 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
145 hrefs[href].add(fname) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
146 for href, sources in hrefs.items(): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
147 assert os.path.exists(f"{directory}/{href}"), ( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
148 f"These files link to {href!r}, which doesn't exist: {', '.join(sources)}"
149 )
152class HtmlReportParser(HTMLParser): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
153 """An HTML parser for our HTML reports.
155 Assertions are made about the structure we expect.
156 """
158 def __init__(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
159 super().__init__() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
160 self.lines: list[list[str]] = [] 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
161 self.in_source = False 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
163 def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
164 if tag == "main": 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
165 assert attrs == [("id", "source")] 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
166 self.in_source = True 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
167 elif self.in_source and tag == "a": 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
168 dattrs = dict(attrs) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
169 assert "id" in dattrs 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
170 ida = dattrs["id"] 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
171 assert ida is not None 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
172 assert ida[0] == "t" 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
173 line_no = int(ida[1:]) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
174 self.lines.append([]) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
175 assert line_no == len(self.lines) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
177 def handle_endtag(self, tag: str) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
178 if tag == "main": 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
179 self.in_source = False 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
181 def handle_data(self, data: str) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
182 if self.in_source and self.lines: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
183 self.lines[-1].append(data) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
185 def text(self) -> list[str]: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
186 """Get the rendered text as a list of strings, one per line."""
187 return ["".join(l).rstrip() for l in self.lines] 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
190class FileWriteTracker: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
191 """A fake object to track how `open` is used to write files."""
193 def __init__(self, written: set[str]) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
194 self.written = written 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
196 def open(self, filename: str, mode: str = "r", encoding: str | None = None) -> IO[str]: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
197 """Be just like `open`, but write written file names to `self.written`."""
198 if mode.startswith("w"): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
199 self.written.add(filename.replace("\\", "/")) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
200 return open(filename, mode, encoding=encoding) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
203class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
204 """Tests of the HTML delta speed-ups."""
206 def setUp(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
207 super().setUp() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
209 # At least one of our tests monkey-patches the version of coverage.py,
210 # so grab it here to restore it later.
211 self.real_coverage_version = coverage.__version__ 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
212 self.addCleanup(setattr, coverage, "__version__", self.real_coverage_version) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
214 self.files_written: set[str] 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
216 def run_coverage( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8
217 self,
218 covargs: dict[str, Any] | None = None,
219 htmlargs: dict[str, Any] | None = None,
220 ) -> float:
221 """Run coverage in-process for the delta tests.
223 For the delta tests, we always want `source=.` and we want to track
224 which files are written. `self.files_written` will be the file names
225 that were opened for writing in html.py.
227 """
228 covargs = covargs or {} 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
229 covargs["source"] = "." 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
230 self.files_written = set() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
231 mock_open = FileWriteTracker(self.files_written).open 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
232 with mock.patch("coverage.html.open", mock_open): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
233 return super().run_coverage(covargs=covargs, htmlargs=htmlargs) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
235 def assert_htmlcov_files_exist(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
236 """Assert that all the expected htmlcov files exist."""
237 self.assert_exists("htmlcov/index.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
238 self.assert_exists("htmlcov/function_index.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
239 self.assert_exists("htmlcov/class_index.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
240 self.assert_exists("htmlcov/main_file_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
241 self.assert_exists("htmlcov/helper1_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
242 self.assert_exists("htmlcov/helper2_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
243 self.assert_exists("htmlcov/.gitignore") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
244 # Cache-busted files have random data in the name, but they should all
245 # be there, and there should only be one of each.
246 statics = ["style.css", "coverage_html.js", "keybd_closed.png", "favicon_32.png"] 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
247 files = os.listdir("htmlcov") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
248 for static in statics: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
249 base, ext = os.path.splitext(static) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
250 busted_file_pattern = rf"{base}_cb_\w{{8}}{ext}" 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
251 matches = [m for f in files if (m := re.fullmatch(busted_file_pattern, f))] 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
252 assert len(matches) == 1, f"Found {len(matches)} files for {static}" 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
254 def test_html_created(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
255 # Test basic HTML generation: files should be created.
256 self.create_initial_files() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
257 self.run_coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
258 self.assert_htmlcov_files_exist() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
260 def test_html_delta_from_source_change(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
261 # HTML generation can create only the files that have changed.
262 # In this case, helper1 changes because its source is different.
263 self.create_initial_files() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
264 self.run_coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
265 index1 = self.get_html_index_content() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
267 # Now change a file (but only in a comment) and do it again.
268 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
269 "helper1.py",
270 """\
271 def func1(x): # A nice function
272 if x % 2:
273 print("odd")
274 """,
275 )
277 self.run_coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
279 # Only the changed files should have been created.
280 self.assert_htmlcov_files_exist() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
281 assert "htmlcov/index.html" in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
282 assert "htmlcov/helper1_py.html" in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
283 assert "htmlcov/helper2_py.html" not in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
284 assert "htmlcov/main_file_py.html" not in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
286 # Because the source change was only a comment, the index is the same.
287 index2 = self.get_html_index_content() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
288 assert index1 == index2 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
290 def test_html_delta_from_coverage_change(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
291 # HTML generation can create only the files that have changed.
292 # In this case, helper1 changes because its coverage is different.
293 self.create_initial_files() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
294 self.run_coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
296 # Now change a file and do it again. main_file is different, and calls
297 # helper1 differently.
298 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
299 "main_file.py",
300 """\
301 import helper1, helper2
302 helper1.func1(23)
303 helper2.func2(23)
304 """,
305 )
307 self.run_coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
309 # Only the changed files should have been created.
310 self.assert_htmlcov_files_exist() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
311 assert "htmlcov/index.html" in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
312 assert "htmlcov/helper1_py.html" in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
313 assert "htmlcov/helper2_py.html" not in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
314 assert "htmlcov/main_file_py.html" in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
316 def test_html_delta_from_settings_change(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
317 # HTML generation can create only the files that have changed.
318 # In this case, everything changes because the coverage.py settings
319 # have changed.
320 self.create_initial_files() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
321 self.run_coverage(covargs=dict(omit=[])) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
322 index1 = self.get_html_index_content() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
324 self.run_coverage(covargs=dict(omit=["xyzzy*"])) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
326 # All the files have been reported again.
327 self.assert_htmlcov_files_exist() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
328 assert "htmlcov/index.html" in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
329 assert "htmlcov/helper1_py.html" in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
330 assert "htmlcov/helper2_py.html" in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
331 assert "htmlcov/main_file_py.html" in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
333 index2 = self.get_html_index_content() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
334 assert index1 == index2 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
336 def test_html_delta_from_coverage_version_change(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
337 # HTML generation can create only the files that have changed.
338 # In this case, everything changes because the coverage.py version has
339 # changed.
340 self.create_initial_files() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
341 self.run_coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
342 index1 = self.get_html_index_content() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
344 # "Upgrade" coverage.py!
345 coverage.__version__ = "XYZZY" 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
347 self.run_coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
349 # All the files have been reported again.
350 self.assert_htmlcov_files_exist() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
351 assert "htmlcov/index.html" in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
352 assert "htmlcov/helper1_py.html" in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
353 assert "htmlcov/helper2_py.html" in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
354 assert "htmlcov/main_file_py.html" in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
356 index2 = self.get_html_index_content() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
357 fixed_index2 = index2.replace("XYZZY", self.real_coverage_version) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
358 assert index1 == fixed_index2 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
360 def test_file_becomes_100(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
361 self.create_initial_files() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
362 self.run_coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
364 # Now change a file and do it again
365 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
366 "main_file.py",
367 """\
368 import helper1, helper2
369 # helper1 is now 100%
370 helper1.func1(12)
371 helper1.func1(23)
372 """,
373 )
375 self.run_coverage(htmlargs=dict(skip_covered=True)) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
377 # The 100% file, skipped, shouldn't be here.
378 self.assert_doesnt_exist("htmlcov/helper1_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
380 def test_status_format_change(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
381 self.create_initial_files() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
382 self.run_coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
384 with open("htmlcov/status.json", encoding="utf-8") as status_json: 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
385 status_data = json.load(status_json) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
387 assert status_data["format"] == 5 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
388 status_data["format"] = 99 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
389 with open("htmlcov/status.json", "w", encoding="utf-8") as status_json: 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
390 json.dump(status_data, status_json) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
392 self.run_coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
394 # All the files have been reported again.
395 self.assert_htmlcov_files_exist() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
396 assert "htmlcov/index.html" in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
397 assert "htmlcov/helper1_py.html" in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
398 assert "htmlcov/helper2_py.html" in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
399 assert "htmlcov/main_file_py.html" in self.files_written 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
401 def test_dont_overwrite_gitignore(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
402 self.create_initial_files() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
403 self.make_file("htmlcov/.gitignore", "# ignore nothing") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
404 self.run_coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
405 with open("htmlcov/.gitignore", encoding="utf-8") as fgi: 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
406 assert fgi.read() == "# ignore nothing" 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
408 def test_dont_write_gitignore_into_existing_directory(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
409 self.create_initial_files() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
410 self.make_file("htmlcov/README", "My files: don't touch!") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
411 self.run_coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
412 self.assert_doesnt_exist("htmlcov/.gitignore") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
413 self.assert_exists("htmlcov/index.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
416class HtmlTitleTest(HtmlTestHelpers, CoverageTest): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
417 """Tests of the HTML title support."""
419 def test_default_title(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
420 self.create_initial_files() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
421 self.run_coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
422 index = self.get_html_index_content() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
423 assert "<title>Coverage report</title>" in index 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
424 assert "<h1>Coverage report:" in index 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
426 def test_title_set_in_config_file(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
427 self.create_initial_files() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
428 self.make_file(".coveragerc", "[html]\ntitle = Metrics & stuff!\n") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
429 self.run_coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
430 index = self.get_html_index_content() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
431 assert "<title>Metrics & stuff!</title>" in index 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
432 assert "<h1>Metrics & stuff!:" in index 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
434 def test_non_ascii_title_set_in_config_file(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
435 self.create_initial_files() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
436 self.make_file(".coveragerc", "[html]\ntitle = «ταБЬℓσ» numbers") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
437 self.run_coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
438 index = self.get_html_index_content() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
439 assert "<title>«ταБЬℓσ» numbers" in index 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
440 assert "<h1>«ταБЬℓσ» numbers" in index 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
442 def test_title_set_in_args(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
443 self.create_initial_files() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
444 self.make_file(".coveragerc", "[html]\ntitle = Good title\n") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
445 self.run_coverage(htmlargs=dict(title="«ταБЬℓσ» & stüff!")) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
446 index = self.get_html_index_content() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
447 expected = ( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
448 "<title>«ταБЬℓσ» "
449 + "& stüff!</title>"
450 )
451 assert expected in index 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
452 assert "<h1>«ταБЬℓσ» & stüff!:" in index 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
455class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
456 """Test the behavior when measuring unparsable files."""
458 def test_dotpy_not_python(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
459 self.make_file("main.py", "import innocuous") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
460 self.make_file("innocuous.py", "a = 1") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
461 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
462 self.start_import_stop(cov, "main") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
463 self.make_file("innocuous.py", "<h1>This isn't python!</h1>") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
464 msg = "Couldn't parse '.*innocuous.py' as Python source: .* at line 1" 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
465 with pytest.raises(NotPython, match=msg): 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
466 cov.html_report() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
468 def test_dotpy_not_python_ignored(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
469 self.make_file("main.py", "import innocuous") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
470 self.make_file("innocuous.py", "a = 2") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
471 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
472 self.start_import_stop(cov, "main") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
474 self.make_file("innocuous.py", "<h1>This isn't python!</h1>") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
475 with pytest.warns(Warning) as warns: 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
476 cov.html_report(ignore_errors=True) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
477 assert_coverage_warnings( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
478 warns,
479 re.compile(r"Couldn't parse Python file '.*innocuous.py' \(couldnt-parse\)"),
480 )
481 self.assert_exists("htmlcov/index.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
482 # This would be better as a glob, if the HTML layout changes:
483 self.assert_doesnt_exist("htmlcov/innocuous.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
485 def test_dothtml_not_python(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
486 # Run an "HTML" file
487 self.make_file("innocuous.html", "a = 3") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
488 self.make_data_file(lines={abs_file("innocuous.html"): [1]}) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
489 # Before reporting, change it to be an HTML file.
490 self.make_file("innocuous.html", "<h1>This isn't python at all!</h1>") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
491 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
492 cov.load() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
493 with pytest.raises(NoDataError, match="No data to report."): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
494 cov.html_report() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
496 def test_execed_liar_ignored(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
497 # Jinja2 sets __file__ to be a non-Python file, and then execs code.
498 # If that file contains non-Python code, a TokenError shouldn't
499 # have been raised when writing the HTML report.
500 source = "exec(compile('','','exec'), {'__file__': 'liar.html'})" 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
501 self.make_file("liar.py", source) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
502 self.make_file("liar.html", "{# Whoops, not python code #}") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
503 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
504 self.start_import_stop(cov, "liar") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
505 cov.html_report() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
506 self.assert_exists("htmlcov/index.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
508 def test_execed_liar_ignored_indentation_error(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
509 # Jinja2 sets __file__ to be a non-Python file, and then execs code.
510 # If that file contains untokenizable code, we shouldn't get an
511 # exception.
512 source = "exec(compile('','','exec'), {'__file__': 'liar.html'})" 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
513 self.make_file("liar.py", source) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
514 # Tokenize will raise an IndentationError if it can't dedent.
515 self.make_file("liar.html", "0\n 2\n 1\n") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
516 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
517 self.start_import_stop(cov, "liar") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
518 cov.html_report() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
519 self.assert_exists("htmlcov/index.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
521 def test_decode_error(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
522 # https://github.com/coveragepy/coveragepy/issues/351
523 # imp.load_module won't load a file with an undecodable character
524 # in a comment, though Python will run them. So we'll change the
525 # file after running.
526 self.make_file("main.py", "import sub.not_ascii") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
527 self.make_file("sub/__init__.py") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
528 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
529 "sub/not_ascii.py",
530 """\
531 # coding: utf-8
532 a = 1 # Isn't this great?!
533 """,
534 )
535 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
536 self.start_import_stop(cov, "main") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
538 # Create the undecodable version of the file. make_file is too helpful,
539 # so get down and dirty with bytes.
540 with open("sub/not_ascii.py", "wb") as f: 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
541 f.write(b"# coding: utf-8\na = 1 # Isn't this great?\xcb!\n") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
543 with open("sub/not_ascii.py", "rb") as f: 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
544 undecodable = f.read() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
545 assert b"?\xcb!" in undecodable 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
547 cov.html_report() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
549 html_report = self.get_html_report_content("sub/not_ascii.py") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
550 expected = "# Isn't this great?�!" 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
551 assert expected in html_report 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
553 def test_formfeeds(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
554 # https://github.com/coveragepy/coveragepy/issues/360
555 self.make_file("formfeed.py", "line_one = 1\n\f\nline_two = 2\n") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
556 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
557 self.start_import_stop(cov, "formfeed") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
558 cov.html_report() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
560 formfeed_html = self.get_html_report_content("formfeed.py") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
561 assert "line_two" in formfeed_html 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
563 def test_splitlines_special_chars(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
564 # https://github.com/coveragepy/coveragepy/issues/1512
565 # See https://docs.python.org/3/library/stdtypes.html#str.splitlines for
566 # the characters splitlines treats specially that readlines does not.
568 # I'm not exactly sure why we need the "a" strings here, but the old
569 # code wasn't failing without them.
570 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
571 "splitlines_is_weird.py",
572 """\
573 test = {
574 "0b": ["\x0b0"], "a1": "this is line 2",
575 "0c": ["\x0c0"], "a2": "this is line 3",
576 "1c": ["\x1c0"], "a3": "this is line 4",
577 "1d": ["\x1d0"], "a4": "this is line 5",
578 "1e": ["\x1e0"], "a5": "this is line 6",
579 "85": ["\x850"], "a6": "this is line 7",
580 "2028": ["\u20280"], "a7": "this is line 8",
581 "2029": ["\u20290"], "a8": "this is line 9",
582 }
583 DONE = 1
584 """,
585 )
586 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
587 self.start_import_stop(cov, "splitlines_is_weird") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
588 cov.html_report() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
590 the_html = self.get_html_report_content("splitlines_is_weird.py") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
591 assert "DONE" in the_html 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
593 # Check that the lines are properly decoded and reported...
594 html_lines = the_html.split("\n") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
595 assert any(re.search(r'id="t2".*"this is line 2"', line) for line in html_lines) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
596 assert any(re.search(r'id="t9".*"this is line 9"', line) for line in html_lines) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
599class HtmlTest(HtmlTestHelpers, CoverageTest): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
600 """Moar HTML tests."""
602 def test_missing_source_file_incorrect_message(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
603 # https://github.com/coveragepy/coveragepy/issues/60
604 self.make_file("thefile.py", "import sub.another\n") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
605 self.make_file("sub/__init__.py", "") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
606 self.make_file("sub/another.py", "print('another')\n") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
607 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
608 self.start_import_stop(cov, "thefile") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
609 os.remove("sub/another.py") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
611 missing_file = os.path.join(self.temp_dir, "sub", "another.py") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
612 missing_file = os.path.realpath(missing_file) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
613 msg = "(?i)No source for code: '%s'" % re.escape(missing_file) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
614 with pytest.raises(NoSource, match=msg): 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
615 cov.html_report() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
617 def test_extensionless_file_collides_with_extension(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
618 # It used to be that "program" and "program.py" would both be reported
619 # to "program.html". Now they are not.
620 # https://github.com/coveragepy/coveragepy/issues/69
621 self.make_file("program", "import program\n") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
622 self.make_file("program.py", "a = 1\n") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
623 self.make_data_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
624 lines={
625 abs_file("program"): [1],
626 abs_file("program.py"): [1],
627 }
628 )
629 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
630 cov.load() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
631 cov.html_report() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
632 self.assert_exists("htmlcov/index.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
633 self.assert_exists("htmlcov/program.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
634 self.assert_exists("htmlcov/program_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
636 def test_has_date_stamp_in_files(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
637 self.create_initial_files() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
638 self.run_coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
640 with open("htmlcov/index.html", encoding="utf-8") as f: 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
641 self.assert_correct_timestamp(f.read()) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
642 with open("htmlcov/main_file_py.html", encoding="utf-8") as f: 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
643 self.assert_correct_timestamp(f.read()) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
645 def test_reporting_on_unmeasured_file(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
646 # It should be ok to ask for an HTML report on a file that wasn't even
647 # measured at all. https://github.com/coveragepy/coveragepy/issues/403
648 self.create_initial_files() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
649 self.make_file("other.py", "a = 1\n") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
650 self.run_coverage(htmlargs=dict(morfs=["other.py"])) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
651 self.assert_exists("htmlcov/index.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
652 self.assert_exists("htmlcov/other_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
654 def make_main_and_not_covered(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
655 """Helper to create files for skip_covered scenarios."""
656 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
657 "main_file.py",
658 """\
659 import not_covered
661 def normal():
662 print("z")
663 normal()
664 """,
665 )
666 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
667 "not_covered.py",
668 """\
669 def not_covered():
670 print("n")
671 """,
672 )
674 def test_report_skip_covered(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
675 self.make_main_and_not_covered() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
676 self.run_coverage(htmlargs=dict(skip_covered=True)) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
677 self.assert_exists("htmlcov/index.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
678 self.assert_doesnt_exist("htmlcov/main_file_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
679 self.assert_exists("htmlcov/not_covered_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
681 def test_html_skip_covered(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
682 self.make_main_and_not_covered() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
683 self.make_file(".coveragerc", "[html]\nskip_covered = True") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
684 self.run_coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
685 self.assert_exists("htmlcov/index.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
686 self.assert_doesnt_exist("htmlcov/main_file_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
687 self.assert_exists("htmlcov/not_covered_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
688 index = self.get_html_index_content() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
689 assert "1 file skipped due to complete coverage." in index 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
691 def test_report_skip_covered_branches(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
692 self.make_main_and_not_covered() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
693 self.run_coverage(covargs=dict(branch=True), htmlargs=dict(skip_covered=True)) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
694 self.assert_exists("htmlcov/index.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
695 self.assert_doesnt_exist("htmlcov/main_file_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
696 self.assert_exists("htmlcov/not_covered_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
698 def test_report_skip_covered_100(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
699 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
700 "main_file.py",
701 """\
702 def normal():
703 print("z")
704 normal()
705 """,
706 )
707 res = self.run_coverage(covargs=dict(source="."), htmlargs=dict(skip_covered=True)) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
708 assert res == 100.0 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
709 self.assert_doesnt_exist("htmlcov/main_file_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
710 # Since there are no files to report, we can't collect any region
711 # information, so there are no region-based index pages.
712 self.assert_doesnt_exist("htmlcov/function_index.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
713 self.assert_doesnt_exist("htmlcov/class_index.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
715 def test_report_skip_covered_100_functions(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
716 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
717 "main_file.py",
718 """\
719 def normal():
720 print("z")
721 def abnormal():
722 print("a")
723 normal()
724 """,
725 )
726 res = self.run_coverage(covargs=dict(source="."), htmlargs=dict(skip_covered=True)) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
727 assert res == 80.0 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
728 self.assert_exists("htmlcov/main_file_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
729 # We have a file to report, so we get function and class index pages,
730 # even though there are no classes.
731 self.assert_exists("htmlcov/function_index.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
732 self.assert_exists("htmlcov/class_index.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
734 def make_init_and_main(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
735 """Helper to create files for skip_empty scenarios."""
736 self.make_file("submodule/__init__.py", "") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
737 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
738 "main_file.py",
739 """\
740 import submodule
742 def normal():
743 print("z")
744 normal()
745 """,
746 )
748 def test_report_skip_empty(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
749 self.make_init_and_main() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
750 self.run_coverage(htmlargs=dict(skip_empty=True)) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
751 self.assert_exists("htmlcov/index.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
752 self.assert_exists("htmlcov/main_file_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
753 self.assert_doesnt_exist("htmlcov/submodule___init___py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
754 index = self.get_html_index_content() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
755 assert "1 empty file skipped." in index 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
757 def test_html_skip_empty(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
758 self.make_init_and_main() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
759 self.make_file(".coveragerc", "[html]\nskip_empty = True") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
760 self.run_coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
761 self.assert_exists("htmlcov/index.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
762 self.assert_exists("htmlcov/main_file_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
763 self.assert_doesnt_exist("htmlcov/submodule___init___py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
766def filepath_to_regex(path: str) -> str: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
767 """Create a regex for scrubbing a file path."""
768 regex = re.escape(path) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
769 # If there's a backslash, let it match either slash.
770 regex = regex.replace(r"\\", r"[\\/]") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
771 if env.WINDOWS: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
772 regex = "(?i)" + regex 1abcdefghijkl5mn6op7qr82
773 return regex 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
776def compare_html( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8
777 expected: str,
778 actual: str,
779 extra_scrubs: list[tuple[str, str]] | None = None,
780) -> None:
781 """Specialized compare function for our HTML files."""
782 __tracebackhide__ = True # pytest, please don't show me this function. 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
783 scrubs = [ 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
784 (r"/coverage\.readthedocs\.io/?[-.\w/]*", "/coverage.readthedocs.io/VER"),
785 (r"coverage\.py v[\d.abcdev]+", "coverage.py vVER"),
786 (r"created at \d\d\d\d-\d\d-\d\d \d\d:\d\d [-+]\d\d\d\d", "created at DATE"),
787 (r"created at \d\d\d\d-\d\d-\d\d \d\d:\d\d", "created at DATE"),
788 # Static files have cache busting.
789 (r"_cb_\w{8}\.", "_CB."),
790 # Un-prettify file paths.
791 (r" [/\\] ", "/"),
792 # Occasionally an absolute path is in the HTML report.
793 (filepath_to_regex(TESTS_DIR), "TESTS_DIR"),
794 (filepath_to_regex(flat_rootname(str(TESTS_DIR))), "_TESTS_DIR"),
795 # The temp dir the tests make.
796 (filepath_to_regex(os.getcwd()), "TEST_TMPDIR"),
797 (filepath_to_regex(flat_rootname(str(os.getcwd()))), "_TEST_TMPDIR"),
798 (filepath_to_regex(abs_file(os.getcwd())), "TEST_TMPDIR"),
799 (filepath_to_regex(flat_rootname(str(abs_file(os.getcwd())))), "_TEST_TMPDIR"),
800 (r"/private/var/[\w/]+/pytest-of-\w+/pytest-\d+/(popen-gw\d+/)?t\d+", "TEST_TMPDIR"),
801 # If the gold files were created on Windows, we need to scrub Windows paths also:
802 (r"[A-Z]:\\Users\\[\w\\]+\\pytest-of-\w+\\pytest-\d+\\(popen-gw\d+\\)?t\d+", "TEST_TMPDIR"),
803 ]
804 if extra_scrubs: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
805 scrubs += extra_scrubs 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
806 compare(expected, actual, file_pattern="*.html", scrubs=scrubs) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
809def unbust(directory: str) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
810 """Find files with cache busting, and rename them to simple names.
812 This makes it possible for us to compare gold files.
813 """
814 with change_dir(directory): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
815 for fname in os.listdir("."): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
816 base, ext = os.path.splitext(fname) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
817 base, _, _ = base.partition("_cb_") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
818 if base != fname: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
819 os.rename(fname, base + ext) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
822class HtmlGoldTest(HtmlTestHelpers, CoverageTest): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
823 """Tests of HTML reporting that use gold files."""
825 def test_a(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
826 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
827 "a.py",
828 """\
829 if 1 < 2:
830 # Needed a < to look at HTML entities.
831 a = 3
832 else:
833 a = 4
834 """,
835 )
837 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
838 a = self.start_import_stop(cov, "a") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
839 cov.html_report(a, directory="out/a") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
841 compare_html(gold_path("html/a"), "out/a") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
842 contains( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
843 "out/a/a_py.html",
844 (
845 '<span class="key">if</span> <span class="num">1</span> '
846 + '<span class="op"><</span> <span class="num">2</span>'
847 ),
848 (
849 ' <span class="nam">a</span> '
850 + '<span class="op">=</span> <span class="num">3</span>'
851 ),
852 '<span class="pc_cov">67%</span>',
853 )
854 contains( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
855 "out/a/index.html",
856 '<a href="a_py.html">a.py</a>',
857 '<span class="pc_cov">67%</span>',
858 '<td data-ratio="2 3">67%</td>',
859 )
861 def test_b_branch(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
862 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
863 "b.py",
864 """\
865 def one(x):
866 # This will be a branch that misses the else.
867 if x < 2:
868 a = 3
869 else:
870 a = 4
872 one(1)
874 def two(x):
875 # A missed else that branches to "exit"
876 if x:
877 a = 5
879 two(1)
881 def three():
882 try:
883 # This if has two branches, *neither* one taken.
884 if name_error_this_variable_doesnt_exist:
885 a = 1
886 else:
887 a = 2
888 except:
889 pass
891 three()
892 """,
893 )
895 cov = coverage.Coverage(branch=True) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
896 b = self.start_import_stop(cov, "b") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
897 cov.html_report(b, directory="out/b_branch") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
898 compare_html(gold_path("html/b_branch"), "out/b_branch") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
899 contains( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
900 "out/b_branch/b_py.html",
901 (
902 '<span class="key">if</span> <span class="nam">x</span> '
903 + '<span class="op"><</span> <span class="num">2</span>'
904 ),
905 (
906 ' <span class="nam">a</span> <span class="op">=</span> '
907 + '<span class="num">3</span>'
908 ),
909 '<span class="pc_cov">70%</span>',
910 (
911 '<span class="annotate short">3 ↛ 6</span>'
912 + '<span class="annotate long">line 3 didn\'t jump to line 6 '
913 + "because the condition on line 3 was always true</span>"
914 ),
915 (
916 '<span class="annotate short">12 ↛ exit</span>'
917 + "<span class=\"annotate long\">line 12 didn't return from function 'two' "
918 + "because the condition on line 12 was always true</span>"
919 ),
920 (
921 '<span class="annotate short">20 ↛ anywhere</span>'
922 + '<span class="annotate long">line 20 didn\'t jump anywhere: '
923 + "it always raised an exception.</span>"
924 ),
925 )
926 contains( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
927 "out/b_branch/index.html",
928 '<a href="b_py.html">b.py</a>',
929 '<span class="pc_cov">70%</span>',
930 '<td data-ratio="16 23">70%</td>',
931 )
933 def test_bom(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
934 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
935 "bom.py",
936 bytes=b"""\
937\xef\xbb\xbf# A Python source file in utf-8, with BOM.
938math = "3\xc3\x974 = 12, \xc3\xb72 = 6\xc2\xb10"
940assert len(math) == 18
941assert len(math.encode('utf-8')) == 21
942""".replace(b"\n", b"\r\n"),
943 )
945 # It's important that the source file really have a BOM, which can
946 # get lost, so check that it's really there, and that we have \r\n
947 # line endings.
948 with open("bom.py", "rb") as f: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
949 data = f.read() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
950 assert data[:3] == b"\xef\xbb\xbf" 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
951 assert data.count(b"\r\n") == 5 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
953 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
954 bom = self.start_import_stop(cov, "bom") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
955 cov.html_report(bom, directory="out/bom") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
957 compare_html(gold_path("html/bom"), "out/bom") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
958 contains( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
959 "out/bom/bom_py.html",
960 '<span class="str">"3×4 = 12, ÷2 = 6±0"</span>',
961 )
963 def test_isolatin1(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
964 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
965 "isolatin1.py",
966 bytes=b"""\
967# -*- coding: iso8859-1 -*-
968# A Python source file in another encoding.
970math = "3\xd74 = 12, \xf72 = 6\xb10"
971assert len(math) == 18
972""",
973 )
975 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
976 isolatin1 = self.start_import_stop(cov, "isolatin1") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
977 cov.html_report(isolatin1, directory="out/isolatin1") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
979 compare_html(gold_path("html/isolatin1"), "out/isolatin1") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
980 contains( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
981 "out/isolatin1/isolatin1_py.html",
982 '<span class="str">"3×4 = 12, ÷2 = 6±0"</span>',
983 )
985 def make_main_etc(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
986 """Make main.py and m1-m3.py for other tests."""
987 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
988 "main.py",
989 """\
990 import m1
991 import m2
992 import m3
994 a = 5
995 b = 6
997 assert m1.m1a == 1
998 assert m2.m2a == 1
999 assert m3.m3a == 1
1000 """,
1001 )
1002 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1003 "m1.py",
1004 """\
1005 m1a = 1
1006 m1b = 2
1007 """,
1008 )
1009 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1010 "m2.py",
1011 """\
1012 m2a = 1
1013 m2b = 2
1014 """,
1015 )
1016 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1017 "m3.py",
1018 """\
1019 m3a = 1
1020 m3b = 2
1021 """,
1022 )
1024 def test_omit_1(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1025 self.make_main_etc() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1026 cov = coverage.Coverage(include=["./*"]) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1027 self.start_import_stop(cov, "main") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1028 cov.html_report(directory="out/omit_1") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1029 compare_html(gold_path("html/omit_1"), "out/omit_1") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1031 def test_omit_2(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1032 self.make_main_etc() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1033 cov = coverage.Coverage(include=["./*"]) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1034 self.start_import_stop(cov, "main") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1035 cov.html_report(directory="out/omit_2", omit=["m1.py"]) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1036 compare_html(gold_path("html/omit_2"), "out/omit_2") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1038 def test_omit_3(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1039 self.make_main_etc() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1040 cov = coverage.Coverage(include=["./*"]) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1041 self.start_import_stop(cov, "main") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1042 cov.html_report(directory="out/omit_3", omit=["m1.py", "m2.py"]) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1043 compare_html(gold_path("html/omit_3"), "out/omit_3") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1045 def test_omit_4(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1046 self.make_main_etc() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1047 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1048 "omit4.ini",
1049 """\
1050 [report]
1051 omit = m2.py
1052 """,
1053 )
1055 cov = coverage.Coverage(config_file="omit4.ini", include=["./*"]) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1056 self.start_import_stop(cov, "main") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1057 cov.html_report(directory="out/omit_4") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1058 compare_html(gold_path("html/omit_4"), "out/omit_4") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1060 def test_omit_5(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1061 self.make_main_etc() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1062 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1063 "omit5.ini",
1064 """\
1065 [report]
1066 omit =
1067 fooey
1068 gooey, m[23]*, kablooey
1069 helloworld
1071 [html]
1072 directory = out/omit_5
1073 """,
1074 )
1076 cov = coverage.Coverage(config_file="omit5.ini", include=["./*"]) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1077 self.start_import_stop(cov, "main") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1078 cov.html_report() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1079 compare_html(gold_path("html/omit_5"), "out/omit_5") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1081 def test_other(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1082 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1083 "src/here.py",
1084 """\
1085 import other
1087 if 1 < 2:
1088 h = 3
1089 else:
1090 h = 4
1091 """,
1092 )
1093 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1094 "othersrc/other.py",
1095 """\
1096 # A file in another directory. We're checking that it ends up in the
1097 # HTML report.
1099 print("This is the other src!")
1100 """,
1101 )
1103 with change_dir("src"): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1104 sys.path.insert(0, "../othersrc") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1105 cov = coverage.Coverage(include=["./*", "../othersrc/*"]) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1106 self.start_import_stop(cov, "here") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1107 cov.html_report(directory="../out/other") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1109 # Different platforms will name the "other" file differently. Rename it
1110 actual_file = list(glob.glob("out/other/*_other_py.html")) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1111 assert len(actual_file) == 1 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1112 os.rename(actual_file[0], "out/other/blah_blah_other_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1114 compare_html( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1115 gold_path("html/other"),
1116 "out/other",
1117 extra_scrubs=[
1118 (r'href="z_[0-9a-z]{16}_other_', 'href="_TEST_TMPDIR_other_othersrc_'),
1119 (r"TEST_TMPDIR\\othersrc\\other.py", "TEST_TMPDIR/othersrc/other.py"),
1120 ],
1121 )
1122 contains( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1123 "out/other/index.html",
1124 '<a href="here_py.html">here.py</a>',
1125 'other_py.html">',
1126 "other.py</a>",
1127 )
1129 def test_partial(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1130 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1131 "partial.py",
1132 """\
1133 # partial branches and excluded lines
1134 a = 2
1136 while "no peephole".upper(): # t4
1137 break
1139 while a: # pragma: no branch
1140 break
1142 if 0:
1143 never_happen()
1145 if 13:
1146 a = 14
1148 if a == 16:
1149 raise ZeroDivisionError("17")
1150 """,
1151 )
1152 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1153 "partial.ini",
1154 """\
1155 [run]
1156 branch = True
1158 [report]
1159 exclude_lines =
1160 raise ZeroDivisionError
1161 """,
1162 )
1164 cov = coverage.Coverage(config_file="partial.ini") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1165 partial = self.start_import_stop(cov, "partial") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1167 cov.html_report(partial, directory="out/partial") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1168 compare_html(gold_path("html/partial"), "out/partial") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1169 contains_rx( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1170 "out/partial/partial_py.html",
1171 r'<p class="par run show_par">.* id="t4"',
1172 r'<p class="run">.* id="t7"',
1173 # The "if 0" and "if 1" statements are marked as run.
1174 r'<p class="run">.* id="t10"',
1175 # The "raise ZeroDivisionError" is excluded by regex in the .ini.
1176 r'<p class="exc show_exc">.* id="t17"',
1177 )
1178 contains( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1179 "out/partial/index.html",
1180 '<a href="partial_py.html">partial.py</a>',
1181 '<span class="pc_cov">92%</span>',
1182 )
1184 def test_styled(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1185 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1186 "a.py",
1187 """\
1188 if 1 < 2:
1189 # Needed a < to look at HTML entities.
1190 a = 3
1191 else:
1192 a = 4
1193 """,
1194 )
1196 self.make_file("myfile/myextra.css", "/* Doesn't matter what's here, it gets copied. */\n") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1198 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1199 a = self.start_import_stop(cov, "a") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1200 cov.html_report(a, directory="out/styled", extra_css="myfile/myextra.css") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1201 self.assert_valid_hrefs("out/styled") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1202 compare_html(gold_path("html/styled"), "out/styled") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1203 unbust("out/styled") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1204 compare(gold_path("html/styled"), "out/styled", file_pattern="*.css") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1205 contains_rx( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1206 "out/styled/a_py.html",
1207 r'<link rel="stylesheet" href="myextra_cb_\w{8}.css" type="text/css">',
1208 (
1209 r'<span class="key">if</span> <span class="num">1</span> '
1210 + r'<span class="op"><</span> <span class="num">2</span>'
1211 ),
1212 (
1213 r' <span class="nam">a</span> <span class="op">=</span> '
1214 + r'<span class="num">3</span>'
1215 ),
1216 r'<span class="pc_cov">67%</span>',
1217 )
1218 contains_rx( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1219 "out/styled/index.html",
1220 r'<link rel="stylesheet" href="myextra_cb_\w{8}.css" type="text/css">',
1221 r'<a href="a_py.html">a.py</a>',
1222 r'<span class="pc_cov">67%</span>',
1223 )
1225 def test_multiline(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1226 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1227 "multiline.py",
1228 """\
1229 x = 0
1230 if (
1231 x or x
1232 ):
1233 print(
1234 # wut
1235 "hello",
1236 )
1237 else:
1238 print(
1239 "bye",
1240 # wut
1241 )
1243 if 0: # pragma: no cover
1244 print(
1245 "never"
1246 )
1247 """,
1248 )
1249 cov = coverage.Coverage(branch=True) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1250 multiline = self.start_import_stop(cov, "multiline") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1251 cov.html_report(multiline, directory="out/multiline") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1252 compare_html(gold_path("html/multiline"), "out/multiline") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1253 contains( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1254 "out/multiline/multiline_py.html",
1255 '<p class="mis mis2 show_mis"><span class="n"><a id="t6" href="#t6">6</a></span>',
1256 '<p class="exc exc2 show_exc"><span class="n"><a id="t17" href="#t17">17</a>',
1257 )
1259 def test_tabbed(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1260 # The file contents would look like this with 8-space tabs:
1261 # x = 1
1262 # if x:
1263 # a = "tabbed" # aligned comments
1264 # if x: # look nice
1265 # b = "no spaces" # when they
1266 # c = "done" # line up.
1267 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1268 "tabbed.py",
1269 """\
1270 x = 1
1271 if x:
1272 \ta = "Tabbed"\t\t\t\t# Aligned comments
1273 \tif x:\t\t\t\t\t# look nice
1274 \t\tb = "No spaces"\t\t\t# when they
1275 \tc = "Done"\t\t\t\t# line up.
1276 """,
1277 )
1279 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1280 tabbed = self.start_import_stop(cov, "tabbed") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1281 cov.html_report(tabbed, directory="out") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1283 # Editors like to change things, make sure our source file still has tabs.
1284 contains("tabbed.py", "\tif x:\t\t\t\t\t# look nice") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1286 contains( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1287 "out/tabbed_py.html",
1288 '> <span class="key">if</span> '
1289 + '<span class="nam">x</span><span class="op">:</span>'
1290 + " "
1291 + '<span class="com"># look nice</span>',
1292 )
1294 doesnt_contain("out/tabbed_py.html", "\t") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1296 def test_bug_1828(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1297 # https://github.com/coveragepy/coveragepy/pull/1828
1298 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1299 "backslashes.py",
1300 """\
1301 a = ["aaa",\\
1302 "bbb \\
1303 ccc"]
1304 """,
1305 )
1307 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1308 backslashes = self.start_import_stop(cov, "backslashes") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1309 cov.html_report(backslashes) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1311 contains( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1312 "htmlcov/backslashes_py.html",
1313 # line 2 is `"bbb \`
1314 r'<a id="t2" href="#t2">2</a></span>'
1315 + r'<span class="t"> <span class="str">"bbb \</span>',
1316 # line 3 is `ccc"]`
1317 r'<a id="t3" href="#t3">3</a></span>'
1318 + r'<span class="t"><span class="str"> ccc"</span><span class="op">]</span>',
1319 )
1321 assert self.get_html_report_text_lines("backslashes.py") == [ 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1322 '1a = ["aaa",\\',
1323 '2 "bbb \\',
1324 '3 ccc"]',
1325 ]
1327 @pytest.mark.parametrize( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1328 "leader",
1329 ["", "f", "r", "fr", "rf"],
1330 ids=["string", "f-string", "raw_string", "f-raw_string", "raw_f-string"],
1331 )
1332 def test_bug_1836(self, leader: str) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1333 # https://github.com/coveragepy/coveragepy/issues/1836
1334 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1335 "py312_fstrings.py",
1336 f"""\
1337 prog_name = 'bug.py'
1338 err_msg = {leader}'''\\
1339 {{prog_name}}: ERROR: This is the first line of the error.
1340 {{prog_name}}: ERROR: This is the second line of the error.
1341 \\
1342 {{prog_name}}: ERROR: This is the third line of the error.
1343 '''
1344 """,
1345 )
1347 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1348 py312_fstrings = self.start_import_stop(cov, "py312_fstrings") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1349 cov.html_report(py312_fstrings) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1351 assert self.get_html_report_text_lines("py312_fstrings.py") == [ 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1352 "1" + "prog_name = 'bug.py'",
1353 "2" + f"err_msg = {leader}'''\\",
1354 "3" + "{prog_name}: ERROR: This is the first line of the error.",
1355 "4" + "{prog_name}: ERROR: This is the second line of the error.",
1356 "5" + "\\",
1357 "6" + "{prog_name}: ERROR: This is the third line of the error.",
1358 "7" + "'''",
1359 ]
1361 def test_bug_1980(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1362 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1363 "fstring_middle.py",
1364 """\
1365 x = 1
1366 f'Look: {x} {{x}}!'
1367 """,
1368 )
1370 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1371 the_mod = self.start_import_stop(cov, "fstring_middle") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1372 cov.html_report(the_mod) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1374 assert self.get_html_report_text_lines("fstring_middle.py") == [ 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1375 "1" + "x = 1",
1376 "2" + "f'Look: {x} {{x}}!'",
1377 ]
1379 def test_unicode(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1380 surrogate = "\U000e0100" 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1382 self.make_file( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1383 "unicode.py",
1384 """\
1385 # -*- coding: utf-8 -*-
1386 # A Python source file with exotic characters.
1388 upside_down = "ʎd˙ǝbɐɹǝʌoɔ"
1389 surrogate = "db40,dd00: x@"
1390 """.replace("@", surrogate),
1391 )
1393 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1394 unimod = self.start_import_stop(cov, "unicode") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1395 cov.html_report(unimod, directory="out/unicode") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1397 compare_html(gold_path("html/unicode"), "out/unicode") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1398 contains( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1399 "out/unicode/unicode_py.html",
1400 '<span class="str">"ʎd˙ǝbɐɹǝʌoɔ"</span>',
1401 )
1403 contains_any( 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1404 "out/unicode/unicode_py.html",
1405 '<span class="str">"db40,dd00: x��"</span>',
1406 '<span class="str">"db40,dd00: x󠄀"</span>',
1407 )
1409 def test_accented_dot_py(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1410 # Make a file with a non-ascii character in the filename.
1411 self.make_file("h\xe2t.py", "print('accented')") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1412 self.make_data_file(lines={abs_file("h\xe2t.py"): [1]}) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1413 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1414 cov.load() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1415 cov.html_report() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1416 self.assert_exists("htmlcov/h\xe2t_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1417 with open("htmlcov/index.html", encoding="utf-8") as indexf: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1418 index = indexf.read() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1419 assert '<a href="hât_py.html">hât.py</a>' in index 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1421 def test_accented_directory(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1422 # Make a file with a non-ascii character in the directory name.
1423 self.make_file("\xe2/accented.py", "print('accented')") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1424 self.make_data_file(lines={abs_file("\xe2/accented.py"): [1]}) 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1426 # The HTML report uses ascii-encoded HTML entities.
1427 cov = coverage.Coverage() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1428 cov.load() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1429 cov.html_report() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1430 self.assert_exists("htmlcov/z_5786906b6f0ffeb4_accented_py.html") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1431 with open("htmlcov/index.html", encoding="utf-8") as indexf: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1432 index = indexf.read() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1433 expected = ( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1434 '<a href="z_5786906b6f0ffeb4_accented_py.html">â %s accented.py</a>'
1435 )
1436 assert expected % os.sep in index 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1439@pytest.mark.skipif(not testenv.DYN_CONTEXTS, reason="No dynamic contexts with this core.") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1440class HtmlWithContextsTest(HtmlTestHelpers, CoverageTest): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1441 """Tests of the HTML reports with shown contexts."""
1443 EMPTY = coverage.html.HtmlDataGeneration.EMPTY 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1445 def html_data_from_cov(self, cov: Coverage, morf: TMorf) -> coverage.html.FileData: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1446 """Get HTML report data from a `Coverage` object for a morf."""
1447 with self.assert_warnings(cov, []): 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1448 datagen = coverage.html.HtmlDataGeneration(cov) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1449 fr, analysis = next(iter(get_analysis_to_report(cov, [morf]))) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1450 file_data = datagen.data_for_file(fr, analysis) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1451 return file_data 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1453 SOURCE = """\ 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1454 def helper(lineno):
1455 x = 2
1457 def test_one():
1458 a = 5
1459 helper(6)
1461 def test_two():
1462 a = 9
1463 b = 10
1464 if a > 11:
1465 b = 12
1466 assert a == (13-4)
1467 assert b == (14-4)
1468 helper(
1469 16
1470 )
1472 test_one()
1473 x = 20
1474 helper(21)
1475 test_two()
1476 """
1478 OUTER_LINES = [1, 4, 8, 19, 20, 21, 2, 22] 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1479 TEST_ONE_LINES = [5, 6, 2] 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1480 TEST_TWO_LINES = [9, 10, 11, 13, 14, 15, 2] 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1482 def test_dynamic_contexts(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1483 self.make_file("two_tests.py", self.SOURCE) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1484 cov = coverage.Coverage(source=["."]) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1485 cov.set_option("run:dynamic_context", "test_function") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1486 cov.set_option("html:show_contexts", True) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1487 mod = self.start_import_stop(cov, "two_tests") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1488 d = self.html_data_from_cov(cov, mod) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1489 context_labels = [self.EMPTY, "two_tests.test_one", "two_tests.test_two"] 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1490 expected_lines = [self.OUTER_LINES, self.TEST_ONE_LINES, self.TEST_TWO_LINES] 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1491 for label, expected in zip(context_labels, expected_lines): 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1492 actual = [ 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1493 ld.number
1494 for ld in d.lines
1495 if label == ld.contexts_label or label in (ld.contexts or ())
1496 ]
1497 assert sorted(expected) == sorted(actual) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1499 cov.html_report(mod, directory="out/contexts") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1500 compare_html(gold_path("html/contexts"), "out/contexts") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1502 def test_filtered_dynamic_contexts(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1503 self.make_file("two_tests.py", self.SOURCE) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1504 cov = coverage.Coverage(source=["."]) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1505 cov.set_option("run:dynamic_context", "test_function") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1506 cov.set_option("html:show_contexts", True) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1507 cov.set_option("report:contexts", ["test_one"]) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1508 mod = self.start_import_stop(cov, "two_tests") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1509 d = self.html_data_from_cov(cov, mod) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1511 context_labels = [self.EMPTY, "two_tests.test_one", "two_tests.test_two"] 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1512 expected_lines: list[list[TLineNo]] = [[], self.TEST_ONE_LINES, []] 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1513 for label, expected in zip(context_labels, expected_lines): 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1514 actual = [ld.number for ld in d.lines if label in (ld.contexts or ())] 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1515 assert sorted(expected) == sorted(actual) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1517 def test_no_contexts_warns_no_contexts(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1518 # If no contexts were collected, then show_contexts emits a warning.
1519 self.make_file("two_tests.py", self.SOURCE) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1520 cov = coverage.Coverage(source=["."]) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1521 cov.set_option("html:show_contexts", True) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1522 self.start_import_stop(cov, "two_tests") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1523 with self.assert_warnings(cov, ["No contexts were measured"]): 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1524 cov.html_report() 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1526 def test_dynamic_contexts_relative_files(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1527 self.make_file("two_tests.py", self.SOURCE) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1528 self.make_file("config", "[run]\nrelative_files = True") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1529 cov = coverage.Coverage(source=["."], config_file="config") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1530 cov.set_option("run:dynamic_context", "test_function") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1531 cov.set_option("html:show_contexts", True) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1532 mod = self.start_import_stop(cov, "two_tests") 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1533 d = self.html_data_from_cov(cov, mod) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1534 context_labels = [self.EMPTY, "two_tests.test_one", "two_tests.test_two"] 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1535 expected_lines = [self.OUTER_LINES, self.TEST_ONE_LINES, self.TEST_TWO_LINES] 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1536 for label, expected in zip(context_labels, expected_lines): 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1537 actual = [ 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1538 ld.number
1539 for ld in d.lines
1540 if label == ld.contexts_label or label in (ld.contexts or ())
1541 ]
1542 assert sorted(expected) == sorted(actual) 1stuvabwxyzcdABCDefEFGHghIJKLijMNOPklQRSTmnUVWXopYZ01qr342
1545class HtmlHelpersTest(HtmlTestHelpers, CoverageTest): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1546 """Tests of the helpers in HtmlTestHelpers."""
1548 def test_bad_link(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1549 # Does assert_valid_hrefs detect links to non-existent files?
1550 self.make_file("htmlcov/index.html", "<a href='nothing.html'>Nothing</a>") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1551 msg = "These files link to 'nothing.html', which doesn't exist: htmlcov.index.html" 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1552 with pytest.raises(AssertionError, match=msg): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1553 self.assert_valid_hrefs() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1555 def test_bad_anchor(self) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1556 # Does assert_valid_hrefs detect fragments that go nowhere?
1557 self.make_file("htmlcov/index.html", "<a href='#nothing'>Nothing</a>") 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1558 msg = "Fragment '#nothing' in htmlcov.index.html has no anchor" 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1559 with pytest.raises(AssertionError, match=msg): 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1560 self.assert_valid_hrefs() 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1563@pytest.mark.parametrize( 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1564 "n, key",
1565 [
1566 (0, "a"),
1567 (1, "b"),
1568 (999999999, "e9S_p"),
1569 ],
1570)
1571def test_encode_int(n: int, key: str) -> None: 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342
1572 assert coverage.html.encode_int(n) == key 1stuvabwxyzcdABCDefEFGHghIJKLijMN9OP!kl5QR#ST$mn6UV%WX'op7YZ(01)qr8342