Skip to content
VLSI Mentor

Python Part II · Module 29 · Coverage Automation

Coverage Deltas Run Over Run

Every coverage dashboard shows an arrow beside the number, and that arrow is produced by subtracting last night's percentage from tonight's. It is the most widely deployed wrong calculation in verification automation. A percentage is a ratio whose numerator and denominator both move between runs, so its difference blends two completely different events — work the team did, and code somebody merged — into one signed number that cannot be acted on. Worse, the single cheapest way to make that number go up involves no testing whatsoever.

Advanced21 min readPythonPart IIAutomationCoverageTrends

Part II · Module 29 · Chapter 29.4 · Coverage Deltas Run Over Run

1. What a Delta Is Asked to Answer

The arrow beside a coverage number is read as one thing and is asked to carry four:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
READINGS = [
    ("the team covered more bins", "progress — the only one anybody wants"),
    ("somebody merged new coverable code", "the denominator grew"),
    ("somebody excluded uncovered bins", "the denominator shrank — §6"),
    ("the covergroups were restructured", "bins renamed, not added — §5"),
]

print(f"{'what happened':<40} what it does to the number")
for what, effect in READINGS:
    print(f"{what:<40} {effect}")

print()
print("events a signed percentage delta can distinguish:", 1)
print("events that produce one:", len(READINGS))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
what happened                            what it does to the number
the team covered more bins               progress — the only one anybody wants
somebody merged new coverable code       the denominator grew
somebody excluded uncovered bins         the denominator shrank — §6
the covergroups were restructured        bins renamed, not added — §5

events a signed percentage delta can distinguish: 1
events that produce one: 4

Four causes, one symptom. A number that four unrelated events can produce is not a measurement of any of them, and the rest of this chapter is about getting back to the four.

2. The Annotation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
delta = today["functional"] - previous["functional"]

It runs, it produces a plausible number, and it is the line almost every coverage dashboard contains:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
previous = {"functional": 90.00, "branch": 88.20}
today = {"functional": 88.00, "branch": 89.10}

delta = today["functional"] - previous["functional"]
print(f"previous: {previous['functional']:.2f}%")
print(f"today   : {today['functional']:.2f}%")
print(f"delta   : {delta:+.2f} points")
print()
print("the dashboard will render:", "a red down-arrow")
print("the conclusion a reader draws:", "coverage got worse")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
previous: 90.00%
today   : 88.00%
delta   : -2.00 points

the dashboard will render: a red down-arrow
the conclusion a reader draws: coverage got worse

Hold on to that conclusion. The next section supplies the two numbers the annotation threw away, and they say the opposite.

3. The Percentage Fell and the Work Advanced

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
RUNS = [
    ("Monday", 900, 1000),
    ("Friday", 1056, 1200),
]

for day, hits, total in RUNS:
    print(f"{day:<8} {hits:>5} / {total:<5} = {100.0*hits/total:>6.2f}%")

(_, h0, t0), (_, h1, t1) = RUNS
print()
print(f"percentage         : {100.0*h1/t1 - 100.0*h0/t0:+.2f} points")
print(f"bins covered       : {h1 - h0:+d}")
print(f"bins coverable     : {t1 - t0:+d}")
print(f"bins still uncovered, Monday: {t0 - h0}")
print(f"bins still uncovered, Friday: {t1 - h1}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Monday     900 / 1000  =  90.00%
Friday    1056 / 1200  =  88.00%

percentage         : -2.00 points
bins covered       : +156
bins coverable     : +200
bins still uncovered, Monday: 100
bins still uncovered, Friday: 144

The team covered 156 bins in a week. The headline fell two points, and the uncovered count rose from 100 to 144 — so in the sense the dashboard means, coverage really did get worse, and in the sense the team means, a great deal of work got done. Both are true, which is precisely why one signed number cannot represent the week.

This is Chapter 29.1 Section 6's argument arriving at its destination. A flow that stored only percentages has lost the ability to make the distinction permanently; no cleverness in the comparison layer can reconstruct 156 from 90.00 and 88.00.

4. Decomposing the Delta Exactly

The two causes can be separated, and not approximately — the split is an algebraic identity:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
h0, t0 = 900, 1000
h1, t1 = 1056, 1200

p0 = 100.0 * h0 / t0
p1 = 100.0 * h1 / t1

numerator_effect = 100.0 * (h1 - h0) / t1
denominator_effect = 100.0 * h0 * (1.0 / t1 - 1.0 / t0)

print(f"before {h0}/{t0} = {p0:.2f}%")
print(f"after  {h1}/{t1} = {p1:.2f}%")
print()
print(f"effect of covering {h1-h0} bins   : {numerator_effect:+.2f} points")
print(f"effect of adding {t1-t0} coverable bins: {denominator_effect:+.2f} points")
print(f"sum                              : {numerator_effect + denominator_effect:+.2f} points")
print(f"actual delta                     : {p1 - p0:+.2f} points")
print(f"exact, not approximate           : {abs((numerator_effect + denominator_effect) - (p1 - p0)) < 1e-9}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
before 900/1000 = 90.00%
after  1056/1200 = 88.00%

effect of covering 156 bins   : +13.00 points
effect of adding 200 coverable bins: -15.00 points
sum                              : -2.00 points
actual delta                     : -2.00 points
exact, not approximate           : True

+13.00 from work, −15.00 from new code, −2.00 net. That is the week, reported in a form a reader can act on: the verification effort was worth thirteen points and the design grew by fifteen.

The identity is worth a line of algebra, because knowing it is exact is what makes it safe to publish:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
100·(h₁−h₀)/t₁  +  100·h₀·(1/t₁ − 1/t₀)
  = 100·h₁/t₁ − 100·h₀/t₁ + 100·h₀/t₁ − 100·h₀/t₀
  = 100·h₁/t₁ − 100·h₀/t₀

The middle terms cancel, so the two effects sum to the delta with no residual. There is no third term, no interaction correction, and no ordering choice to defend — which is unusual for a decomposition and is the reason to prefer this one over the obvious alternative of attributing the numerator effect at the old denominator.

5. A Renamed Bin Is Not a New Bin

Counts cannot see a rename, and covergroups get restructured constantly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
before = {"cg.burst.len4", "cg.burst.len8", "cg.mode.idle", "cg.mode.run"}
after = {"cg.burst.len_4", "cg.burst.len8", "cg.mode.idle", "cg.mode.run",
         "cg.err.parity"}

print(f"bins before: {len(before)}")
print(f"bins after : {len(after)}")
print(f"net change : {len(after) - len(before):+d}")
print()
print("appeared   :", sorted(after - before))
print("disappeared:", sorted(before - after))
print("unchanged  :", len(before & after))
print()
print(f"the net count of {len(after)-len(before):+d} is the sum of "
      f"{len(after-before)} appearances and {len(before-after)} disappearances")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
bins before: 4
bins after : 5
net change : +1

appeared   : ['cg.burst.len_4', 'cg.err.parity']
disappeared: ['cg.burst.len4']
unchanged  : 3

the net count of +1 is the sum of 2 appearances and 1 disappearances

A net +1 describes two genuinely different events: one bin was really added (cg.err.parity) and one was renamed (len4 to len_4). The rename matters because cg.burst.len4 may well have been covered — and its replacement starts at zero, so the numerator drops for a reason that has nothing to do with the design or the tests.

Chapter 7.4's set operations are the whole tool here. Compare the names:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
BEFORE = {"cg.burst.len4": 40, "cg.burst.len8": 40, "cg.mode.idle": 10,
          "cg.mode.run": 10}
AFTER = {"cg.burst.len_4": 0, "cg.burst.len8": 40, "cg.mode.idle": 10,
         "cg.mode.run": 12, "cg.err.parity": 0}

appeared = sorted(set(AFTER) - set(BEFORE))
gone = sorted(set(BEFORE) - set(AFTER))
common = sorted(set(BEFORE) & set(AFTER))

print("hits lost to bins that no longer exist:",
      sum(BEFORE[b] for b in gone))
print("hits gained in bins that did not exist:",
      sum(AFTER[b] for b in appeared))
print("hits moved within bins present in both:",
      sum(AFTER[b] - BEFORE[b] for b in common))
print()
print(f"total before: {sum(BEFORE.values())}")
print(f"total after : {sum(AFTER.values())}")
print(f"net         : {sum(AFTER.values()) - sum(BEFORE.values()):+d}")
print()
print("so a net of", sum(AFTER.values()) - sum(BEFORE.values()), "decomposes as:")
print("   -40 lost with a disappeared bin")
print("    +0 gained with bins that are new")
print("    +2 of real progress in bins present in both")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
hits lost to bins that no longer exist: 40
hits gained in bins that did not exist: 0
hits moved within bins present in both: 2

total before: 100
total after : 62
net         : -38

so a net of -38 decomposes as:
   -40 lost with a disappeared bin
    +0 gained with bins that are new
    +2 of real progress in bins present in both

The three components sum to the net exactly: −40 + 0 + 2 = −38. And only the last of them is verification progress.

A week reported as "coverage hits fell by 38" was a week in which the team covered two more things and a covergroup was restructured. A comparator that reports the three components separately makes the rename self-evident; one that reports the net reports a number that is an accounting artefact with a week of real work buried inside it.

6. An Exclusion Is Not Progress

This is the number to remember from this chapter:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
h, t = 900, 1000
base = 100.0 * h / t

print(f"starting point      : {h}/{t} = {base:.2f}%")
print(f"uncovered bins       : {t - h}")
print()
for excluded in (25, 50, 100):
    new_total = t - excluded
    print(f"exclude {excluded:>3} uncovered bins -> {h}/{new_total} = "
          f"{100.0*h/new_total:>6.2f}%   ({100.0*h/new_total - base:+.2f} points)")

print()
print("tests written:", 0)
print("bins covered :", 0)
print("lines of RTL exercised:", 0)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
starting point      : 900/1000 = 90.00%
uncovered bins       : 100

exclude  25 uncovered bins -> 900/975 =  92.31%   (+2.31 points)
exclude  50 uncovered bins -> 900/950 =  94.74%   (+4.74 points)
exclude 100 uncovered bins -> 900/900 = 100.00%   (+10.00 points)

tests written: 0
bins covered : 0
lines of RTL exercised: 0

One commit excluding every uncovered bin takes this design from 90% to a clean 100%, and the dashboard renders a ten-point green arrow. No test ran. Nothing was verified.

That is not a hypothetical abuse; it is the ordinary consequence of a legitimate activity. Excluding genuinely unreachable bins is correct and necessary work — Chapter 29.5 is about exactly which bins deserve it — and every one of those legitimate exclusions raises the percentage without testing anything. So a flow cannot treat exclusions as suspicious, and it cannot treat percentage movement as progress either. It has to report them as separate facts:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
BEFORE = {"hits": 900, "total": 1000, "excluded": 0}
AFTER = {"hits": 912, "total": 950, "excluded": 50}

p0 = 100.0 * BEFORE["hits"] / BEFORE["total"]
p1 = 100.0 * AFTER["hits"] / AFTER["total"]

print(f"headline moved: {p0:.2f}% -> {p1:.2f}%  ({p1-p0:+.2f} points)")
print()
print(f"  bins covered      : {AFTER['hits'] - BEFORE['hits']:+d}")
print(f"  bins excluded     : {AFTER['excluded'] - BEFORE['excluded']:+d}")
print(f"  coverable bins    : {AFTER['total'] - BEFORE['total']:+d}")
print()
hits_only = 100.0 * AFTER["hits"] / BEFORE["total"]
print(f"headline if NOTHING had been excluded: {hits_only:.2f}%  "
      f"({hits_only - p0:+.2f} points)")
print(f"points attributable to the exclusion : {p1 - hits_only:+.2f}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
headline moved: 90.00% -> 96.00%  (+6.00 points)

  bins covered      : +12
  bins excluded     : +50
  coverable bins    : -50

headline if NOTHING had been excluded: 91.20%  (+1.20 points)
points attributable to the exclusion : +4.80

Six points of apparent movement: 1.20 earned by covering twelve bins, and 4.80 granted by an exclusion commit. A reviewer who sees only +6.00 has no way to ask the right question, and the right question — what was excluded, and why? — is Chapter 29.5's subject and Chapter 29.6's evidence requirement.

7. Two Points Are Not a Trend

The chapter's title says trend, and the canonical purpose is explicit that a single number is not one. Here is why, with two series that a one-step delta cannot tell apart:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
flat = [88.0, 88.4, 87.9, 88.2, 88.1, 88.3]
rising = [88.0, 88.9, 89.7, 90.6, 91.4, 91.6]

for name, s in (("flat", flat), ("rising", rising)):
    print(f"{name:<7} {s}")
    print(f"{'':<7} last delta {s[-1]-s[-2]:+.2f}   "
          f"spread {max(s)-min(s):.2f}   "
          f"total move {s[-1]-s[0]:+.2f}")

print()
print("both have a positive last delta:",
      flat[-1] - flat[-2] > 0, rising[-1] - rising[-2] > 0)
print("the deltas alone are indistinguishable:",
      abs((flat[-1]-flat[-2]) - 0.20) < 1e-9)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
flat    [88.0, 88.4, 87.9, 88.2, 88.1, 88.3]
        last delta +0.20   spread 0.50   total move +0.30
rising  [88.0, 88.9, 89.7, 90.6, 91.4, 91.6]
        last delta +0.20   spread 3.60   total move +3.60

both have a positive last delta: True True
the deltas alone are indistinguishable: True

Both series end with the same +0.20 step. One is six weeks of a project going nowhere; the other is six weeks of steady closure that happens to have ended on a quiet night. The last delta is identical, and a dashboard showing only the last delta showed the same green arrow for both.

What distinguishes them is history, and the cheapest useful amount of it is "enough runs to see the spread":

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
flat = [88.0, 88.4, 87.9, 88.2, 88.1, 88.3]
rising = [88.0, 88.9, 89.7, 90.6, 91.4, 91.6]


def describe(series):
    moves = [b - a for a, b in zip(series, series[1:])]
    up = sum(1 for m in moves if m > 0)
    return {
        "runs": len(series),
        "net": series[-1] - series[0],
        "spread": max(series) - min(series),
        "steps_up": up,
        "steps_down": len(moves) - up,
        "monotonic": all(m > 0 for m in moves),
    }


for name, s in (("flat", flat), ("rising", rising)):
    d = describe(s)
    print(f"{name:<7} runs {d['runs']}  net {d['net']:+.2f}  "
          f"spread {d['spread']:.2f}  up {d['steps_up']}  "
          f"down {d['steps_down']}  monotonic {d['monotonic']}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
flat    runs 6  net +0.30  spread 0.50  up 3  down 2  monotonic False
rising  runs 6  net +3.60  spread 3.60  up 5  down 0  monotonic True

Five steps up out of five and a net of +3.60 against three up, two down and a net of +0.30. That is a trend, reported as four numbers over six runs. Not one of them is available from a pair.

8. The Comparison, as a Flow

Everything above is one pipeline, and it has a specific order: comparability is checked before names, names before counts, and the decomposition happens last because it needs the sets to be reconciled first.

Comparing two coverage runs

A seven-stage flow for comparing two coverage runs: load both records, check comparability, compare bin names, split the bin sets, reconcile hits, decompose the delta, and publish with history01LOAD BOTH RECORDShits, total, excluded, layout02CHECK COMPARABILITYsame tool, layout and scope03COMPARE BIN NAMESnot counts — a rename hides in a net04SPLIT THE SETSappeared, disappeared, common05RECONCILE HITSlost with bins, gained with bins, moved06DECOMPOSE THE DELTAnumerator, denominator, exclusion07PUBLISH WITH HISTORYN runs, spread, steps up and down
Seven stages. The two checks come first because a delta between incomparable runs is not a small error — it is a number with no meaning, and the decomposition in stage 6 is exact only once stages 3 to 5 have reconciled the bin sets.

Stage 2 is the one most flows omit, and Chapter 29.1 Section 8's layout field is what makes it possible: two runs parsed by different layout handlers after a tool upgrade produce a delta that is an artefact of the parser. Stage 7 is what makes the output a trend rather than an arrow.

9. The Comparator, Assembled

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
from dataclasses import dataclass
from typing import Dict, Optional


@dataclass(frozen=True)
class CoverageRun:
    label: str
    metric: str
    layout: str
    scope: str
    excluded: int
    bins: Dict[str, int]      # bin name -> hits

    @property
    def hits(self):
        return sum(1 for v in self.bins.values() if v > 0)

    @property
    def total(self):
        return len(self.bins)

    @property
    def pct(self):
        return 100.0 * self.hits / self.total


def compare(a: CoverageRun, b: CoverageRun) -> Optional[dict]:
    if (a.metric, a.layout, a.scope) != (b.metric, b.layout, b.scope):
        return {"comparable": False,
                "reason": f"{(a.metric, a.layout, a.scope)} vs {(b.metric, b.layout, b.scope)}"}

    appeared = sorted(set(b.bins) - set(a.bins))
    gone = sorted(set(a.bins) - set(b.bins))
    common = sorted(set(a.bins) & set(b.bins))

    covered_in_common = sum(
        (1 if b.bins[k] > 0 else 0) - (1 if a.bins[k] > 0 else 0) for k in common)
    lost_with_bins = sum(1 for k in gone if a.bins[k] > 0)
    gained_with_bins = sum(1 for k in appeared if b.bins[k] > 0)

    num_eff = 100.0 * (b.hits - a.hits) / b.total
    den_eff = 100.0 * a.hits * (1.0 / b.total - 1.0 / a.total)

    return {
        "comparable": True,
        "pct_before": round(a.pct, 2),
        "pct_after": round(b.pct, 2),
        "delta": round(b.pct - a.pct, 2),
        "numerator_effect": round(num_eff, 2),
        "denominator_effect": round(den_eff, 2),
        "identity_holds": abs((num_eff + den_eff) - (b.pct - a.pct)) < 1e-9,
        "appeared": appeared,
        "disappeared": gone,
        "progress_in_common": covered_in_common,
        "lost_with_bins": lost_with_bins,
        "gained_with_bins": gained_with_bins,
        "excluded_delta": b.excluded - a.excluded,
    }


MON = CoverageRun("Mon", "functional", "covergroup-list", "tb.dut", 0, {
    "cg.burst.len4": 3, "cg.burst.len8": 1, "cg.mode.idle": 7,
    "cg.mode.run": 2, "cg.err.parity": 0, "cg.err.crc": 0,
})
FRI = CoverageRun("Fri", "functional", "covergroup-list", "tb.dut", 1, {
    "cg.burst.len_4": 0, "cg.burst.len8": 4, "cg.mode.idle": 9,
    "cg.mode.run": 5, "cg.err.parity": 2, "cg.err.crc": 0,
    "cg.err.timeout": 0,
})

r = compare(MON, FRI)
print("comparable:", r["comparable"])
print(f"headline  : {r['pct_before']:.2f}% -> {r['pct_after']:.2f}%  ({r['delta']:+.2f})")
print()
print(f"  numerator effect   {r['numerator_effect']:+.2f}")
print(f"  denominator effect {r['denominator_effect']:+.2f}")
print(f"  identity holds     {r['identity_holds']}")
print()
print("  appeared    :", r["appeared"])
print("  disappeared :", r["disappeared"])
print(f"  hits lost with disappeared bins : {r['lost_with_bins']}")
print(f"  hits gained with new bins       : {r['gained_with_bins']}")
print(f"  progress in bins present in both: {r['progress_in_common']}")
print(f"  exclusions added                : {r['excluded_delta']}")

print()
BAD = CoverageRun("Fri", "functional", "kv-block", "tb.dut", 0, {"cg.a": 1})
print("after a tool upgrade:", compare(MON, BAD))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
comparable: True
headline  : 66.67% -> 57.14%  (-9.52)

  numerator effect   +0.00
  denominator effect -9.52
  identity holds     True

  appeared    : ['cg.burst.len_4', 'cg.err.timeout']
  disappeared : ['cg.burst.len4']
  hits lost with disappeared bins : 1
  hits gained with new bins       : 0
  progress in bins present in both: 1
  exclusions added                : 1

after a tool upgrade: {'comparable': False, 'reason': "('functional', 'covergroup-list', 'tb.dut') vs ('functional', 'kv-block', 'tb.dut')"}

Read the middle block rather than the headline, because this run pair contains something the headline cannot express: the numerator effect is exactly zero. Four bins were covered on Monday and four on Friday, so the entire −9.52 came from the denominator growing by one bin.

A flow that reported "numerator effect 0.00" and stopped would be telling a second untruth, though — nothing stood still. One covered bin was lost to the rename (cg.burst.len4 had hits, cg.burst.len_4 starts at zero) and one bin was genuinely covered (cg.err.parity went from 0 to 2). The two cancelled:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
hits lost with disappeared bins  -1
hits gained with new bins        +0
progress in bins present in both +1
                                 --
net change in covered bins        0

A zero is the sum of a loss and a gain, and only the component view says so. That is six facts where the annotation produced one, and it is the reason stage 5 of the flow exists separately from stage 6.

And the last line is the stage-2 check doing its job: a run parsed by a different layout handler is refused rather than subtracted. Chapter 28.5 Section 2 made the same demand of two QoR runs — comparability is a precondition, not a caveat.

10. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
h0, t0, h1, t1 = 900, 1000, 1056, 1200

# A
print("A:", f"{100.0*h1/t1 - 100.0*h0/t0:+.2f}")

# B
print("B:", h1 - h0, t1 - t0)

# C
print("C:", f"{100.0*(h1-h0)/t1:+.2f}", f"{100.0*h0*(1.0/t1-1.0/t0):+.2f}")

# D
print("D:", f"{100.0*900/900:.2f}")

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: -2.00
B: 156 200
C: +13.00 -15.00
D: 100.00

A — the annotation's answer: two points down.

B — the two numbers it discarded. 156 bins covered, 200 added.

C — the exact decomposition. Thirteen points earned, fifteen points of new code.

D — 900 of 900. Excluding every uncovered bin reads as perfect closure, which is Section 6 and the reason Chapter 29.5 exists.

11. Exercises

Exercise 1 — four causes. List the four events that produce a negative coverage delta in your flow and say which you could currently distinguish.

Exercise 2 — the discarded pair. Take a week of your own dashboard history and say whether the bin counts behind it are recoverable.

Exercise 3 — your own inversion. Find a week in your project where coverage fell and the team covered more bins. Report all four numbers.

Exercise 4 — decompose. Implement the numerator and denominator effects and verify the identity on your own data.

Exercise 5 — the other attribution. Compute the numerator effect at the old denominator and say why mixing the two in one table is wrong.

Exercise 6 — a rename. Find a renamed bin in your covergroup history and say what it did to the numerator.

Exercise 7 — three components. Report hits lost with disappeared bins, hits gained with new bins, and progress in common bins, for two of your runs.

Exercise 8 — the exclusion arithmetic. For your design, compute the headline you would get by excluding every uncovered bin.

Exercise 9 — split the movement. Take a real exclusion commit and separate the points earned from the points granted.

Exercise 10 — report the count. Add the excluded-bin count beside your headline and say what it lets a reviewer ask.

Exercise 11 — two series. Construct a flat series and a rising series with the same last delta, and say which four numbers separate them.

Exercise 12 — enough history. Decide how many runs your trend view should carry and justify the number.

Exercise 13 — stage 2. Add the comparability check to your comparator and run it across a tool upgrade boundary.

Exercise 14 — the order of stages. Explain why names are compared before counts and the decomposition happens last.

Exercise 15 — six facts. Take the Section 9 output and write the two sentences you would put in a weekly status mail.

Exercise 16 — all four causes in one week. Find a week in your history containing at least two of the four causes, and separate them.

Exercise 17 — the identity by hand. Write out the cancellation that makes the two effects sum to the delta exactly.

Exercise 18 — residual check. Add an assertion that your decomposition's two terms sum to the observed delta, and run it over a month.

Exercise 19 — zero is a sum. Construct a run pair whose numerator effect is zero because a loss and a gain cancelled, and report the three components.

Exercise 20 — the rename hunt. Diff two runs' bin-name sets and list every name that appeared and disappeared on the same night.

Exercise 21 — a covered bin lost. Say what happens to the numerator when a covered bin is renamed, and why no arithmetic over totals shows it.

Exercise 22 — exclusions beside the headline. Add the exclusion count and its weekly delta to your dashboard, and say what question it enables.

Exercise 23 — earned versus granted. For one real week, split your movement into points earned by covering and points granted by excluding.

Exercise 24 — the four trend numbers. Compute net, spread, steps up and steps down over your last eight runs.

Exercise 25 — stage order. Explain what breaks if the decomposition runs before the bin sets are reconciled.

12. Summary

  • Four different events produce a coverage delta and a signed percentage can distinguish exactly one of them.
  • The percentage fell two points in a week when the team covered 156 more bins. Both readings are true, which is why one number cannot carry the week.
  • The delta decomposes exactly: 100·(h₁−h₀)/t₁ + 100·h₀·(1/t₁ − 1/t₀) equals the delta with no residual — +13.00 of work and −15.00 of new code.
  • Attribute the numerator effect at the new denominator, state that you did, and never mix the two attributions in one table.
  • Counts cannot see a rename. A net +1 was two appearances and one disappearance, and the disappeared bin was covered.
  • Compare bin names and report three components: hits lost with bins, hits gained with bins, and progress in bins present in both. Only the third is verification progress.
  • Excluding 100 uncovered bins takes 90.00% to 100.00% with no testing at all — and legitimate exclusion work has exactly the same effect, so the defence is reporting the count, not suspicion.
  • Separate points earned from points granted: +6.00 of movement was +1.20 of covering and +4.80 of excluding.
  • Two points are not a trend. A flat series and a six-week climb had the same +0.20 last step.
  • A trend is four numbers over N runs — net, spread, steps up, steps down — and none of them exists in a pair.
  • Check comparability first. A delta across a tool-layout change is an artefact of the parser, and the layout field is what detects it.
  • A zero numerator effect can be the sum of a loss and a gain — one covered bin lost to a rename and one genuinely covered, cancelling exactly.
  • Assert the decomposition's residual is zero. It is an identity, so a non-zero residual means a bug in the comparator rather than an interesting result.
  • A renamed bin's hits do not transfer. The new name starts at zero, and totals alone cannot show it.
  • Report the exclusion count and its weekly delta, so legitimate exclusion work and a shrinking target look different.
  • Reconcile the bin sets before decomposing; the two effects are only meaningful once appearances and disappearances are accounted for.

The next chapter takes the uncovered bins this one has been counting and asks which of them anybody should ever work on.

Where this fits

Part of the Python curriculum.