Skip to content

Commit 6cb802d

Browse files
committed
feat: multi-line statments get dim colors in HTML. #1308
1 parent 184dc78 commit 6cb802d

File tree

13 files changed

+736
-12
lines changed

13 files changed

+736
-12
lines changed

CHANGES.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,14 @@ Unreleased
4343
:ref:`config_json_output`, :ref:`config_lcov_output` and
4444
:ref:`config_run_debug_file`. This is now fixed.
4545

46+
- The HTML report now dimly colors subsequent lines in multi-line statements.
47+
They used to have no color. This gives a better indication of the amount of
48+
code executed or missing. Closes `issue 1308`_.
4649

4750
.. _issue 310: https://github.com/nedbat/coveragepy/issues/310
4851
.. _issue 312: https://github.com/nedbat/coveragepy/issues/312
4952
.. _issue 831: https://github.com/nedbat/coveragepy/issues/831
53+
.. _issue 1308: https://github.com/nedbat/coveragepy/issues/1308
5054
.. _issue 1845: https://github.com/nedbat/coveragepy/issues/1845
5155
.. _issue 1941: https://github.com/nedbat/coveragepy/issues/1941
5256

coverage/html.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,13 @@ def data_for_file(self, fr: FileReporter, analysis: Analysis) -> FileData:
138138

139139
lines = []
140140
branch_stats = analysis.branch_stats()
141+
multiline_map = {}
142+
if hasattr(fr, "multiline_map"):
143+
multiline_map = fr.multiline_map()
141144

142145
for lineno, tokens in enumerate(fr.source_token_lines(), start=1):
143146
# Figure out how to mark this line.
144-
category = ""
147+
category = category2 = ""
145148
short_annotations = []
146149
long_annotations = []
147150

@@ -169,6 +172,18 @@ def data_for_file(self, fr: FileReporter, analysis: Analysis) -> FileData:
169172
)
170173
elif lineno in analysis.statements:
171174
category = "run"
175+
elif first_line := multiline_map.get(lineno):
176+
if first_line in analysis.excluded:
177+
category2 = "exc2"
178+
elif first_line in analysis.missing:
179+
category2 = "mis2"
180+
elif self.has_arcs and first_line in missing_branch_arcs:
181+
category2 = "par2"
182+
# I don't understand why this last condition is marked as
183+
# partial. If I add an else with an exception, the exception
184+
# is raised.
185+
elif first_line in analysis.statements: # pragma: part covered
186+
category2 = "run2"
172187

173188
contexts = []
174189
contexts_label = ""
@@ -184,7 +199,7 @@ def data_for_file(self, fr: FileReporter, analysis: Analysis) -> FileData:
184199
lines.append(LineData(
185200
tokens=tokens,
186201
number=lineno,
187-
category=category,
202+
category=category or category2,
188203
contexts=contexts,
189204
contexts_label=contexts_label,
190205
context_list=context_list,
@@ -306,6 +321,10 @@ def __init__(self, cov: Coverage) -> None:
306321
"mis": "mis show_mis",
307322
"par": "par run show_par",
308323
"run": "run",
324+
"exc2": "exc exc2 show_exc",
325+
"mis2": "mis mis2 show_mis",
326+
"par2": "par par2 ru2 show_par",
327+
"run2": "run run2",
309328
},
310329
}
311330
self.index_tmpl = Templite(read_data("index.html"), self.template_globals)

coverage/htmlfiles/style.css

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,16 @@ kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em
214214

215215
@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } }
216216

217+
#source p.mis.mis2 .t { border-left: 0.2em dotted #ff0000; }
218+
219+
#source p.mis.mis2.show_mis .t { background: #ffeeee; }
220+
221+
@media (prefers-color-scheme: dark) { #source p.mis.mis2.show_mis .t { background: #351b1b; } }
222+
223+
#source p.mis.mis2.show_mis .t:hover { background: #f2d2d2; }
224+
225+
@media (prefers-color-scheme: dark) { #source p.mis.mis2.show_mis .t:hover { background: #532323; } }
226+
217227
#source p.run .t { border-left: 0.2em solid #00dd00; }
218228

219229
#source p.run.show_run .t { background: #dfd; }
@@ -224,6 +234,16 @@ kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em
224234

225235
@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } }
226236

237+
#source p.run.run2 .t { border-left: 0.2em dotted #00dd00; }
238+
239+
#source p.run.run2.show_run .t { background: #eeffee; }
240+
241+
@media (prefers-color-scheme: dark) { #source p.run.run2.show_run .t { background: #2b2e24; } }
242+
243+
#source p.run.run2.show_run .t:hover { background: #d2f2d2; }
244+
245+
@media (prefers-color-scheme: dark) { #source p.run.run2.show_run .t:hover { background: #404633; } }
246+
227247
#source p.exc .t { border-left: 0.2em solid #808080; }
228248

229249
#source p.exc.show_exc .t { background: #eee; }
@@ -234,6 +254,16 @@ kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em
234254

235255
@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } }
236256

257+
#source p.exc.exc2 .t { border-left: 0.2em dotted #808080; }
258+
259+
#source p.exc.exc2.show_exc .t { background: #f7f7f7; }
260+
261+
@media (prefers-color-scheme: dark) { #source p.exc.exc2.show_exc .t { background: #292929; } }
262+
263+
#source p.exc.exc2.show_exc .t:hover { background: #e2e2e2; }
264+
265+
@media (prefers-color-scheme: dark) { #source p.exc.exc2.show_exc .t:hover { background: #3c3c3c; } }
266+
237267
#source p.par .t { border-left: 0.2em solid #bbbb00; }
238268

239269
#source p.par.show_par .t { background: #ffa; }
@@ -244,6 +274,16 @@ kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em
244274

245275
@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } }
246276

277+
#source p.par.par2 .t { border-left: 0.2em dotted #bbbb00; }
278+
279+
#source p.par.par2.show_par .t { background: #ffffd5; }
280+
281+
@media (prefers-color-scheme: dark) { #source p.par.par2.show_par .t { background: #423a0f; } }
282+
283+
#source p.par.par2.show_par .t:hover { background: #f2f2a2; }
284+
285+
@media (prefers-color-scheme: dark) { #source p.par.par2.show_par .t:hover { background: #6d5d0c; } }
286+
247287
#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; }
248288

249289
#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; }

coverage/htmlfiles/style.scss

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ $font-code: SFMono-Regular, Menlo, Monaco, Consolas, monospace;
2727

2828
$off-button-lighten: 50%;
2929
$hover-dark-amt: 95%;
30+
$multi-dim-amt: 50%;
3031

3132
$focus-color: #007acc;
3233

@@ -521,6 +522,22 @@ $border-indicator-width: .2em;
521522
@include background-dark(mix($dark-mis-bg, $dark-fg, $hover-dark-amt));
522523
}
523524
}
525+
526+
&.mis2 {
527+
.t {
528+
border-left: $border-indicator-width dotted $mis-color;
529+
}
530+
531+
&.show_mis .t {
532+
background: mix($light-mis-bg, $light-bg, $multi-dim-amt);
533+
@include background-dark(mix($dark-mis-bg, $dark-bg, $multi-dim-amt));
534+
535+
&:hover {
536+
background: mix($light-mis-bg, $light-fg, $hover-dark-amt);
537+
@include background-dark(mix($dark-mis-bg, $dark-fg, $hover-dark-amt));
538+
}
539+
}
540+
}
524541
}
525542

526543
&.run {
@@ -537,6 +554,22 @@ $border-indicator-width: .2em;
537554
@include background-dark(mix($dark-run-bg, $dark-fg, $hover-dark-amt));
538555
}
539556
}
557+
558+
&.run2 {
559+
.t {
560+
border-left: $border-indicator-width dotted $run-color;
561+
}
562+
563+
&.show_run .t {
564+
background: mix($light-run-bg, $light-bg, $multi-dim-amt);
565+
@include background-dark(mix($dark-run-bg, $dark-bg, $multi-dim-amt));
566+
567+
&:hover {
568+
background: mix($light-run-bg, $light-fg, $hover-dark-amt);
569+
@include background-dark(mix($dark-run-bg, $dark-fg, $hover-dark-amt));
570+
}
571+
}
572+
}
540573
}
541574

542575
&.exc {
@@ -553,6 +586,22 @@ $border-indicator-width: .2em;
553586
@include background-dark(mix($dark-exc-bg, $dark-fg, $hover-dark-amt));
554587
}
555588
}
589+
590+
&.exc2 {
591+
.t {
592+
border-left: $border-indicator-width dotted $exc-color;
593+
}
594+
595+
&.show_exc .t {
596+
background: mix($light-exc-bg, $light-bg, $multi-dim-amt);
597+
@include background-dark(mix($dark-exc-bg, $dark-bg, $multi-dim-amt));
598+
599+
&:hover {
600+
background: mix($light-exc-bg, $light-fg, $hover-dark-amt);
601+
@include background-dark(mix($dark-exc-bg, $dark-fg, $hover-dark-amt));
602+
}
603+
}
604+
}
556605
}
557606

558607
&.par {
@@ -570,6 +619,21 @@ $border-indicator-width: .2em;
570619
}
571620
}
572621

622+
&.par2 {
623+
.t {
624+
border-left: $border-indicator-width dotted $par-color;
625+
}
626+
627+
&.show_par .t {
628+
background: mix($light-par-bg, $light-bg, $multi-dim-amt);
629+
@include background-dark(mix($dark-par-bg, $dark-bg, $multi-dim-amt));
630+
631+
&:hover {
632+
background: mix($light-par-bg, $light-fg, $hover-dark-amt);
633+
@include background-dark(mix($dark-par-bg, $dark-fg, $hover-dark-amt));
634+
}
635+
}
636+
}
573637
}
574638

575639
.r {

coverage/python.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ def lines(self) -> set[TLineNo]:
194194
"""Return the line numbers of statements in the file."""
195195
return self.parser.statements
196196

197+
def multiline_map(self) -> dict[TLineNo, TLineNo]:
198+
"""A map of line numbers to first-line in a multi-line statement."""
199+
return self.parser._multiline
200+
197201
def excluded_lines(self) -> set[TLineNo]:
198202
"""Return the line numbers of statements in the file."""
199203
return self.parser.excluded

tests/gold/html/contexts/two_tests_py.html

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
55
<title>Coverage for two_tests.py: 94%</title>
66
<link rel="icon" sizes="32x32" href="favicon_32_cb_58284776.png">
7-
<link rel="stylesheet" href="style_cb_8e611ae1.css" type="text/css">
7+
<link rel="stylesheet" href="style_cb_6b508a39.css" type="text/css">
88
<script type="text/javascript">
99
contexts = {
1010
"a": "(empty)",
1111
"b": "two_tests.test_two",
1212
"c": "two_tests.test_one"
1313
}
1414
</script>
15-
<script src="coverage_html_cb_606408f0.js" defer></script>
15+
<script src="coverage_html_cb_6fb7b396.js" defer></script>
1616
</head>
1717
<body class="pyfile">
1818
<header>
@@ -71,8 +71,8 @@ <h2>
7171
<a id="indexLink" class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
7272
<a id="nextFileLink" class="nav" href="index.html">&#xbb; next</a>
7373
&nbsp; &nbsp; &nbsp;
74-
<a class="nav" href="https://coverage.readthedocs.io/en/7.5.1a0.dev1">coverage.py v7.5.1a0.dev1</a>,
75-
created at 2024-04-24 09:22 -0400
74+
<a class="nav" href="https://coverage.readthedocs.io/en/7.10.0a0.dev1">coverage.py v7.10.0a0.dev1</a>,
75+
created at 2025-07-11 08:28 -0400
7676
</p>
7777
<aside class="hidden">
7878
<button type="button" class="button_next_chunk" data-shortcut="j"></button>
@@ -102,8 +102,8 @@ <h2>
102102
<p class="run"><span class="n"><a id="t13" href="#t13">13</a></span><span class="t"> <span class="key">assert</span> <span class="nam">a</span> <span class="op">==</span> <span class="op">(</span><span class="num">13</span><span class="op">-</span><span class="num">4</span><span class="op">)</span>&nbsp;</span><input type="checkbox" id="ctxs13"><span class="r"><label for="ctxs13" class="ctx">1 ctx</label></span><span class="ctxs">1b</span></p>
103103
<p class="run"><span class="n"><a id="t14" href="#t14">14</a></span><span class="t"> <span class="key">assert</span> <span class="nam">b</span> <span class="op">==</span> <span class="op">(</span><span class="num">14</span><span class="op">-</span><span class="num">4</span><span class="op">)</span>&nbsp;</span><input type="checkbox" id="ctxs14"><span class="r"><label for="ctxs14" class="ctx">1 ctx</label></span><span class="ctxs">1b</span></p>
104104
<p class="run"><span class="n"><a id="t15" href="#t15">15</a></span><span class="t"> <span class="nam">helper</span><span class="op">(</span>&nbsp;</span><input type="checkbox" id="ctxs15"><span class="r"><label for="ctxs15" class="ctx">1 ctx</label></span><span class="ctxs">1b</span></p>
105-
<p class="pln"><span class="n"><a id="t16" href="#t16">16</a></span><span class="t"> <span class="num">16</span>&nbsp;</span><span class="r"></span></p>
106-
<p class="pln"><span class="n"><a id="t17" href="#t17">17</a></span><span class="t"> <span class="op">)</span>&nbsp;</span><span class="r"></span></p>
105+
<p class="run run2"><span class="n"><a id="t16" href="#t16">16</a></span><span class="t"> <span class="num">16</span>&nbsp;</span><span class="r"></span></p>
106+
<p class="run run2"><span class="n"><a id="t17" href="#t17">17</a></span><span class="t"> <span class="op">)</span>&nbsp;</span><span class="r"></span></p>
107107
<p class="pln"><span class="n"><a id="t18" href="#t18">18</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
108108
<p class="run"><span class="n"><a id="t19" href="#t19">19</a></span><span class="t"><span class="nam">test_one</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"><label for="ctxs19" class="ctx">(empty)</label></span></p>
109109
<p class="run"><span class="n"><a id="t20" href="#t20">20</a></span><span class="t"><span class="nam">x</span> <span class="op">=</span> <span class="num">20</span>&nbsp;</span><span class="r"><label for="ctxs20" class="ctx">(empty)</label></span></p>
@@ -117,8 +117,8 @@ <h2>
117117
<a class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
118118
<a class="nav" href="index.html">&#xbb; next</a>
119119
&nbsp; &nbsp; &nbsp;
120-
<a class="nav" href="https://coverage.readthedocs.io/en/7.5.1a0.dev1">coverage.py v7.5.1a0.dev1</a>,
121-
created at 2024-04-24 09:22 -0400
120+
<a class="nav" href="https://coverage.readthedocs.io/en/7.10.0a0.dev1">coverage.py v7.10.0a0.dev1</a>,
121+
created at 2025-07-11 08:28 -0400
122122
</p>
123123
</div>
124124
</footer>

0 commit comments

Comments
 (0)