惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Blog — PlanetScale
Blog — PlanetScale
SecWiki News
SecWiki News
Google DeepMind News
Google DeepMind News
WordPress大学
WordPress大学
小众软件
小众软件
C
CERT Recently Published Vulnerability Notes
Jina AI
Jina AI
N
Netflix TechBlog - Medium
GbyAI
GbyAI
IT之家
IT之家
Apple Machine Learning Research
Apple Machine Learning Research
AWS News Blog
AWS News Blog
G
GRAHAM CLULEY
L
Lohrmann on Cybersecurity
C
Cybersecurity and Infrastructure Security Agency CISA
I
Intezer
T
Tor Project blog
P
Palo Alto Networks Blog
P
Privacy & Cybersecurity Law Blog
P
Privacy International News Feed
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Proofpoint News Feed
T
Tailwind CSS Blog
C
Check Point Blog
Cloudbric
Cloudbric
Y
Y Combinator Blog
The Last Watchdog
The Last Watchdog
Forbes - Security
Forbes - Security
Last Week in AI
Last Week in AI
S
Security Affairs
博客园 - Franky
F
Fortinet All Blogs
量子位
M
MIT News - Artificial intelligence
C
Cisco Blogs
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
S
Secure Thoughts
V
Visual Studio Blog
AI
AI
美团技术团队
B
Blog RSS Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog
博客园 - 三生石上(FineUI控件)
阮一峰的网络日志
阮一峰的网络日志
Engineering at Meta
Engineering at Meta
人人都是产品经理
人人都是产品经理
Microsoft Security Blog
Microsoft Security Blog
T
Threatpost
Cyberwarzone
Cyberwarzone

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Branch Coverage for Lua with cluacov: From Line-Level Approximation to Instruction-Level Precision
yeshan333 · 2026-05-18 · via DEV Community

I recently spent some spare time reworking cluacov, a C extension around LuaCov, with help from GPT-5.5 and Claude Opus 4.7. The result was a branch-coverage pipeline with real output: https://shansan.top/cluacov/. The test corpus behind that report lives in the e2e directory.

【中文】

Introduction

Line coverage answers a simple question: "Did this line execute?" That is useful, but it is often not the question you actually care about. For testing quality, the stronger question is: "Did both paths of this branch execute?"

Consider this Lua code:

if a or b or c then
   do_something()
end

Enter fullscreen mode Exit fullscreen mode

A line-based tool can only tell you that the if line ran. It cannot tell you whether only a was tested, or whether b and c were also reached. At the bytecode level, the Lua compiler emits three independent TEST decisions for the short-circuit chain. That means there are three distinct branch sites, not one.

cluacov exposes two branch-coverage strategies:

  • A line-hook approximation based on LUA_MASKLINE, compatible with Lua 5.1-5.5 and LuaJIT.
  • A per-instruction design based on LUA_MASKCOUNT with count = 1, available on Lua 5.4+.

The difference between those two paths is not just an implementation detail. It is a direct reflection of what the Lua debug API can and cannot observe at different granularities.

cluacov architecture comparison: line-level path vs instruction-level path

Enough Lua Internals to Follow the Design

If you already know how Lua compiles source to bytecode and how its C debug hooks work, you can jump straight to the quick start section. Otherwise, these are the pieces that matter.

Lua compiles to bytecode, then executes on a register VM

Lua is interpreted, but not directly from source text. A source file is compiled into bytecode first, then run on a register-based virtual machine.

Each compiled Lua function corresponds to a Proto structure inside the VM. That Proto holds the static metadata needed for execution:

Proto field Meaning
code[] The bytecode instruction array
lineinfo[] Source line mapping for debugging and reporting
source Source filename such as "@test.lua"
linedefined First line where the function is defined
sizecode Number of bytecode instructions
p[] Nested child function Protos
k[] Constant table

When a Lua file is loaded, the file itself becomes an outer Proto, and each nested function introduces another Proto under p[]. cluacov discovers branch sites by walking Proto.code[].

Debug hooks are the only runtime interception point

Lua exposes a C-level hook API:

lua_sethook(L, callback, mask, count);

Enter fullscreen mode Exit fullscreen mode

The mask decides when the callback fires:

Mask Fires when
LUA_MASKCALL A function is called
LUA_MASKRET A function returns
LUA_MASKLINE Execution moves to a new source line
LUA_MASKCOUNT Every count instructions

Coverage tools piggyback on those hooks. Line coverage uses LUA_MASKLINE. The precise cluacov path uses a small trick: it sets count = 1, which makes the count hook behave like an instruction hook.

A few C API terms matter

  • lua_State is the Lua execution context.
  • CallInfo is one frame on the VM call stack.
  • Registry is a private global table reserved for C code.

cluacov stores per-Proto coverage state inside the Registry because that data should not be visible or mutable from plain Lua code.

GC finalizers are dangerous during shutdown

Lua supports __gc finalizers. That is convenient for "emit coverage when the process exits", but it creates a serious hazard: during lua_close, objects are freed in luaC_freeallobjects, and the destruction order is not predictable.

If finalizer A depends on object B, B may already be gone.

That is the core reason the precise path needs a snapshot-on-first-write design. Anything the finalizer needs later must be copied out of the live Proto before shutdown starts.

Version differences are real

Lua 5.1, 5.2, 5.3, 5.4, 5.5, and LuaJIT do not share one stable internal layout. Proto, CallInfo, opcode encoding, and even line-number encoding change between versions.

That is why cluacov has version-specific code paths and bundled vendor headers.

Quick Start

Precise instruction-level coverage on Lua 5.4+

The most practical entry point is cluacov.runner:

lua -lcluacov.runner your_program.lua

Enter fullscreen mode Exit fullscreen mode

When the program exits, cluacov writes:

  • luacov.stats.out for LuaCov-compatible line hits
  • lcov.info for LCOV output with both line and branch data

You can turn that into HTML with:

genhtml lcov.info --output-directory html --branch-coverage

Enter fullscreen mode Exit fullscreen mode

If you need full control over the lifecycle:

local pchook = require("cluacov.pchook")
local branchcov = require("cluacov.branchcov")

pchook.start()

local func = loadfile("module_under_test.lua")
local mod = func()
mod.run_tests()

pchook.stop()

local result = branchcov.analyze(func)
for _, branch in ipairs(result.branches) do
   print(string.format("Line %d [%s]: %s",
      branch.line, branch.kind, branch.status))
end

Enter fullscreen mode Exit fullscreen mode

Approximate branch coverage on Lua 5.1-5.3 or LuaJIT

If you cannot use the 5.4+ path, you can still combine line hits with static bytecode analysis:

lua -lluacov your_program.lua

Enter fullscreen mode Exit fullscreen mode

Then use deepbranches.get(func) to find branch sites and intersect them with line-hit data. This path needs branchfilter.lua to remove branch sites that line-level observation cannot distinguish reliably.

Static Discovery Comes First Either Way

Both architectures start from the same idea: find every branch site statically from bytecode before reasoning about hits.

deepbranches.get(func) walks a function's Proto chain and identifies four branch categories:

Kind Typical opcodes Source construct
test OP_TEST, OP_TESTSET, OP_EQ, OP_LT, ... if, elseif, and, or, comparisons
loop OP_FORLOOP Numeric for continue or exit
loop-entry OP_FORPREP on Lua 5.4+ Initial entry test for numeric for
iterator OP_TFORLOOP Generic for iterator exhaustion

Each branch site has exactly two targets. Those targets are sorted by program counter, not by semantic direction such as "true" vs "false".

local deepbranches = require("cluacov.deepbranches")

local function example(x)
   if x > 0 then
      return "positive"
   else
      return "non-positive"
   end
end

local branches = deepbranches.get(example)

Enter fullscreen mode Exit fullscreen mode

One call recurses through nested child Protos and returns the full branch list for the whole file.

deepbranches.c branch discovery pipeline

The Legacy Line-Level Path

How it works

The old path uses LUA_MASKLINE. Each time execution moves to a new source line, hook.c updates a line-hit counter such as data[file][line] += 1.

Branch coverage is then inferred in three steps:

  1. Use deepbranches.get(func) to discover branch sites and their target line numbers.
  2. Check whether each target line has line hits.
  3. Mark a branch as covered, partial, or uncovered based on how many target lines were hit.

Why it cannot be exact

The problem is granularity. Line hits are attached to source lines, not individual bytecode instructions.

In if a or b or c then, Lua emits three TEST instructions on the same source line. If a is truthy, the second and third tests never execute. A line hook still only sees "the line executed". That is not enough information to distinguish the three decisions.

Bytecode flow and observability for  raw `if a or b or c then` endraw

That is not a bug in cluacov. It is the hard ceiling of the line hook itself.

branchfilter is what keeps the report honest

Because same-line branch sites cannot be distinguished, cluacov deliberately filters them out instead of pretending to know more than it does.

The rule is simple:

  • If a line has only one branch site, keep it.
  • If a line has multiple branch sites, keep only those whose two target lines are both outside the branch line itself.
  • If multiple sites collapse to the same target-line pair, keep just one.

That is why if a or b or c then shrinks from three TEST instructions to one reportable branch in the line-based architecture.

The line path is conservative by design. It throws information away to preserve trust.

The New Instruction-Level Path

One callback per instruction

The new path uses LUA_MASKCOUNT with count = 1:

lua_sethook(L, pc_hook, LUA_MASKCOUNT, 1);

Enter fullscreen mode Exit fullscreen mode

In pc_hook, cluacov:

  1. Gets the currently executing Lua function.
  2. Reads its Proto.
  3. Reads the current savedpc from CallInfo.
  4. Increments the hit counter for that PC.

Stripped down to the core:

static void pc_hook(lua_State *L, lua_Debug *ar) {
    Proto *proto;
    CallInfo *ci;
    int pc;

    lua_getinfo(L, "f", ar);
    if (lua_iscfunction(L, -1)) { lua_pop(L, 1); return; }

    proto = get_proto(L, -1);
    lua_pop(L, 1);

    ci = (CallInfo *)ar->i_ci;
    pc = (int)(ci->u.l.savedpc - proto->code);

    if (push_hits_for_proto(L, proto) != 0) return;
    lua_rawgeti(L, -1, pc);
    lua_Integer count = lua_tointeger(L, -1) + 1;
    lua_pop(L, 1);
    lua_pushinteger(L, count);
    lua_rawseti(L, -2, pc);
    lua_pop(L, 1);
}

Enter fullscreen mode Exit fullscreen mode

The subtlety is that savedpc points to the next instruction, not the instruction that just executed. That means hits[pc] stores next-instruction PCs. This is not a bug; it is the convention used by the Lua interpreter itself.

That convention creates two distinct meanings:

Layer Meaning of hits[pc] Adjustment needed?
Storage "the hook fired when savedpc pointed here" No
Branch analysis Compatible with target.pc from branch discovery No
Line aggregation Needs to map back to the actual executed instruction Yes, use pc - 1

The pc - 1 in the line-aggregation path is not a 0-based vs 1-based conversion. It is a correction for Lua's next-instruction convention. Removing it breaks the first executable line in each function body.

Snapshot Proto metadata on first write

The most important part of the precise design is not the hook itself. It is how the hook survives shutdown safely.

When a Proto is first seen, cluacov copies every field it will need later into a pure Lua table:

  • source
  • linedefined
  • sizecode
  • a precomputed pc -> line table
  • an initially empty hits table

That snapshot means the finalizer never needs to dereference a live Proto * during shutdown.

pchook.c data lifecycle: snapshot first, survive GC later

Conceptually, the Registry ends up looking like this:

PCHOOK_KEY[entry_id] = {
    source      = "string",
    linedefined = integer,
    sizecode    = integer,
    lines       = { [pc] = line, ... },
    hits        = { [pc] = count, ... },
}

PROTO_INDEX_KEY[Proto*] = entry_id

Enter fullscreen mode Exit fullscreen mode

PROTO_INDEX_KEY is the only place that still references a raw Proto *, and it is only needed while the hook is active.

When stop() runs, cluacov precomputes two cached snapshots:

  • SNAPSHOT_ALL_HITS_KEY for per-PC data used by branchcov.lua
  • SNAPSHOT_LINE_HITS_KEY for per-line aggregation used to emit luacov.stats.out

After that, finalizer code only reads cached Lua tables.

branchcov.lua becomes simple once hits are per-PC

With independent target PCs, branch analysis is straightforward:

  1. Discover branch sites with deepbranches.get(func).
  2. Read per-PC hit tables from pchook.get_hits(func).
  3. For each branch target, look up hits[target.pc].
  4. Mark the branch covered, partial, or uncovered.

Because every target has its own counter, the precise path does not need branchfilter.

For if a or b or c then, the report contains three branch sites and six total targets.

Important Behavioral Details

Target ordering is stable but not semantic

targets[1] is not "the true branch" and targets[2] is not "the false branch". Targets are sorted by PC, nothing more.

That is a good design choice for a coverage tool. It avoids semantic guessing and focuses on the only invariant that matters for reporting: whether both paths were executed.

Shared target PCs are still an approximation

Multiple branch instructions can point to the same target PC.

In if a or b or c then do_something() end, all three truthy branches eventually land on the first instruction of do_something(). If a is truthy, that target PC is executed, but you only know it was reached, not which predecessor edge led there.

So the precise path is closer to instruction coverage than full edge coverage. In practice, that is still accurate enough for most testing and CI use cases.

get_hits(func) requires the same function object that actually ran

pchook.get_hits(func) keys everything by Proto *. If you loadfile() the same source again after stop(), you get a different Proto and therefore no hit data.

That is a common misuse case and worth calling out explicitly.

Lua 5.4 changed line-number encoding

Lua 5.4 moved from direct lineinfo[pc] mappings to an abslineinfo baseline plus incremental deltas. Both deepbranches.c and pchook.c have to reconstruct actual line numbers from that two-level encoding.

That is one of the hidden reasons version support is narrower for the precise path.

Why some end lines show hits and others do not

This confuses people in HTML coverage reports all the time.

An end line is only executable if the compiler actually attached some bytecode instruction to that line.

That gives you a useful rule of thumb:

  • Function end lines usually map to a return instruction, so they are executable.
  • if ... end and do ... end closing lines are usually pure syntax and never executable.
  • Loop end lines are conditional: they only become executable if the compiler anchors real cleanup or control-flow instructions there, such as OP_CLOSE.

This is compiler behavior, not a reporting bug.

Performance and Trade-Offs

The precise path fires on every VM instruction, so its raw event count is much higher than the line path. Intuitively, that sounds like it should always be slower.

In practice, the picture is more nuanced.

Across three machines, the geometric-mean slowdowns reported in the cluacov benchmarks were:

Machine cluacov C line hook pchook Overall winner
Xeon 8269CY (Linux CI) 28.1x 36.1x line hook
Xeon 8369B (Linux) 36.9x 49.6x line hook
Apple M4 Pro (macOS) 43.3x 54.0x line hook

The line hook wins overall by about 1.25x-1.34x, but the gap is not huge.

Why? Because the cost model has two parts:

  • The line hook fires less often, but each callback is relatively expensive because it needs lua_getstack and lua_getinfo(L, "S", ...) to resolve the source file.
  • The PC hook fires much more often, but each callback is cheaper because it can look up the current function, read savedpc, and update integer-indexed tables without reparsing source metadata.

That is why pchook can still win on tight-loop workloads even though it is slower on the geometric mean.

Memory overhead

The precise path stores one Lua integer per executed PC, plus the snapshot metadata tables. For a moderately sized Lua file, that usually means tens of kilobytes rather than megabytes. It is measurable, but not alarming in test environments.

Accuracy trade-off

The line path is conservative and intentionally under-reports some branch sites.

The precise path reports far more detail, but it still does not give you compiler-grade edge coverage like gcov.

That leads to a practical summary:

Dimension Line path (hook.c) Precise path (pchook.c)
Supported runtimes Lua 5.1-5.5, LuaJIT Lua 5.4+
Hook type LUA_MASKLINE LUA_MASKCOUNT with count = 1
Granularity line hits per-PC hits + line aggregation
Short-circuit visibility poor strong
Filtering required yes no
Performance better overall slightly slower overall
GC safety historically fragile fixed with Proto snapshots

How It Compares to LuaCov and gcov

LuaCov itself only does line coverage. It does not discover branch sites and it does not emit LCOV branch records such as BRDA.

cluacov adds two incremental capabilities on top of that world:

  • deepbranches for static branch discovery
  • pchook plus branchcov for PC-level hit collection and branch reporting

Compared with gcov, cluacov still operates under tighter constraints:

  • gcov uses compile-time instrumentation and can count actual CFG edges.
  • Lua is interpreted, so cluacov has to observe runtime execution through debug hooks.
  • That means higher overhead and less perfect edge semantics.

Even so, cluacov is currently a very strong branch-coverage option in the Lua ecosystem because it gets as close to precise branch observability as the runtime allows.

When to Use Which Path

Use cluacov.runner and the instruction-level path when:

  • your project runs on Lua 5.4+
  • you care about short-circuit conditions such as and and or
  • you want LCOV output with branch annotations for CI or HTML reports

Use the line path when:

  • you must support Lua 5.1-5.3 or LuaJIT
  • coarse coverage is acceptable
  • compatibility matters more than branch fidelity

Do not use either hook-heavy path in production monitoring. A 28x-54x slowdown belongs in tests and CI, not in production traffic.

Common mistakes:

  • reloading a file after pchook.stop() and passing the new closure to get_hits
  • calling pchook.start() on Lua 5.3
  • forgetting to exclude cluacov's own modules in .luacov

Conclusion

cluacov's two branch-coverage architectures are best understood as a progression rather than a replacement.

The line-hook path maximizes compatibility and preserves report credibility by filtering out ambiguous same-line branches. The instruction-level path uses count hooks, next-instruction PCs, and snapshot-on-first-write metadata to get much closer to true branch observability while staying safe across Lua shutdown.

That trade-off is exactly what makes the design interesting: both implementations are pushing against the edge of what the Lua debug interface makes possible, just at different points on the compatibility-versus-fidelity curve.

Appendix: Reading the LCOV Output

cluacov.runner emits lcov.info, which standard LCOV tools such as genhtml can consume directly.

How to read an LCOV report emitted by cluacov

Record structure

Each record starts with TN and SF, then contains three major coverage families:

  • Function coverage via FN, FNDA, FNF, and FNH
  • Branch coverage via BRDA, BRF, and BRH
  • Line coverage via DA, LF, and LH

Interpreting branch states

For the two BRDA entries that share one site id:

  • covered means both paths have hit counts
  • partial means only one path has a hit count
  • uncovered means both paths are still -

Rendering HTML

genhtml lcov.info --output-directory html --branch-coverage

Enter fullscreen mode Exit fullscreen mode

That HTML output is the easiest way to inspect branch states in CI: covered branches show both paths hit, partial branches show one side missing, and uncovered branches show neither side executed.