• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lunarmodules / copas / 30211738911

26 Jul 2026 05:02PM UTC coverage: 85.343% (+0.1%) from 85.229%
30211738911

push

github

Tieske
fix(semaphore): take() must reject requests below 1 or NaN

A negative `requested` on the fast path (`count = count - requested`)
turned a take into a give, inflating `count` past `max` with no check
at all (eg. take(-100) on a max=5 semaphore left count at 100). A NaN
request would queue forever, since `count >= NaN` is always false and
can never be satisfied, deadlocking that entry and everyone behind it
in the FIFO queue. Reject both instead.

2 of 2 new or added lines in 1 file covered. (100.0%)

9 existing lines in 5 files now uncovered.

1444 of 1692 relevant lines covered (85.34%)

55080.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/src/copas/future.lua
1
local copas = require("copas")
6✔
2
local semaphore = require("copas.semaphore")
6✔
3

4
local pcall = pcall
6✔
5
if _VERSION == "Lua 5.1" and not jit then  -- obsolete: only for Lua 5.1 compatibility
6✔
UNCOV
6
  pcall = require("coxpcall").pcall
1✔
7
end
8

9

10
-- nil-safe versions for pack/unpack
11
local _unpack = unpack or table.unpack
6✔
12
local unpack = function(t, i, j) return _unpack(t, i or 1, j or t.n or #t) end
108✔
13
local pack = function(...) return { n = select("#", ...), ...} end
78✔
14

15

16

17
-- Module table
18

19
local M = {}
6✔
20

21
M.SUCCESS = true
6✔
22
M.PENDING = false
6✔
23
M.ERROR   = "error"
6✔
24

25
setmetatable(M, {
12✔
26
  __index = function(_, k)
27
    error("unknown field 'future." .. tostring(k) .. "'", 2)
6✔
28
  end,
29
})
30

31

32

33
-- Future class
34

35
local future = {}
6✔
36
future.__index = future
6✔
37

38
-- calling on the future executes the `get` method
39
future.__call = function(self, ...) return self:get(...) end
12✔
40

41

42
local function new_future()
43
  local self = setmetatable({
144✔
44
    results = nil, -- results will be stored here in a 'packed' table (pcall-style: true/false prefix)
60✔
45
    sema = semaphore.new(math.huge, 0, math.huge),
84✔
46
    coro = nil -- the coroutine that will execute the task
60✔
47
  }, future)
72✔
48

49
  return self
72✔
50
end
51

52

53
-- Waits for the task to complete.
54
-- Returns like pcall: true + results on success, false + errmsg on error.
55
function future:get()
6✔
56
  if not self.results then
96✔
57
    self.sema:take(1, math.huge) -- wait until the result is ready
84✔
58
  end
59
  return unpack(self.results)
96✔
60
end
61

62

63
-- Non-blocking check on the future status.
64
-- Returns:
65
--   M.PENDING (false)             -- task not yet complete
66
--   M.SUCCESS (true), results...  -- task completed successfully
67
--   M.ERROR ("error"), errmsg     -- task failed with an error
68
function future:try()
6✔
69
  if not self.results then
24✔
70
    return M.PENDING
6✔
71
  end
72
  if self.results[1] then
18✔
73
    return M.SUCCESS, unpack(self.results, 2)
7✔
74
  else
75
    return M.ERROR, self.results[2]
12✔
76
  end
77
end
78

79

80
-- Cancels the task if it has not yet completed.
81
-- Returns true if cancelled, false if already done.
82
function future:cancel()
6✔
83
  if self.results then
18✔
84
    return false  -- already done (or already cancelled)
6✔
85
  end
86
  self.results = pack(false, "cancelled")
14✔
87
  self.sema:release_all()
12✔
88
  copas.removethread(self.coro)
12✔
89
  return true
12✔
90
end
91

92

93

94
-- Module implementation
95

96
-- Mimics copas.addnamedthread but returns a future instead of the coroutine.
97
function M.addnamedthread(name, func, ...)
6✔
98
  local f = new_future()
72✔
99

100
  f.coro = copas.addnamedthread(name, function(...)
144✔
101
    local results
102
    local ok, err = pcall(function(...) results = pack(true, func(...)) end, ...)
136✔
103
    if not ok then
60✔
104
      results = pack(false, err)
14✔
105
    end
106
    if not f.results then  -- don't overwrite a cancel
60✔
107
      f.results = results
60✔
108
      f.sema:release_all()
60✔
109
    end
110
  end, ...)
204✔
111

112
  return f
72✔
113
end
114

115

116
-- Mimica copas.addthread but returns a future instead of the coroutine.
117
function M.addthread(func, ...)
6✔
118
  return M.addnamedthread(nil, func, ...)
66✔
119
end
120

121

122
return M
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc