← 목록

I Wrote a Tool to Find Blind Spots. It Had One.

devto 2026-08-28 원문 보기 ↗


I spend a lot of time writing quality gates to enforce invariants in my software. For weeks, my test runner stayed green. Every guard was in place, every check passed, and every test suite reported clean execution.

Then I asked an uncomfortable question: are these gates actually measuring anything, or are they just executing without complaining?

To find out, I wrote a script to sabotage my own engine. The tool parses the source code, finds every fail() assertion call in the engine, disables them one by one, and re-runs the entire mutation test suite for each line. It asks a simple question: if I delete this guard line, does any test notice?

If no test notices, that line is uncovered. It isn't a protection; it's a comment pretending to be a guard.

I expected to find a few gaps. I even wrote that expectation directly into the script's exit codes, annotating exit code 1 as (expected in v2.4.1). I knew I had blind spots—I just didn't know how many.

When the measurement finished across 61 protection lines in the engine, 40 of them were completely unmeasured by any test—a 66% blind spot rate.

Yet, the baseline test suite run right beside it reported: 36/36 biting mutants. A 100% green test suite, sitting on top of an engine where two-thirds of the assertions could be erased without triggering a single alert.

0  her fail() kapsamli (hicbir kor nokta yok)
1  en az bir KAPSAMSIZ fail() var  (v2.4.1'de beklenen)
2  OLCULEMEDI / kurulum hatasi

# translation:
# 0  every fail() is covered (no blind spots)
# 1  at least one UNCOVERED fail() exists (expected in v2.4.1)
# 2  UNMEASURABLE / setup error

Three ways a gate dies

A quality gate usually dies in one of three ways. All three look identical from the outside: they give you a green checkmark.

Most engineering setups have answers for the first two: linters, pipeline validation, and coverage runners. Almost nobody writes tests for the third.

Handling environmental failure requires thinking about what happens when the measurement tool itself gets corrupted by the environment. For instance, in my repository, the hukum_kapisi.py gate uses explicit ASCII character patterns for string matching rather than UTF-8 dashes. Why? Because when an encoding error triggers Python's errors="replace" handler, a hyphen or dash can be printed as a ?. If the gate matched against unicode dashes, the very encoding failure it was designed to catch would corrupt the pattern matcher itself.

There is a vast difference between "I wrote a gate" and "I thought about what happens to the gate when its environment breaks."

Level 1: mutate the gate — and why it isn't enough

The standard response to testing quality gates is Level 1 mutation: intentionally break the gate's code, run it against dirty input, and confirm that exit != 0.

This is a good reflex, but it is incomplete. A gate requires two-way proof:

If you only test that a gate triggers on bad input, you risk keeping a gate that constantly fires false positives on clean environments.

More importantly, proving that a gate bites on a synthetic input does not prove that it covers the entire class of errors it claims to guard. Having a single mutant test pass against a gate gives you a false sense of security.

Level 2: mutate the engine, ask if any gate notices

Level 2 flips the approach: don't mutate the gate. Mutate the engine that the gates are guarding, then check if any gate notices the change.

My script, sabotaj.py, automates this process through three specific design rules:

# 1) fail() cagri yerlerini AST ile bul  (regex DEGIL: cok satirli cagrilar var)
# translation: 1) Find fail() call sites via AST (NOT regex: multi-line calls exist)
def fail_cagrilari(kaynak):
    agac = ast.parse(kaynak)
    bulunan = []
    for d in ast.walk(agac):
        if isinstance(d, ast.Call) and isinstance(d.func, ast.Name) and d.func.id == "fail":
            ...

def sabote_et(kaynak, hedef):
    """Tek bir fail(...) cagrisini `None` ile degistir. Kaynagi DEGISTIRMEZ."""
    # translation: Replace a single fail(...) call with `None`. Does NOT modify source.
    ...
    compile(yeni, "<sabotaj>", "exec")   # sozdizimi bozulduysa burada patlar
    # translation: # explodes here if syntax is broken
    return yeni

When I ran this analysis against hafiza.py, the core value was not the total number of 40 unmeasured lines—it was how those blind spots clustered. Looking at key selected gates from the results:

Gate Identifier Comprehensive (Covered) Uncovered (Blind)
H1 0 6
H9 0 1
H11 1 10
H8 2 4
H3 / H7 / H15 1 / 1 / 2 0

Gate H11 is the clearest example. Out of 11 fail() protection calls inside H11, 10 were completely unmeasured. Yet, because 1 mutant (M-H11) triggered a single line, the gate marked itself as "tested" in naive summary reports. Testing a single sink line with one mutant does not cover the gate's invariant class.

The third verdict — and the day my own tool lost it

Every measurement tool must support a third verdict: UNMEASURABLE (ÖLÇÜLEMEDİ).

Binary PASS/FAIL models force tools to make false statements when execution environment issues prevent proper testing. I learned this when an earlier CI run hit a read-only filesystem restriction, threw a raw PermissionError, and returned exit code 1. The contract declared 1 = MUTANT ESCAPED. The runner had reported an unmeasurable state as if it were a legitimate test failure. Fake failures destroy trust in real failures.

While reading through sabotaj.py line by line to prepare this writeup, I looked at how main() evaluates its final exit code:

if olculemedi and not kapsamsiz:
    print("HUKUM: OLCULEMEDI kalemler var — 'tam kapsamli' DEMEK YASAK.")
    return 2
if kapsamsiz:
    print("HUKUM: %d KOR NOKTA var." % len(kapsamsiz))
    return 1

# translation:
# if unmeasured and not uncovered:
#     print("VERDICT: UNMEASURABLE items exist — FORBIDDEN to claim 'fully covered'.")
#     return 2
# if uncovered:
#     print("VERDICT: %d BLIND SPOTS exist." % len(uncovered))
#     return 1

Look at the logical condition in that first branch: if olculemedi and not kapsamsiz:.

If the script encountered both unmeasurable compilation errors AND uncovered lines during a run, not kapsamsiz evaluated to False. The code bypassed the first block entirely and dropped straight into if kapsamsiz:, returning exit code 1. The warning explicitly forbidding a claim of full coverage would never execute, and exit code 2 was swallowed.

The third verdict existed in a print statement, but it had dropped out of the exit code contract.

In my actual measurement run, ÖLÇÜLEMEDİ happened to be 0, so the script reported its final count correctly. I didn't find this bug because a test failed or an alert fired—I found it because I read the source code line by line.

It was a latent defect sitting silently inside the script's main() function. The tool I wrote to find blind spots had a blind spot in its contract logic.

What this does NOT tell you

To maintain clear boundaries around what these metrics mean, three constraints must be stated directly:

  kacanlar = sorted(set(re.findall(r"(M-[A-Z0-9a-z_]+)\s+.*?KACTI", cikti)))
  if not kacanlar:
      kacanlar = sorted(set(m for m in re.findall(r"^\s*(M-\S+).*KACTI", cikti, re.M)))

If both regexes fail to match an escaped mutant name in the log output, kacan evaluates to 0, which defaults the verdict to KAPSAMSIZ (uncovered). The tool that uses strict AST parsing for input files relies on fragile string parsing for output reading.

Try it on your repo

If you want to verify your own quality gates, the approach can be implemented in any codebase using a simple sequence:

For every fix you apply to a system, you owe it a sabotage: strip the fix out temporarily, rerun the suite, and verify that the exact target error reproduces. If removing the fix doesn't bring back the original failure, your test is measuring a neighboring component, not the bug.

Measured at commit a86ffe4 (a86ffe440c0740241fdf20387c14420029ec0555), 1 Aug 2026.

To verify locally:

git clone https://github.com/onur-kesim/hafiza-kur.git
cd hafiza-kur
git checkout a86ffe4
python faz0/sabotaj.py --motor skill/scripts/hafiza.py

# The run above prints the counts. To see the defect itself — the branch that
# swallows the third verdict — read the contract logic directly:
grep -n -A 4 "if olculemedi and not kapsamsiz" faz0/sabotaj.py