From b9bc3246feaa1bbd8adfa4c921547b24bc7dcd92 Mon Sep 17 00:00:00 2001 From: Marc Gibbons <1726961+marcgibbons@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:15:12 -0400 Subject: [PATCH 1/3] fix: endblock falsely reported as uncovered when on its own line (issue #74) In parent templates, a TEXT token inside a {% block %} can end with a whitespace-only fragment before {% endblock %} with no newline terminator. That fragment is not an executable line but was incorrectly added to source_lines, causing {% endblock %} to appear as an uncovered line. Skip the trailing fragment when inside a block (inblock=True). --- django_coverage_plugin/plugin.py | 7 ++++ tests/test_extends.py | 68 ++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/django_coverage_plugin/plugin.py b/django_coverage_plugin/plugin.py index 13e9c41..30adb8f 100644 --- a/django_coverage_plugin/plugin.py +++ b/django_coverage_plugin/plugin.py @@ -349,6 +349,13 @@ def lines(self): if lines[0].isspace(): lineno += 1 num_lines -= 1 + # When {% endblock %} is not at the start of a line, the + # preceding TEXT token ends with whitespace and no newline. + # That partial line is not executable content. + if inblock and num_lines > 0 and ( + lines[-1].isspace() and not lines[-1].endswith(("\n", "\r")) + ): + num_lines -= 1 source_lines.update(range(lineno, lineno+num_lines)) if SHOW_PARSING: diff --git a/tests/test_extends.py b/tests/test_extends.py index 3f7a987..43dd0dc 100644 --- a/tests/test_extends.py +++ b/tests/test_extends.py @@ -95,6 +95,74 @@ def test_inheriting_with_unused_blocks(self): self.assert_analysis([1, 2, 3], name="base.html") self.assert_analysis([1, 4, 8], [8], name="specific.html") + def test_empty_parent_block_on_new_line_when_extended(self): + """ + When a block is empty and extended, endblock should not appear + as an uncovered line. + + https://github.com/coveragepy/django_coverage_plugin/issues/74 + """ + self.make_template(name="base.html", text="""\ + Hello + {% block content %} + {% endblock content %} + Goodbye + """) + self.make_template(name="child.html", text="""\ + {% extends "base.html" %} + {% block content %} + Override + {% endblock %} + """) + text = self.run_django_coverage(name="child.html") + self.assert_analysis([1, 2, 4], name="base.html") + self.assert_analysis([1, 3], name="child.html") + self.assertEqual(text.strip(), "Hello\n \n Override\n\nGoodbye") + + def test_non_empty_parent_block_when_extended(self): + self.make_template(name="base.html", text="""\ + Hello + {% block content %} + This line should be reported as uncovered. + {% endblock content %} + Goodbye + """) + self.make_template(name="child.html", text="""\ + {% extends "base.html" %} + {% block content %} + Override + {% endblock %} + """) + text = self.run_django_coverage(name="child.html") + self.assert_analysis([1, 2, 3, 5], missing=[3], name="base.html") + self.assert_analysis([1, 3], name="child.html") + + self.assertEqual(text.strip(), "Hello\n \n Override\n\nGoodbye") + + def test_nested_blocks_outer_endblock_on_its_own_line(self): + """ + When blocks are nested, on their own lines, and extended, + then endblock should not appear as uncovered. + + Ref: https://github.com/coveragepy/django_coverage_plugin/issues/74 + """ + self.make_template(name="base.html", text="""\ + {% block outer %} + {% block inner %} + {% endblock inner %} + {% endblock outer %} + """) + self.make_template(name="child.html", text="""\ + {% extends "base.html" %} + {% block inner %} + Override + {% endblock %} + """) + text = self.run_django_coverage(name="child.html") + self.assert_analysis([1, 2], missing=[], name="base.html") + self.assert_analysis([1, 3], name="child.html") + self.assertEqual(text.strip(), "Override") + class LoadTest(DjangoPluginTestCase): def test_load(self): From b0a833ec82f61be4aee8ede307e20bf27b973d6f Mon Sep 17 00:00:00 2001 From: Marc Gibbons <1726961+marcgibbons@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:05:09 -0400 Subject: [PATCH 2/3] fix: skipped tags falsely reported as uncovered when not at start of line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a tag ({% endif %}, {% endfor %}, {% else %}, etc.) is not at the start of a line, the preceding TEXT token ends with a whitespace-only fragment with no newline terminator. That partial line is not executable content but was incorrectly added to source_lines. Extends the endblock fix to apply universally — built-in and user-defined end tags are all covered by the same condition. --- django_coverage_plugin/plugin.py | 6 ++--- tests/test_flow.py | 42 +++++++++++++++++++++++++++++--- tests/test_simple.py | 11 +++++++++ 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/django_coverage_plugin/plugin.py b/django_coverage_plugin/plugin.py index 30adb8f..4037558 100644 --- a/django_coverage_plugin/plugin.py +++ b/django_coverage_plugin/plugin.py @@ -349,10 +349,10 @@ def lines(self): if lines[0].isspace(): lineno += 1 num_lines -= 1 - # When {% endblock %} is not at the start of a line, the - # preceding TEXT token ends with whitespace and no newline. + # When a tag is not at the start of a line, the preceding + # TEXT token ends with whitespace and no newline. # That partial line is not executable content. - if inblock and num_lines > 0 and ( + if num_lines > 0 and ( lines[-1].isspace() and not lines[-1].endswith(("\n", "\r")) ): num_lines -= 1 diff --git a/tests/test_flow.py b/tests/test_flow.py index db83fa8..7a3a5a1 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -25,6 +25,31 @@ def test_if(self): self.assertEqual(text.strip(), '') self.assert_analysis([1, 2], [2]) + def test_endif_not_at_start_of_line(self): + self.make_template("""\ +
+ {% if foo %} + Hello + {% endif %} + After +
+ """) + self.run_django_coverage(context={'foo': False}) + self.assert_analysis([1, 2, 3, 5, 6], missing=[3]) + + def test_else_not_at_start_of_line(self): + self.make_template("""\ +
+ {% if foo %} + Hello + {% else %} + Goodbye + {% endif %} +
+ """) + self.run_django_coverage(context={'foo': True}) + self.assert_analysis([1, 2, 3, 5, 7], missing=[5]) + def test_if_else(self): self.make_template("""\ {% if foo %} @@ -84,6 +109,17 @@ def test_loop(self): self.assertEqual(text, "Before\n\nAfter\n") self.assert_analysis([1, 2, 3, 5], [3]) + def test_endfor_not_at_start_of_line(self): + self.make_template("""\ + + """) + self.run_django_coverage(context={'items': []}) + self.assert_analysis([1, 2, 3, 5], missing=[3]) + def test_loop_with_empty_clause(self): self.make_template("""\ Before @@ -135,7 +171,7 @@ def test_regroup(self):
  • Japan
  • """)) - self.assert_analysis([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13]) + self.assert_analysis([1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 13]) class IfChangedTest(DjangoPluginTestCase): @@ -154,7 +190,7 @@ def test_ifchanged(self): 'items': [("A", "X"), ("A", "Y"), ("B", "Z"), ("B", "W")], }) self.assertEqual(squashed(text), 'AXYBZW') - self.assert_analysis([1, 2, 3, 4, 5]) + self.assert_analysis([1, 2, 3, 5]) def test_ifchanged_variable(self): self.make_template("""\ @@ -170,7 +206,7 @@ def test_ifchanged_variable(self): 'items': [("A", "X"), ("A", "Y"), ("B", "Z"), ("B", "W")], }) self.assertEqual(squashed(text), 'AXYBZW') - self.assert_analysis([1, 2, 3, 4, 5]) + self.assert_analysis([1, 2, 3, 5]) @django_stop_before(4, 0) diff --git a/tests/test_simple.py b/tests/test_simple.py index c4e326d..dea5ff2 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -238,6 +238,17 @@ def test_with(self): self.assertEqual(text, "\nalpha = 1, beta = 2.\n\n") self.assert_analysis([1, 2]) + def test_endwith_not_at_start_of_line(self): + self.make_template("""\ +
    + {% with alpha=1 %} + {{ alpha }} + {% endwith %} +
    + """) + self.run_django_coverage() + self.assert_analysis([1, 2, 3, 5]) + class StringTemplateTest(DjangoPluginTestCase): From d7734a32d2bc7a15bcc45674bb204887041e8b9c Mon Sep 17 00:00:00 2001 From: Marc Gibbons <1726961+marcgibbons@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:33:09 -0400 Subject: [PATCH 3/3] Update missed assertion when runing Django < 4 --- tests/test_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_flow.py b/tests/test_flow.py index 7a3a5a1..0bc7420 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -226,7 +226,7 @@ def test_ifequal(self): 'items': [(0, 'A'), (1, 'X'), (2, 'X'), (3, 'B')], }) self.assertEqual(squashed(text), '0X1X23') - self.assert_analysis([1, 2, 3, 4, 5]) + self.assert_analysis([1, 2, 3, 5]) def test_ifnotequal(self): self.make_template("""\ @@ -242,4 +242,4 @@ def test_ifnotequal(self): 'items': [(0, 'A'), (1, 'X'), (2, 'X'), (3, 'B')], }) self.assertEqual(squashed(text), 'X012X3') - self.assert_analysis([1, 2, 3, 4, 5]) + self.assert_analysis([1, 2, 3, 5])