Template Coverage

Track which template lines execute during rendering with LCOV and Cobertura export

4 min read 805 words

Kida can track which template lines execute during rendering and export coverage data in standard formats (LCOV, Cobertura XML). Coverage collection is opt-in — it has zero overhead when disabled because the compiler-emitted_rc.line = N markers only record hits when a CoverageCollectoris active.

from kida.coverage import CoverageCollector

Quick Start

from kida import Environment, FileSystemLoader
from kida.coverage import CoverageCollector

env = Environment(loader=FileSystemLoader("templates/"))
cov = CoverageCollector()

with cov:
    template = env.get_template("page.html")
    template.render(title="Hello")

print(cov.summary())
cov.write_lcov("coverage.lcov")

Output:

Template                                            Lines    Hit   Cover
-----------------------------------------------------------------------
page.html                                               8      8  100.0%
-----------------------------------------------------------------------
TOTAL                                                   8      8  100.0%

CoverageCollector API

CoverageCollector is the primary interface. It dynamically patches RenderContext.__setattr__while active, so there is zero overhead when no collector is running.

Context Manager

The recommended usage is as a context manager:

with CoverageCollector() as cov:
    template.render(**context)
# cov now contains all line hits from the render

Manual Start / Stop

For longer-lived collection (e.g., across an entire test suite):

cov = CoverageCollector()
cov.start()

# ... render many templates ...

cov.stop()
print(cov.summary())

Calling start() on an already-started collector is a no-op. Calling stop() resets the ContextVartoken and decrements the internal reference count.

Raw Data

Access the raw coverage data directly:

cov.data
# {"page.html": {1, 3, 5, 7, 8}, "base.html": {2, 4, 6}}

The data property returns a dict[str, set[int]]mapping template names to sets of executed line numbers.

Clearing Data

Reset collected data without creating a new collector:

cov.clear()

Getting Results

get_results() returns a list of CoverageResultobjects, one per template, sorted by name:

results = cov.get_results()
for r in results:
    print(f"{r.template_name}: {r.hit_count}/{r.total_count} ({r.percentage:.1f}%)")

Pass an optional source_mapto provide full line enumeration for accurate miss counting:

source_map = {
    "page.html": frozenset({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}),
}
results = cov.get_results(source_map=source_map)
for r in results:
    print(f"{r.template_name}: {r.missed_count} lines missed")

Without a source_map, total_lines equals executed_lines(100% coverage for all touched templates). This is useful for hit tracking even without full line enumeration.

CoverageResult

Property Type Description
template_name str Template identifier
executed_lines frozenset[int] Line numbers that executed
total_lines frozenset[int] All trackable line numbers
hit_count int Number of executed lines (property)
total_count int Total trackable lines (property)
missed_count int Lines not executed (property)
percentage float Coverage percentage (property)

Text Summary

summary()returns a formatted text table of coverage across all templates:

print(cov.summary(source_map=source_map))

If no templates were rendered, summary() returns "No templates rendered.".

LCOV Export

Write coverage data in LCOV tracefile format, compatible withlcov, genhtml, and most CI coverage tools:

cov.write_lcov("coverage.lcov")

Or get the LCOV content as a string:

lcov_string = cov.format_lcov()

The output follows the standard LCOV format:

SF:page.html
DA:1,1
DA:3,1
DA:5,1
LH:3
LF:3
end_of_record

Each DA: line records a line number and its hit count (always 1 for any executed line). LH is the number of lines hit, LFis the total number of lines in the record.

Cobertura Export

Write coverage data in Cobertura XML format, compatible with Jenkins, GitLab CI, and other CI systems:

cov.write_cobertura("coverage.xml")

Or get the XML content as a string:

xml_string = cov.format_cobertura()

The output follows the Cobertura schema. Templates are grouped under a single "templates" package, with each template represented as a class. The root element includes a line-rateattribute with the aggregate coverage ratio.

Thread Safety

CoverageCollector uses ContextVar for data isolation and a threading.Lockfor the global reference count. This means:

  • Concurrent renders — each collector tracks only its own renders; parallel test runners or async handlers do not interfere with each other.
  • Nested collectors — multipleCoverageCollector instances can be active simultaneously. The RenderContext.__setattr__ patch stays active as long as at least one collector is running (reference-counted via _active_count).
  • Cleanup — when the last active collector stops, the__setattr__patch is removed entirely, restoring zero-overhead rendering.

CI Integration Example

pytest + LCOV

import pytest
from kida.coverage import CoverageCollector

@pytest.fixture(scope="session")
def template_coverage():
    cov = CoverageCollector()
    cov.start()
    yield cov
    cov.stop()
    cov.write_lcov("template-coverage.lcov")
    cov.write_cobertura("template-coverage.xml")

def test_homepage(template_coverage, env):
    template = env.get_template("home.html")
    result = template.render(title="Home")
    assert "Home" in result

def test_about(template_coverage, env):
    template = env.get_template("about.html")
    result = template.render(title="About")
    assert "About" in result

GitHub Actions

- name: Run tests with template coverage
  run: pytest --tb=short

- name: Upload template coverage
  uses: codecov/codecov-action@v4
  with:
    files: template-coverage.lcov
    flags: templates

GitLab CI

test:
  script:
    - pytest --tb=short
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: template-coverage.xml

See Also