fix(runner): proper timeout
This commit is contained in:
parent
312a74d785
commit
c9ba8281b0
4 changed files with 86 additions and 37 deletions
|
|
@ -19,6 +19,7 @@ https://github.com/user-attachments/assets/50b19481-8e6d-47b4-bebc-15e16c61a9c9
|
|||
|
||||
- [uv](https://docs.astral.sh/uv/) for problem scraping
|
||||
- [LuaSnip](https://github.com/L3MON4D3/LuaSnip) for templates
|
||||
- GNU [time](https://www.gnu.org/software/time/) and [timeout](https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html)
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
|
|
|||
|
|
@ -74,10 +74,7 @@ local function parse_and_strip_time_v(output, memory_mb)
|
|||
end
|
||||
end
|
||||
if not timing_idx then
|
||||
while #lines > 0 and lines[#lines]:match('^%s*$') do
|
||||
table.remove(lines, #lines)
|
||||
end
|
||||
return table.concat(lines, '\n'), 0, false
|
||||
return output or '', 0, false
|
||||
end
|
||||
|
||||
local start_idx = timing_idx
|
||||
|
|
@ -110,20 +107,22 @@ local function parse_and_strip_time_v(output, memory_mb)
|
|||
end
|
||||
|
||||
function M.run(cmd, stdin, timeout_ms, memory_mb)
|
||||
local prog = table.concat(cmd, ' ')
|
||||
local pre = {}
|
||||
if memory_mb and memory_mb > 0 then
|
||||
table.insert(pre, ('ulimit -v %d'):format(memory_mb * 1024))
|
||||
end
|
||||
local prefix = (#pre > 0) and (table.concat(pre, '; ') .. '; ') or ''
|
||||
local time_bin = utils.time_path()
|
||||
local sh = prefix .. ('%s -v sh -c %q 2>&1'):format(time_bin, prog)
|
||||
local timeout_bin = utils.timeout_path()
|
||||
|
||||
local prog = table.concat(cmd, ' ')
|
||||
local pre = {
|
||||
('ulimit -v %d'):format(memory_mb * 1024),
|
||||
}
|
||||
local prefix = table.concat(pre, '; ') .. '; '
|
||||
local sec = math.ceil(timeout_ms / 1000)
|
||||
local timeout_prefix = ('%s -k 1s %ds '):format(timeout_bin, sec)
|
||||
local sh = prefix .. timeout_prefix .. ('%s -v sh -c %q 2>&1'):format(time_bin, prog)
|
||||
|
||||
local t0 = vim.uv.hrtime()
|
||||
local r = vim
|
||||
.system({ 'sh', '-c', sh }, {
|
||||
stdin = stdin,
|
||||
timeout = timeout_ms,
|
||||
text = true,
|
||||
})
|
||||
:wait()
|
||||
|
|
@ -131,7 +130,7 @@ function M.run(cmd, stdin, timeout_ms, memory_mb)
|
|||
|
||||
local code = r.code or 0
|
||||
local raw = r.stdout or ''
|
||||
local cleaned, peak_mb = parse_and_strip_time_v(raw)
|
||||
local cleaned, peak_mb, mled = parse_and_strip_time_v(raw, memory_mb)
|
||||
local tled = code == 124
|
||||
|
||||
local signal = nil
|
||||
|
|
@ -139,21 +138,6 @@ function M.run(cmd, stdin, timeout_ms, memory_mb)
|
|||
signal = constants.signal_codes[code]
|
||||
end
|
||||
|
||||
local lower = (cleaned or ''):lower()
|
||||
local oom_hint = lower:find('std::bad_alloc', 1, true)
|
||||
or lower:find('cannot allocate memory', 1, true)
|
||||
or lower:find('enomem', 1, true)
|
||||
|
||||
local near_cap = false
|
||||
if memory_mb and memory_mb > 0 then
|
||||
near_cap = (peak_mb >= (0.90 * memory_mb))
|
||||
end
|
||||
|
||||
local mled = false
|
||||
if peak_mb >= memory_mb or near_cap or oom_hint then
|
||||
mled = true
|
||||
end
|
||||
|
||||
if tled then
|
||||
logger.log(('Execution timed out in %.1fms.'):format(dt))
|
||||
elseif mled then
|
||||
|
|
|
|||
|
|
@ -30,19 +30,17 @@ function M.get_status_info(ran_test_case)
|
|||
return { text = 'AC', highlight_group = 'CpTestAC' }
|
||||
end
|
||||
|
||||
if ran_test_case.actual == '' then
|
||||
return { text = '...', highlight_group = 'CpTestPending' }
|
||||
end
|
||||
|
||||
if ran_test_case.tled then
|
||||
return { text = 'TLE', highlight_group = 'CpTestTLE' }
|
||||
elseif ran_test_case.mled then
|
||||
return { text = 'MLE', highlight_group = 'CpTestMLE' }
|
||||
elseif ran_test_case.code and ran_test_case.code >= 128 then
|
||||
elseif ran_test_case.code > 0 and ran_test_case.code >= 128 then
|
||||
return { text = 'RTE', highlight_group = 'CpTestRTE' }
|
||||
else
|
||||
elseif ran_test_case.code == 0 and not ran_test_case.ok then
|
||||
return { text = 'WA', highlight_group = 'CpTestWA' }
|
||||
end
|
||||
|
||||
return { text = '...', highlight_group = 'CpTestPending' }
|
||||
end
|
||||
|
||||
local function format_exit_code(code)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ local uname = vim.loop.os_uname()
|
|||
local _time_cached = false
|
||||
local _time_path = nil
|
||||
local _time_reason = nil
|
||||
local _timeout_cached = false
|
||||
local _timeout_path = nil
|
||||
local _timeout_reason = nil
|
||||
|
||||
local function is_windows()
|
||||
return uname and uname.sysname == 'Windows_NT'
|
||||
|
|
@ -146,9 +149,14 @@ function M.check_required_runtime()
|
|||
return false, 'Neovim 0.10.0+ required'
|
||||
end
|
||||
|
||||
local cap = M.time_capability()
|
||||
if not cap.ok then
|
||||
return false, 'GNU time not found: ' .. (cap.reason or '')
|
||||
local time = M.time_capability()
|
||||
if not time.ok then
|
||||
return false, 'GNU time not found: ' .. (time.reason or '')
|
||||
end
|
||||
|
||||
local timeout = M.timeout_capability()
|
||||
if not timeout.ok then
|
||||
return false, 'GNU timeout not found: ' .. (timeout.reason or '')
|
||||
end
|
||||
|
||||
if vim.fn.executable('uv') ~= 1 then
|
||||
|
|
@ -162,4 +170,62 @@ function M.check_required_runtime()
|
|||
return true
|
||||
end
|
||||
|
||||
local function check_timeout_is_gnu_timeout(bin)
|
||||
if vim.fn.executable(bin) ~= 1 then
|
||||
return false
|
||||
end
|
||||
local r = vim.system({ bin, '--version' }, { text = true }):wait()
|
||||
if r and r.code == 0 and r.stdout then
|
||||
local s = r.stdout:lower()
|
||||
if s:find('gnu coreutils', 1, true) or s:find('timeout %(gnu coreutils%)', 1, true) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function find_gnu_timeout()
|
||||
if _timeout_cached then
|
||||
return _timeout_path, _timeout_reason
|
||||
end
|
||||
|
||||
if is_windows() then
|
||||
_timeout_cached = true
|
||||
_timeout_path = nil
|
||||
_timeout_reason = 'unsupported on Windows'
|
||||
return _timeout_path, _timeout_reason
|
||||
end
|
||||
|
||||
local candidates
|
||||
if uname and uname.sysname == 'Darwin' then
|
||||
candidates = { 'gtimeout', '/opt/homebrew/bin/gtimeout', '/usr/local/bin/gtimeout' }
|
||||
else
|
||||
candidates = { '/usr/bin/timeout', 'timeout' }
|
||||
end
|
||||
|
||||
for _, bin in ipairs(candidates) do
|
||||
if check_timeout_is_gnu_timeout(bin) then
|
||||
_timeout_cached = true
|
||||
_timeout_path = bin
|
||||
_timeout_reason = nil
|
||||
return _timeout_path, _timeout_reason
|
||||
end
|
||||
end
|
||||
|
||||
_timeout_cached = true
|
||||
_timeout_path = nil
|
||||
_timeout_reason = 'GNU timeout not found (install `coreutils`; macOS: `brew install coreutils`)'
|
||||
return _timeout_path, _timeout_reason
|
||||
end
|
||||
|
||||
function M.timeout_path()
|
||||
local path = find_gnu_timeout()
|
||||
return path
|
||||
end
|
||||
|
||||
function M.timeout_capability()
|
||||
local path, reason = find_gnu_timeout()
|
||||
return { ok = path ~= nil, path = path, reason = reason }
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue