Merge pull request #138 from barrett-ruth/feat/timeout
Fix Timeout with GNU Timeout
This commit is contained in:
commit
7be37ad96e
7 changed files with 137 additions and 73 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
|
- [uv](https://docs.astral.sh/uv/) for problem scraping
|
||||||
- [LuaSnip](https://github.com/L3MON4D3/LuaSnip) for templates
|
- [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
|
## Quick Start
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -419,11 +419,12 @@ Status Indicators ~
|
||||||
|
|
||||||
Test cases use competitive programming terminology with color highlighting:
|
Test cases use competitive programming terminology with color highlighting:
|
||||||
|
|
||||||
AC Accepted (passed) - Green
|
AC Accepted (passed)
|
||||||
WA Wrong Answer (output mismatch) - Red
|
WA Wrong Answer (output mismatch)
|
||||||
TLE Time Limit Exceeded (timeout) - Orange
|
TLE Time Limit Exceeded (timeout)
|
||||||
MLE Memory Limit Exceeded Error (heuristic) - Orange
|
MLE Memory Limit Exceeded Error (heuristic)
|
||||||
RTE Runtime Error (other non-zero exit code) - Purple
|
RTE Runtime Error (other non-zero exit code)
|
||||||
|
NA Any other state (undecipherable, error, running)
|
||||||
<
|
<
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
|
|
@ -442,8 +443,9 @@ Test cases use competitive programming terminology with color highlighting:
|
||||||
CpTestAC Green foreground for AC status
|
CpTestAC Green foreground for AC status
|
||||||
CpTestWA Red foreground for WA status
|
CpTestWA Red foreground for WA status
|
||||||
CpTestTLE Orange foreground for TLE status
|
CpTestTLE Orange foreground for TLE status
|
||||||
|
CpTestMLE Orange foreground for MLE status
|
||||||
CpTestRTE Purple foreground for RTE status
|
CpTestRTE Purple foreground for RTE status
|
||||||
CpTestPending Gray foreground for pending tests
|
CpTestNA Gray foreground for remaining state
|
||||||
|
|
||||||
ANSI Color Groups ~
|
ANSI Color Groups ~
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,18 @@ local function check_required()
|
||||||
vim.health.info('Python virtual environment not set up (created on first scrape)')
|
vim.health.info('Python virtual environment not set up (created on first scrape)')
|
||||||
end
|
end
|
||||||
|
|
||||||
local cap = utils.time_capability()
|
local time_cap = utils.time_capability()
|
||||||
if cap.ok then
|
if time_cap.ok then
|
||||||
vim.health.ok('GNU time found: ' .. cap.path)
|
vim.health.ok('GNU time found: ' .. time_cap.path)
|
||||||
else
|
else
|
||||||
vim.health.error('GNU time not found: ' .. (cap.reason or ''))
|
vim.health.error('GNU time not found: ' .. (time_cap.reason or ''))
|
||||||
|
end
|
||||||
|
|
||||||
|
local timeout_cap = utils.time_capability()
|
||||||
|
if timeout_cap.ok then
|
||||||
|
vim.health.ok('GNU timeout found: ' .. timeout_cap.path)
|
||||||
|
else
|
||||||
|
vim.health.error('GNU timeout not found: ' .. (timeout_cap.reason or ''))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,67 +63,62 @@ function M.compile(language_config, substitutions)
|
||||||
return r
|
return r
|
||||||
end
|
end
|
||||||
|
|
||||||
local function parse_and_strip_time_v(output, memory_mb)
|
local function parse_and_strip_time_v(output)
|
||||||
local lines = vim.split(output or '', '\n', { plain = true })
|
local s = output or ''
|
||||||
|
local last_i, from = nil, 1
|
||||||
local timing_idx
|
while true do
|
||||||
for i = #lines, 1, -1 do
|
local i = string.find(s, 'Command being timed:', from, true)
|
||||||
if lines[i]:match('^%s*Command being timed:') then
|
if not i then
|
||||||
timing_idx = i
|
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
last_i, from = i, i + 1
|
||||||
end
|
end
|
||||||
if not timing_idx then
|
if not last_i then
|
||||||
while #lines > 0 and lines[#lines]:match('^%s*$') do
|
return s, 0
|
||||||
table.remove(lines, #lines)
|
|
||||||
end
|
|
||||||
return table.concat(lines, '\n'), 0, false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local start_idx = timing_idx
|
local k = last_i - 1
|
||||||
local k = timing_idx - 1
|
while k >= 1 do
|
||||||
while k >= 1 and lines[k]:match('^%s*Command ') do
|
local ch = s:sub(k, k)
|
||||||
start_idx = k
|
if ch ~= ' ' and ch ~= '\t' then
|
||||||
|
break
|
||||||
|
end
|
||||||
k = k - 1
|
k = k - 1
|
||||||
end
|
end
|
||||||
|
|
||||||
local peak_mb, mled = 0, false
|
local head = s:sub(1, k)
|
||||||
for j = timing_idx, #lines do
|
local tail = s:sub(last_i)
|
||||||
local kb = lines[j]:match('Maximum resident set size %(kbytes%):%s*(%d+)')
|
|
||||||
|
local peak_kb = 0.0
|
||||||
|
for line in tail:gmatch('[^\n]+') do
|
||||||
|
local kb = line:match('Maximum resident set size %(kbytes%):%s*(%d+)')
|
||||||
if kb then
|
if kb then
|
||||||
peak_mb = tonumber(kb) / 1024.0
|
peak_kb = tonumber(kb) or 0
|
||||||
if memory_mb and memory_mb > 0 and peak_mb > memory_mb then
|
|
||||||
mled = true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
for j = #lines, start_idx, -1 do
|
local peak_mb = peak_kb / 1024.0
|
||||||
table.remove(lines, j)
|
head = head:gsub('\n+$', '')
|
||||||
end
|
return head, peak_mb
|
||||||
|
|
||||||
while #lines > 0 and lines[#lines]:match('^%s*$') do
|
|
||||||
table.remove(lines, #lines)
|
|
||||||
end
|
|
||||||
|
|
||||||
return table.concat(lines, '\n'), peak_mb, mled
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.run(cmd, stdin, timeout_ms, memory_mb)
|
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 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 t0 = vim.uv.hrtime()
|
||||||
local r = vim
|
local r = vim
|
||||||
.system({ 'sh', '-c', sh }, {
|
.system({ 'sh', '-c', sh }, {
|
||||||
stdin = stdin,
|
stdin = stdin,
|
||||||
timeout = timeout_ms,
|
|
||||||
text = true,
|
text = true,
|
||||||
})
|
})
|
||||||
:wait()
|
:wait()
|
||||||
|
|
@ -142,17 +137,12 @@ function M.run(cmd, stdin, timeout_ms, memory_mb)
|
||||||
local lower = (cleaned or ''):lower()
|
local lower = (cleaned or ''):lower()
|
||||||
local oom_hint = lower:find('std::bad_alloc', 1, true)
|
local oom_hint = lower:find('std::bad_alloc', 1, true)
|
||||||
or lower:find('cannot allocate memory', 1, true)
|
or lower:find('cannot allocate memory', 1, true)
|
||||||
|
or lower:find('out of memory', 1, true)
|
||||||
|
or lower:find('oom', 1, true)
|
||||||
or lower:find('enomem', 1, true)
|
or lower:find('enomem', 1, true)
|
||||||
|
local near_cap = peak_mb >= (0.90 * memory_mb)
|
||||||
|
|
||||||
local near_cap = false
|
local mled = (peak_mb >= memory_mb) or near_cap or (oom_hint and not tled)
|
||||||
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
|
if tled then
|
||||||
logger.log(('Execution timed out in %.1fms.'):format(dt))
|
logger.log(('Execution timed out in %.1fms.'):format(dt))
|
||||||
|
|
|
||||||
|
|
@ -122,9 +122,8 @@ local function run_single_test_case(contest_config, cp_config, test_case)
|
||||||
|
|
||||||
local cmd = build_command(language_config, substitutions)
|
local cmd = build_command(language_config, substitutions)
|
||||||
local stdin_content = (test_case.input or '') .. '\n'
|
local stdin_content = (test_case.input or '') .. '\n'
|
||||||
local timeout_ms = (run_panel_state.constraints and run_panel_state.constraints.timeout_ms)
|
local timeout_ms = (run_panel_state.constraints and run_panel_state.constraints.timeout_ms) or 0
|
||||||
or 2000
|
local memory_mb = run_panel_state.constraints and run_panel_state.constraints.memory_mb or 0
|
||||||
local memory_mb = run_panel_state.constraints and run_panel_state.constraints.memory_mb or nil
|
|
||||||
|
|
||||||
local r = exec.run(cmd, stdin_content, timeout_ms, memory_mb)
|
local r = exec.run(cmd, stdin_content, timeout_ms, memory_mb)
|
||||||
|
|
||||||
|
|
@ -184,7 +183,7 @@ local function run_single_test_case(contest_config, cp_config, test_case)
|
||||||
signal = signal,
|
signal = signal,
|
||||||
tled = r.tled or false,
|
tled = r.tled or false,
|
||||||
mled = r.mled or false,
|
mled = r.mled or false,
|
||||||
rss_mb = r.peak_mb,
|
rss_mb = r.peak_mb or 0,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -284,6 +283,7 @@ function M.handle_compilation_failure(output)
|
||||||
tc.signal = ''
|
tc.signal = ''
|
||||||
tc.tled = false
|
tc.tled = false
|
||||||
tc.mled = false
|
tc.mled = false
|
||||||
|
tc.rss_mb = 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,19 +30,17 @@ function M.get_status_info(ran_test_case)
|
||||||
return { text = 'AC', highlight_group = 'CpTestAC' }
|
return { text = 'AC', highlight_group = 'CpTestAC' }
|
||||||
end
|
end
|
||||||
|
|
||||||
if ran_test_case.actual == '' then
|
|
||||||
return { text = '...', highlight_group = 'CpTestPending' }
|
|
||||||
end
|
|
||||||
|
|
||||||
if ran_test_case.tled then
|
if ran_test_case.tled then
|
||||||
return { text = 'TLE', highlight_group = 'CpTestTLE' }
|
return { text = 'TLE', highlight_group = 'CpTestTLE' }
|
||||||
elseif ran_test_case.mled then
|
elseif ran_test_case.mled then
|
||||||
return { text = 'MLE', highlight_group = 'CpTestMLE' }
|
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' }
|
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' }
|
return { text = 'WA', highlight_group = 'CpTestWA' }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
return { text = 'N/A', highlight_group = 'CpTestNA' }
|
||||||
end
|
end
|
||||||
|
|
||||||
local function format_exit_code(code)
|
local function format_exit_code(code)
|
||||||
|
|
@ -356,7 +354,7 @@ function M.get_highlight_groups()
|
||||||
CpTestTLE = { fg = '#f59e0b' },
|
CpTestTLE = { fg = '#f59e0b' },
|
||||||
CpTestMLE = { fg = '#f59e0b' },
|
CpTestMLE = { fg = '#f59e0b' },
|
||||||
CpTestRTE = { fg = '#8b5cf6' },
|
CpTestRTE = { fg = '#8b5cf6' },
|
||||||
CpTestPending = { fg = '#6b7280' },
|
CpTestNA = { fg = '#6b7280' },
|
||||||
CpDiffRemoved = { fg = '#ef4444', bg = '#1f1f1f' },
|
CpDiffRemoved = { fg = '#ef4444', bg = '#1f1f1f' },
|
||||||
CpDiffAdded = { fg = '#10b981', bg = '#1f1f1f' },
|
CpDiffAdded = { fg = '#10b981', bg = '#1f1f1f' },
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ local uname = vim.loop.os_uname()
|
||||||
local _time_cached = false
|
local _time_cached = false
|
||||||
local _time_path = nil
|
local _time_path = nil
|
||||||
local _time_reason = nil
|
local _time_reason = nil
|
||||||
|
local _timeout_cached = false
|
||||||
|
local _timeout_path = nil
|
||||||
|
local _timeout_reason = nil
|
||||||
|
|
||||||
local function is_windows()
|
local function is_windows()
|
||||||
return uname and uname.sysname == 'Windows_NT'
|
return uname and uname.sysname == 'Windows_NT'
|
||||||
|
|
@ -146,9 +149,14 @@ function M.check_required_runtime()
|
||||||
return false, 'Neovim 0.10.0+ required'
|
return false, 'Neovim 0.10.0+ required'
|
||||||
end
|
end
|
||||||
|
|
||||||
local cap = M.time_capability()
|
local time = M.time_capability()
|
||||||
if not cap.ok then
|
if not time.ok then
|
||||||
return false, 'GNU time not found: ' .. (cap.reason or '')
|
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
|
end
|
||||||
|
|
||||||
if vim.fn.executable('uv') ~= 1 then
|
if vim.fn.executable('uv') ~= 1 then
|
||||||
|
|
@ -162,4 +170,62 @@ function M.check_required_runtime()
|
||||||
return true
|
return true
|
||||||
end
|
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
|
return M
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue