Branch data Line data Source code
1 : 1 : local M = {}
2 : :
3 : 1 : function M.classify(n)
4 [ + + ]: 500 : if n > 0 then
5 : 167 : return "positive"
6 [ + + ]: 333 : elseif n == 0 then
7 : 167 : return "zero"
8 : : else
9 : 166 : return "negative"
10 : : end
11 : 1 : end
12 : :
13 : 1 : function M.abs(x)
14 [ + # ]: 500 : if x < 0 then
15 : 500 : return -x
16 : : end
17 : 0 : return x
18 : 1 : end
19 : :
20 : 1 : function M.clamp(x, lo, hi)
21 [ # # ]: 0 : if x < lo then
22 : 0 : return lo
23 [ # # ]: 0 : elseif x > hi then
24 : 0 : return hi
25 : : end
26 : 0 : return x
27 : 1 : end
28 : :
29 : 1 : function M.sum(t)
30 : 500 : local total = 0
31 [ + + ][ + + ]: 2500 : for i = 1, #t do
32 : 2500 : total = total + t[i]
33 : : end
34 : 500 : return total
35 : 1 : end
36 : :
37 : 1 : function M.find(t, value)
38 [ + # ]: 500 : for _, v in ipairs(t) do
39 [ + + ]: 1000 : if v == value then
40 : 500 : return true
41 : : end
42 : 0 : end
43 : 0 : return false
44 : 1 : end
45 : :
46 : 1 : function M.safe_div(a, b)
47 [ # # ]: 0 : if b == 0 then
48 : 0 : return nil, "division by zero"
49 : : end
50 : 0 : return a / b
51 : 1 : end
52 : :
53 : 1 : function M.fizzbuzz(n)
54 [ + # ]: 33 : if n % 15 == 0 then
55 : 33 : return "fizzbuzz"
56 [ # # ]: 0 : elseif n % 3 == 0 then
57 : 0 : return "fizz"
58 [ # # ]: 0 : elseif n % 5 == 0 then
59 : 0 : return "buzz"
60 : : else
61 : 0 : return tostring(n)
62 : : end
63 : 1 : end
64 : :
65 : 1 : function M.max_of_three(a, b, c)
66 [ # # ][ # # ]: 0 : if a >= b and a >= c then
67 : 0 : return a
68 [ # # ]: 0 : elseif b >= c then
69 : 0 : return b
70 : : else
71 : 0 : return c
72 : : end
73 : 1 : end
74 : :
75 : 1 : function M.any_truthy(a, b, c)
76 [ # # ][ # # ]: 0 : if a or b or c then
[ # # ]
77 : 0 : return "yes"
78 : : else
79 : 0 : return "no"
80 : : end
81 : 1 : end
82 : :
83 : 1 : function M.all_truthy(a, b, c)
84 [ # # ][ # # ]: 0 : if a and b and c then
[ # # ]
85 : 0 : return "all"
86 : : else
87 : 0 : return "not all"
88 : : end
89 : 1 : end
90 : :
91 : 1 : function M.mixed_logic(x, y)
92 [ # # ][ # # ]: 0 : if (x > 0 and y > 0) or (x < -10) then
[ # # ]
93 : 0 : return "match"
94 : : else
95 : 0 : return "no match"
96 : : end
97 : 1 : end
98 : :
99 : : -- Regression target: function-body first line must be HIT.
100 : : -- Before the savedpc-off-by-one fix in collect_line_hits_recursive,
101 : : -- the very first executable line of a function body (`local t = ...`
102 : : -- right after the function header) was reported with hits = 0,
103 : : -- because the hits-table key (savedpc - code) is the NEXT instruction's
104 : : -- PC and the line aggregator was reading it without the -1 shift.
105 : 1 : function M.first_line_local(cobj)
106 : 0 : local t = cobj.kind -- regression: function-body first line
107 [ # # ]: 0 : if t == "ok" then
108 : 0 : return "ok:" .. t
109 : : end
110 : 0 : return "skip"
111 : 1 : end
112 : :
113 : : -- Regression target: first executable line INSIDE an if-block.
114 : : -- Same root cause as above. Before the fix this `local cleaned = v` line
115 : : -- consistently reported 0, even though the if-block was clearly entered
116 : : -- (the next line, `out[#out+1] = cleaned`, absorbed the missing hits).
117 : : -- Mirrors the real-world pattern from Path.join_path that triggered
118 : : -- the original bug report.
119 : 1 : function M.if_block_first_line(items)
120 : 0 : local out = {}
121 [ # # ]: 0 : for _, v in ipairs(items) do
122 [ # # ]: 0 : if type(v) == "string" then
123 : 0 : local cleaned = v -- regression: if-block first line
124 : 0 : out[#out + 1] = cleaned
125 : : end
126 : 0 : end
127 : 0 : return out
128 : 1 : end
129 : :
130 : : -- ===========================================================================
131 : : -- Lua-language showcase block (line 132+).
132 : : --
133 : : -- Everything ABOVE this line is locked: the e2e suite asserts on hard-coded
134 : : -- line numbers (L30, L76, L84, L106, L108, L123). DO NOT shuffle those.
135 : : --
136 : : -- Everything BELOW intentionally exercises a wide variety of Lua control
137 : : -- flow / syntax to make the public coverage demo richer:
138 : : --
139 : : -- * while / break / repeat-until
140 : : -- * numeric-for with explicit step (incl. negative)
141 : : -- * generic-for with pairs / ipairs early-return
142 : : -- * deeply nested if / elseif chains
143 : : -- * short-circuit `or` / `and` for default values & ternary
144 : : -- * pcall / xpcall error capture branches
145 : : -- * goto / continue idiom (Lua 5.2+)
146 : : -- * vararg `...` + select
147 : : -- * multiple return values + destructuring
148 : : -- * closures with upvalues, factory pattern
149 : : -- * direct recursion + mutual recursion
150 : : -- * metatable __index / __call dispatch
151 : : -- * string-keyed dispatch table
152 : : -- * method-call colon syntax
153 : : --
154 : : -- run_test.lua deliberately exercises ONLY a subset of the paths so the
155 : : -- public HTML report shows a believable mix of red & green lines, which
156 : : -- is exactly what someone evaluating cluacov wants to see.
157 : : -- ===========================================================================
158 : :
159 : : -- while + break: walks t looking for `target`, returns the 1-based index
160 : : -- (or nil if absent). The break path is only taken when target is found
161 : : -- in the middle of the table.
162 : 1 : function M.while_break(t, target)
163 : 0 : local i = 1
164 [ # # ]: 0 : while i <= #t do
165 [ # # ]: 0 : if t[i] == target then
166 : 0 : break
167 : : end
168 : 0 : i = i + 1
169 : : end
170 [ # # ]: 0 : if i > #t then
171 : 0 : return nil
172 : : end
173 : 0 : return i
174 : 1 : end
175 : :
176 : : -- repeat..until: collects squares until the running total exceeds limit.
177 : : -- The until-condition is evaluated AFTER the body runs at least once.
178 : 1 : function M.repeat_until(limit)
179 : 0 : local i = 0
180 : 0 : local total = 0
181 : : repeat
182 : 0 : i = i + 1
183 : 0 : total = total + i * i
184 [ # # ]: 0 : until total >= limit
185 : 0 : return i, total
186 : 1 : end
187 : :
188 : : -- numeric for with explicit (and possibly negative) step. Three branches
189 : : -- on the step sign let cluacov demonstrate "branch not taken" rendering
190 : : -- when the caller never exercises a particular step direction.
191 : 1 : function M.for_step(start_n, stop_n, step)
192 : 0 : local out = {}
193 [ # # ]: 0 : if step == 0 then
194 : 0 : return nil, "step cannot be zero"
195 : : end
196 [ # # ][ # # ]: 0 : for i = start_n, stop_n, step do
197 : 0 : out[#out + 1] = i
198 : : end
199 : 0 : return out
200 : 1 : end
201 : :
202 : : -- generic-for with pairs + early return. `pairs` ordering is undefined,
203 : : -- but the early-return branch is deterministic when key is present.
204 : 1 : function M.find_in_map(map, key)
205 [ # # ]: 0 : for k, v in pairs(map) do
206 [ # # ]: 0 : if k == key then
207 : 0 : return v, true
208 : : end
209 : 0 : end
210 : 0 : return nil, false
211 : 1 : end
212 : :
213 : : -- Deeply nested if/elseif (HTTP status code classifier). Designed to
214 : : -- have many branches; run_test.lua only covers some of them.
215 : 1 : function M.classify_status(code)
216 [ # # ]: 0 : if code >= 500 then
217 [ # # ]: 0 : if code == 503 then
218 : 0 : return "unavailable"
219 [ # # ]: 0 : elseif code == 504 then
220 : 0 : return "gateway timeout"
221 : : else
222 : 0 : return "server error"
223 : 0 : end
224 [ # # ]: 0 : elseif code >= 400 then
225 [ # # ]: 0 : if code == 404 then
226 : 0 : return "not found"
227 [ # # ][ # # ]: 0 : elseif code == 401 or code == 403 then
228 : 0 : return "auth error"
229 : : else
230 : 0 : return "client error"
231 : 0 : end
232 [ # # ]: 0 : elseif code >= 300 then
233 : 0 : return "redirect"
234 [ # # ]: 0 : elseif code >= 200 then
235 : 0 : return "ok"
236 : : else
237 : 0 : return "informational"
238 : : end
239 : 1 : end
240 : :
241 : : -- Short-circuit `or` for default values. The default branch (a == nil)
242 : : -- is only taken when caller passes nil.
243 : 1 : function M.with_default(a, default)
244 [ # # ]: 0 : local v = a or default
245 : 0 : return v
246 : 1 : end
247 : :
248 : : -- Lua's idiomatic ternary: `cond and then_value or else_value`.
249 : : -- The pitfall (then_value being false/nil) is intentionally NOT guarded
250 : : -- here so cluacov shows both branch arms.
251 : 1 : function M.ternary_max(a, b)
252 [ # # ][ # # ]: 0 : return a > b and a or b
253 : 1 : end
254 : :
255 : : -- pcall: protected call with branch on success vs. failure.
256 : 1 : function M.try_parse_int(s)
257 : 0 : local ok, result = pcall(function()
258 : 0 : local n = tonumber(s)
259 [ # # ]: 0 : if n == nil then
260 : 0 : error("not a number: " .. tostring(s))
261 : : end
262 [ # # ]: 0 : if n ~= math.floor(n) then
263 : 0 : error("not an integer: " .. tostring(n))
264 : : end
265 : 0 : return n
266 : 0 : end)
267 [ # # ]: 0 : if ok then
268 : 0 : return result, nil
269 : : else
270 : 0 : return nil, result -- result is the error message
271 : : end
272 : 1 : end
273 : :
274 : : -- goto/continue idiom (Lua 5.2+). Skips even numbers in the source list.
275 : 1 : function M.sum_odd(t)
276 : 0 : local total = 0
277 [ # # ]: 0 : for _, v in ipairs(t) do
278 [ # # ]: 0 : if v % 2 == 0 then
279 : 0 : goto continue
280 : : end
281 : 0 : total = total + v
282 : : ::continue::
283 : 0 : end
284 : 0 : return total
285 : 1 : end
286 : :
287 : : -- Vararg + select: count how many args are non-nil.
288 : 1 : function M.count_args(...)
289 : 0 : local n = select("#", ...)
290 : 0 : local count = 0
291 [ # # ][ # # ]: 0 : for i = 1, n do
292 [ # # ]: 0 : if select(i, ...) ~= nil then
293 : 0 : count = count + 1
294 : : end
295 : : end
296 : 0 : return count
297 : 1 : end
298 : :
299 : : -- Multiple return values, exercised via destructuring at the call site.
300 : 1 : function M.divmod(a, b)
301 [ # # ]: 0 : if b == 0 then
302 : 0 : return nil, nil, "division by zero"
303 : : end
304 : 0 : local q = math.floor(a / b)
305 : 0 : local r = a - q * b
306 : 0 : return q, r
307 : 1 : end
308 : :
309 : : -- Closure factory: returns a counter closure capturing `count` as upvalue.
310 : : -- Each invocation returns a fresh closure with its own state.
311 : 1 : function M.make_counter(start_n)
312 [ # # ]: 0 : local count = start_n or 0
313 : : return function(step)
314 [ # # ]: 0 : step = step or 1
315 : 0 : count = count + step
316 : 0 : return count
317 : 0 : end
318 : 1 : end
319 : :
320 : : -- Direct recursion + base case. The base case is naturally taken once
321 : : -- per top-level call; the recursive case is taken for any n >= 2.
322 : 1 : function M.factorial(n)
323 [ # # ]: 0 : if n <= 1 then
324 : 0 : return 1
325 : : end
326 : 0 : return n * M.factorial(n - 1)
327 : 1 : end
328 : :
329 : : -- Mutual recursion (even/odd). Forward declaration via local.
330 : 1 : local is_even, is_odd
331 : 1 : function is_even(n)
332 [ # # ]: 0 : if n == 0 then return true end
333 : 0 : return is_odd(n - 1)
334 : 1 : end
335 : 1 : function is_odd(n)
336 [ # # ]: 0 : if n == 0 then return false end
337 : 0 : return is_even(n - 1)
338 : 1 : end
339 : 1 : M.is_even = is_even
340 : 1 : M.is_odd = is_odd
341 : :
342 : : -- String-keyed dispatch table: typical alternative to long if/elseif.
343 : : -- The fallback branch (unknown verb) is only hit when caller passes an
344 : : -- unknown key.
345 : 1 : local dispatch = {
346 : 1 : add = function(a, b) return a + b end,
347 : 1 : sub = function(a, b) return a - b end,
348 : 1 : mul = function(a, b) return a * b end,
349 : : div = function(a, b)
350 [ # # ]: 0 : if b == 0 then return nil, "div by zero" end
351 : 0 : return a / b
352 : 1 : end,
353 : : }
354 : 1 : function M.dispatch(op, a, b)
355 : 0 : local fn = dispatch[op]
356 [ # # ]: 0 : if not fn then
357 : 0 : return nil, "unknown op: " .. tostring(op)
358 : : end
359 : 0 : return fn(a, b)
360 : 1 : end
361 : :
362 : : -- Metatable __index + colon-syntax method call. The `Point` "class"
363 : : -- mixes both data fields and bound methods.
364 : 1 : local Point = {}
365 : 1 : Point.__index = Point
366 : 1 : function Point.new(x, y)
367 : 0 : return setmetatable({ x = x, y = y }, Point)
368 : 1 : end
369 : 1 : function Point:translate(dx, dy)
370 : 0 : self.x = self.x + dx
371 : 0 : self.y = self.y + dy
372 : 0 : return self
373 : 1 : end
374 : 1 : function Point:length()
375 : 0 : return math.sqrt(self.x * self.x + self.y * self.y)
376 : 1 : end
377 : 1 : M.Point = Point
378 : :
379 : : -- table.concat with separator + conditional empty branch.
380 : 1 : function M.join(t, sep)
381 [ # # ]: 0 : if #t == 0 then
382 : 0 : return ""
383 : : end
384 [ # # ]: 0 : return table.concat(t, sep or ",")
385 : 1 : end
386 : :
387 : : -- goto/continue: accumulates only non-negative values.
388 : : -- Both branches of `if v < 0` are exercisable:
389 : : -- taken → goto skip (negative input)
390 : : -- not taken → total = total + v (non-negative input)
391 : 1 : function M.goto_filter(t)
392 : 0 : local total = 0
393 [ # # ]: 0 : for _, v in ipairs(t) do
394 [ # # ]: 0 : if v < 0 then
395 : 0 : goto skip
396 : : end
397 : 0 : total = total + v
398 : : ::skip::
399 : 0 : end
400 : 0 : return total
401 : 1 : end
402 : :
403 : : -- goto with multiple forward labels: returns the first positive value,
404 : : -- or -1 if none found.
405 : : -- if v > 0 → goto found (early exit with value)
406 : : -- end of loop → goto done (fall-through, no positive found)
407 : 1 : function M.goto_first_match(t)
408 : 0 : local result = -1
409 [ # # ]: 0 : for _, v in ipairs(t) do
410 [ # # ]: 0 : if v > 0 then
411 : 0 : result = v
412 : 0 : goto found
413 : : end
414 : 0 : end
415 : 0 : goto done
416 : : ::found::
417 : : ::done::
418 : 0 : return result
419 : 1 : end
420 : :
421 : : -- goto uncovered: never called from run_test.lua, so the conditional
422 : : -- goto branch stays fully uncovered in the report.
423 : 1 : function M.goto_early_return(err)
424 : 0 : local result
425 [ # # ]: 0 : if err then
426 : 0 : goto bail
427 : : end
428 : 0 : result = "ok"
429 : : ::bail::
430 [ # # ]: 0 : if result then return result end
431 : 0 : return nil, err
432 : 1 : end
433 : :
434 : : -- assert: exercises assert branches
435 : 1 : function M.verify_assert(val)
436 [ # # ]: 0 : assert(val, "value must be truthy")
437 : 0 : return "ok"
438 : 1 : end
439 : :
440 : : -- assert: exercises partial assert branches (always true)
441 : 1 : function M.verify_partial_assert(val)
442 [ # # ]: 0 : assert(val, "always true in test")
443 : 0 : return "partial-ok"
444 : 1 : end
445 : :
446 : 1 : return M
|